janky multiplayer

main
Shad Amethyst 1 year ago
parent 849dbfed20
commit 02f70808b5

@ -17,7 +17,7 @@
"astro": "^2.3.3", "astro": "^2.3.3",
"boardgame.io": "^0.50.2", "boardgame.io": "^0.50.2",
"sass": "^1.62.1", "sass": "^1.62.1",
"solid-js": "^1.4.3", "solid-js": "^1.7.8",
"@shadryx/pptk": "latest" "@shadryx/pptk": "latest"
} }
} }

@ -0,0 +1,167 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="173.2"
height="200"
viewBox="0 0 1.732 2"
version="1.1"
id="svg7723"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="tile-laser.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7725"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
showgrid="false"
inkscape:zoom="8"
inkscape:cx="69.3125"
inkscape:cy="124.9375"
inkscape:window-width="1920"
inkscape:window-height="986"
inkscape:window-x="0"
inkscape:window-y="72"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs7720" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
sodipodi:type="star"
style="display:none;fill:#707070;fill-opacity:1;stroke:#4050f0;stroke-width:0.132184;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
id="path10510"
inkscape:flatsided="true"
sodipodi:sides="6"
sodipodi:cx="2.6478419"
sodipodi:cy="0.71499872"
sodipodi:r1="0.60318714"
sodipodi:r2="0.5223754"
sodipodi:arg1="0.52359878"
sodipodi:arg2="1.0471976"
inkscape:rounded="0"
inkscape:randomized="0"
d="M 3.1702173,1.0165923 2.6478419,1.3181859 2.1254665,1.0165923 l 0,-0.60318715 0.5223754,-0.30159357 0.5223754,0.30159357 z"
transform="matrix(1.471662,0,0,1.471662,-3.022182,-0.05223681)"
inkscape:label="bg" />
<path
style="fill:#3d3d3d;fill-opacity:1;stroke:none;stroke-width:0.05;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
id="path59514"
sodipodi:type="arc"
sodipodi:cx="0.5661419"
sodipodi:cy="1.2501459"
sodipodi:rx="0.025"
sodipodi:ry="0.025"
sodipodi:start="3.1415927"
sodipodi:end="0"
sodipodi:arc-type="slice"
d="m 0.5411419,1.2501459 a 0.025,0.025 0 0 1 0.025,-0.025 0.025,0.025 0 0 1 0.025,0.025 h -0.025 z"
inkscape:label="foot" />
<path
style="fill:#363636;fill-opacity:1;stroke:none;stroke-width:0.05;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
id="path59517"
inkscape:label="foot-shade"
sodipodi:type="arc"
sodipodi:cx="0.61614186"
sodipodi:cy="1.2501459"
sodipodi:rx="0.025"
sodipodi:ry="0.025"
sodipodi:start="3.1415927"
sodipodi:end="0"
sodipodi:arc-type="slice"
d="m 0.59114186,1.2501459 a 0.025,0.025 0 0 1 0.025,-0.025 0.025,0.025 0 0 1 0.025,0.025 h -0.025 z" />
<path
style="fill:none;stroke:#3d3d3d;stroke-width:0.05;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-dasharray:none"
d="M 0.59114188,0.90179679 V 1.2501459"
id="path59212"
inkscape:label="leg" />
<rect
style="fill:#363636;fill-opacity:1;stroke:none;stroke-width:0.05;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
id="rect59407"
width="0.025"
height="0.34834912"
x="0.59114188"
y="0.90179682"
ry="0"
inkscape:label="leg-shade" />
<g
id="g56116"
inkscape:label="body"
transform="translate(0,0.07665485)">
<path
style="fill:none;stroke:#3d3d3d;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="M 0.59114188,0.72514194 0.79114187,0.92514193"
id="path47832"
sodipodi:nodetypes="cc"
inkscape:label="main-body" />
<circle
style="fill:#3d3d3d;fill-opacity:1;stroke:none;stroke-width:0.00238367;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
id="circle47776"
cx="0.59114188"
cy="0.72514194"
r="0.1"
inkscape:label="end-back" />
<circle
style="fill:#363636;fill-opacity:1;stroke:none;stroke-width:0.00238368;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
id="path46160"
cx="0.79114187"
cy="0.92514193"
r="0.1"
inkscape:label="end-front" />
</g>
<path
style="fill:none;stroke:#e6e6e6;stroke-width:0.05;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="M 0.79114187,1.0017968 1.0394909,1.2501459"
id="path50811"
inkscape:label="main-laser" />
<g
id="g56122"
inkscape:label="streaks"
transform="translate(0,0.07665485)">
<path
style="fill:none;stroke:#e6e6e6;stroke-width:0.02;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 1.113313,1.173491 h 0.05"
id="path51543"
inkscape:label="streak-4" />
<path
style="fill:none;stroke:#e6e6e6;stroke-width:0.02;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="M 1.0918149,1.121167 1.1271699,1.085812"
id="path51681"
inkscape:label="streak-3" />
<path
style="fill:none;stroke:#e6e6e6;stroke-width:0.02;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 1.0391385,1.0993165 v -0.05"
id="path51541"
inkscape:label="streak-2" />
<path
style="fill:none;stroke:#e6e6e6;stroke-width:0.02;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 0.914964,1.173491 h 0.05"
id="path51543-1"
inkscape:label="streak-1" />
</g>
<circle
style="fill:#525252;fill-opacity:1;stroke:none;stroke-width:0.02;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
id="path56247"
cx="0.6911419"
cy="0.90179682"
r="0.015207117" />
<circle
style="fill:#525252;fill-opacity:1;stroke:none;stroke-width:0.02;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
id="circle59207"
cx="0.66072768"
cy="0.87138259"
r="0.015207117" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

@ -0,0 +1,9 @@
import { Server, Origins } from "boardgame.io/server";
import { AcrossTheHex } from "./src/game.js";
const server = Server({
games: [AcrossTheHex],
origins: [Origins.LOCALHOST]
});
server.run(3100);

@ -0,0 +1,16 @@
function isPlainObject(value: unknown): value is Record<string | number, unknown> {
if (!value) return false;
return [undefined, Object as any].includes(value.constructor);
}
export default function clone<T>(value: T): T {
if (typeof value !== "object" || value === null) return value;
else if (Array.isArray(value)) {
return value.map(clone) as T;
} else if (isPlainObject(value)) {
return Object.fromEntries(Object.entries(value).map(([key, value]) => [key, clone(value)])) as T;
} else {
console.warn(`Cannot clone ${value}!`);
return value;
}
}

@ -1,33 +1,56 @@
import { Show, createEffect, createMemo, createResource, createSignal, onCleanup } from "solid-js"; import { Show, createEffect, createMemo, createResource, createSignal, onCleanup } from "solid-js";
import { Client } from "boardgame.io/client"; import { Client } from "boardgame.io/client";
import { Local } from "boardgame.io/multiplayer"; import { Local, SocketIO } from "boardgame.io/multiplayer";
import { AcrossTheHex, Buildings, Cell, Moves, State, adjacentCells, canAttack, canAttackFrom, canPlaceBuilding, cellIn, getCell, getLocalState, getResourceGain, iterateCells, remainingResources } from "../game.js"; import * as game from "../game.js";
// import type { State as GIOState } from "boardgame.io";
import { createStore, reconcile } from "solid-js/store"; import { createStore, reconcile } from "solid-js/store";
import { PixelPerfectCanvas } from "@shadryx/pptk/solid"; import { PixelPerfectCanvas } from "@shadryx/pptk/solid";
import Toolbar, { Tool } from "./Toolbar.jsx"; import Toolbar, { Tool } from "./Toolbar.jsx";
import { GHOST_COLORS, PLAYER_COLORS } from "../consts.js"; import { GHOST_COLORS, PLAYER_COLORS, SOCKETIO_SERVER } from "../consts.js";
import { TileImages, loadTileImages } from "./tiles.js"; import { TileImages, loadTileImages } from "./tiles.js";
import clone from "../clone.js";
export type GameProps = { export type GameProps = ({
playerID: string, playerID: string,
solo?: boolean, solo: true,
} | {
playerID: string,
solo?: false,
matchID: string,
} | {
playerID?: undefined,
solo?: false,
matchID: string,
}) & {
debug?: boolean
}; };
export default function Game(props: GameProps) { export default function Game(props: GameProps) {
const client = Client({ const client = Client({
game: AcrossTheHex, game: game.AcrossTheHex,
multiplayer: Local(), multiplayer: props.solo ? Local() : SocketIO({
playerID: props.playerID server: SOCKETIO_SERVER
}),
debug: props.debug ?? false,
...("matchID" in props ? { matchID: props.matchID } : {}),
...(props.playerID ? { playerID: props.playerID } : {})
}); });
const [state, setState] = createStore<State>(client.getState()?.G ?? { const [state, setState] = createStore<game.State>(clone(client.getState()?.G) ?? {
resources: {}, resources: {},
cells: {}, cells: {},
moves: {} moves: {}
}); });
const [soloTurn, setSoloTurn] = createSignal<string>(""); const [soloTurn, setSoloTurn] = createSignal<string>("");
const [stage, setStage] = createSignal<string>(); const [stage, setStage] = createSignal<string>();
const cells = createMemo(() => "cells" in state ? getLocalState(state, props.playerID) : {});
const cells = createMemo(() => {
if ("cells" in state && props.playerID) {
return game.getLocalState(state, props.playerID);
} else if ("cells" in state) {
return state.cells;
} else {
return {};
}
});
const [hoveredCell, setHoveredCell] = createSignal<[number, number] | null>(null); const [hoveredCell, setHoveredCell] = createSignal<[number, number] | null>(null);
const [selectedTool, setSelectedTool] = createSignal<Tool>({ const [selectedTool, setSelectedTool] = createSignal<Tool>({
type: "placeBuilding", type: "placeBuilding",
@ -42,18 +65,18 @@ export default function Game(props: GameProps) {
const targetTiles = createMemo(() => { const targetTiles = createMemo(() => {
const tool = selectedTool(); const tool = selectedTool();
if (tool.type !== "attack" || !tool.selected) return []; if (tool.type !== "attack" || !tool.selected || !props.playerID) return [];
const building = getCell(state.cells, tool.selected.x, tool.selected.y); const building = game.getCell(state.cells, tool.selected.x, tool.selected.y);
if (building?.owner !== props.playerID) return []; if (building?.owner !== props.playerID) return [];
const buildingRules = Buildings[building.building]; const buildingRules = game.Buildings[building.building];
if (!("attack" in buildingRules)) return []; if (!("attack" in buildingRules)) return [];
return buildingRules.attack.targets(state.cells, tool.selected.x, tool.selected.y) return buildingRules.attack.targets(state.cells, tool.selected.x, tool.selected.y)
.filter(([x, y]) => cellIn(state.cells, x, y)); .filter(([x, y]) => game.cellIn(state.cells, x, y));
}); });
const moves = () => client.moves as Moves; const moves = () => client.moves as game.Moves;
const getTransform = createMemo(() => { const getTransform = createMemo(() => {
if (!("cells" in state)) return { if (!("cells" in state)) return {
@ -89,13 +112,15 @@ export default function Game(props: GameProps) {
const images = tileImages(); const images = tileImages();
if (!ctx || !("cells" in state) || !images) return; if (!ctx || !("cells" in state) || !images) return;
ctx.imageSmoothingQuality = "high";
ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
const transform = getTransform(); const transform = getTransform();
ctx.setTransform(transform.scale, 0, 0, transform.scale, transform.sx, transform.sy); ctx.setTransform(transform.scale, 0, 0, transform.scale, transform.sx, transform.sy);
for (const [x, y, cell] of iterateCells(cells())) { for (const [x, y, cell] of game.iterateCells(cells())) {
drawCell(ctx, cell, x, y, images); drawCell(ctx, cell, x, y, images);
} }
@ -104,10 +129,11 @@ export default function Game(props: GameProps) {
const tool = selectedTool(); const tool = selectedTool();
let valid = true; let valid = true;
if (props.playerID) {
if (tool.type === "placeBuilding") { if (tool.type === "placeBuilding") {
valid = canPlaceBuilding( valid = game.canPlaceBuilding(
state.cells, state.cells,
remainingResources(state.resources, state.moves, props.playerID), game.remainingResources(state.cells, state.resources, state.moves, props.playerID),
props.playerID, props.playerID,
x, x,
y, y,
@ -116,14 +142,15 @@ export default function Game(props: GameProps) {
} else if (tool.type === "attack" && tool.selected) { } else if (tool.type === "attack" && tool.selected) {
valid = !!targetTiles().find(([x2, y2]) => x2 === x && y2 === y); valid = !!targetTiles().find(([x2, y2]) => x2 === x && y2 === y);
} else if (tool.type === "attack" && !tool.selected) { } else if (tool.type === "attack" && !tool.selected) {
valid = canAttackFrom( valid = game.canAttackFrom(
state.cells, state.cells,
remainingResources(state.resources, state.moves, props.playerID), game.remainingResources(state.cells, state.resources, state.moves, props.playerID),
props.playerID, props.playerID,
x, x,
y y
); );
} }
}
drawCellOutline(ctx, x, y, valid); drawCellOutline(ctx, x, y, valid);
} }
@ -133,15 +160,17 @@ export default function Game(props: GameProps) {
} }
function clickTile(x: number, y: number) { function clickTile(x: number, y: number) {
if (!props.playerID) return;
const tool = selectedTool(); const tool = selectedTool();
if (tool.type === "placeBuilding") { if (tool.type === "placeBuilding") {
moves().placeBuilding?.(x, y, tool.building); moves().placeBuilding?.(x, y, tool.building);
} else if (tool.type === "attack") { } else if (tool.type === "attack") {
if (tool.selected) { if (tool.selected) {
if (canAttack( if (game.canAttack(
state.cells, state.cells,
remainingResources(state.resources, state.moves, props.playerID), game.remainingResources(state.cells, state.resources, state.moves, props.playerID),
props.playerID, props.playerID,
tool.selected.x, tool.selected.x,
tool.selected.y, tool.selected.y,
@ -165,17 +194,21 @@ export default function Game(props: GameProps) {
} }
} }
} }
client.start(); client.start();
client.subscribe((state) => { client.subscribe((state) => {
if (state) { if (state) {
setState(reconcile(state.G)); // For some ungodly reason, I need to clone the state, because otherwise the frozenness sporadically gets
setStage(state.ctx.activePlayers![props.playerID]); // saved in the store, triggering various issues later down the line.
// My guess for as to why is that `state.G` can be mutated and some of its properties are passed to Object.freeze
setState(reconcile(clone(state.G)));
if (props.playerID) {
setStage(state.ctx.activePlayers?.[props.playerID] ?? "connecting");
}
if (props.solo) { if (props.solo) {
const activePlayers = Object.entries(state.ctx.activePlayers ?? {}) const activePlayers = Object.entries(state.ctx.activePlayers ?? {})
.flatMap(([player, state]) => state === "prepare" ? [player] : []) .flatMap(([player, state]) => state === "prepare" ? [player] : [])
.sort((a, b) => +b - +a); .sort((a, b) => +b - +a);
setSoloTurn(activePlayers[activePlayers.length - 1] ?? ""); setSoloTurn(activePlayers[activePlayers.length - 1] ?? "0");
} }
} }
}); });
@ -197,7 +230,7 @@ export default function Game(props: GameProps) {
const y = ((event.clientY - bounds.top) / window.devicePixelRatio - transform.sy) / transform.scale; const y = ((event.clientY - bounds.top) / window.devicePixelRatio - transform.sy) / transform.scale;
const [hexX, hexY] = inverseHexPosition(x, y); const [hexX, hexY] = inverseHexPosition(x, y);
if ("cells" in state && cellIn(state.cells, hexX, hexY)) { if ("cells" in state && game.cellIn(state.cells, hexX, hexY)) {
return [hexX, hexY]; return [hexX, hexY];
} else { } else {
return null; return null;
@ -235,19 +268,23 @@ export default function Game(props: GameProps) {
draw(); draw();
}} }}
style={{ style={{
width: "400px", width: "600px",
height: "400px", height: "600px",
}} }}
/> />
<Toolbar <Show when={props.playerID}>
playerID={props.playerID} {(playerID) => {
return <Toolbar
playerID={playerID()}
selectedTool={selectedTool} selectedTool={selectedTool}
setSelectedTool={setSelectedTool} setSelectedTool={setSelectedTool}
resources={() => "resources" in state ? remainingResources(state.resources, state.moves, props.playerID) : 0} resources={() => "resources" in state ? game.remainingResources(state.cells, state.resources, state.moves, playerID()) : 0}
resourceGain={() => "cells" in state ? getResourceGain(state.cells, props.playerID) : 0} resourceGain={() => "cells" in state ? game.getResourceGain(state.cells, playerID()) : 0}
setReady={() => moves().setReady?.()} setReady={() => moves().setReady?.()}
ready={() => stage() === "ready"} ready={() => stage() === "ready"}
/> />;
}}
</Show>
</Show> </Show>
</div>; </div>;
} }
@ -288,7 +325,7 @@ function inverseHexPosition(spatialX: number, spatialY: number): [hexX: number,
let closest = [guessHexX, guessHexY, distance(guessHexX, guessHexY)] as const; let closest = [guessHexX, guessHexY, distance(guessHexX, guessHexY)] as const;
for (const [neighborX, neighborY] of adjacentCells(guessHexX, guessHexY)) { for (const [neighborX, neighborY] of game.adjacentCells(guessHexX, guessHexY)) {
const dist = distance(neighborX, neighborY); const dist = distance(neighborX, neighborY);
if (dist < closest[2]) { if (dist < closest[2]) {
closest = [neighborX, neighborY, dist]; closest = [neighborX, neighborY, dist];
@ -298,7 +335,7 @@ function inverseHexPosition(spatialX: number, spatialY: number): [hexX: number,
return [closest[0], closest[1]]; return [closest[0], closest[1]];
} }
function getBounds(grid: Record<string, Cell>) { function getBounds(grid: Record<string, game.Cell>) {
let res = { let res = {
minX: Infinity, minX: Infinity,
maxX: -Infinity, maxX: -Infinity,
@ -306,7 +343,7 @@ function getBounds(grid: Record<string, Cell>) {
maxY: -Infinity maxY: -Infinity
}; };
for (const [x, y] of iterateCells(grid)) { for (const [x, y] of game.iterateCells(grid)) {
const [x2, y2] = getHexPosition(x, y); const [x2, y2] = getHexPosition(x, y);
res.minX = Math.min(res.minX, x2 + HEX_BOUNDS.left); res.minX = Math.min(res.minX, x2 + HEX_BOUNDS.left);
res.maxX = Math.max(res.maxX, x2 + HEX_BOUNDS.right); res.maxX = Math.max(res.maxX, x2 + HEX_BOUNDS.right);
@ -353,13 +390,13 @@ function drawCellInfoBubble(ctx: CanvasRenderingContext2D, x: number, y: number,
ctx.fill(); ctx.fill();
ctx.fillStyle = "white"; ctx.fillStyle = "white";
ctx.font = `${RADIUS}px sans-serif`; ctx.font = `${RADIUS}px Arial`;
ctx.textAlign = "center"; ctx.textAlign = "center";
ctx.textBaseline = "middle"; ctx.textBaseline = "middle";
ctx.fillText(value, dotX, dotY - 0.2); ctx.fillText(value, dotX, dotY);
} }
function drawCell(ctx: CanvasRenderingContext2D, cell: Cell, x: number, y: number, tileImages: TileImages) { function drawCell(ctx: CanvasRenderingContext2D, cell: game.Cell, x: number, y: number, tileImages: TileImages) {
const [x2, y2] = getHexPosition(x, y); const [x2, y2] = getHexPosition(x, y);
let line_offset = (LINE_WIDTH + SPACING) / 2; let line_offset = (LINE_WIDTH + SPACING) / 2;
@ -392,7 +429,7 @@ function drawCell(ctx: CanvasRenderingContext2D, cell: Cell, x: number, y: numbe
drawCellImage(ctx, x, y, tileImages[cell.building]); drawCellImage(ctx, x, y, tileImages[cell.building]);
} }
if (cell.owner && !cell.ghost && (cell.hp !== Buildings[cell.building].hp || true)) { if (cell.owner && !cell.ghost && (cell.hp !== game.Buildings[cell.building].hp || true)) {
drawCellInfoBubble( drawCellInfoBubble(
ctx, ctx,
x, x,

@ -0,0 +1,26 @@
---
import Game from "./Game.tsx";
export type Props = {
playerID: string | undefined,
matchID: string,
debug: boolean,
};
---
{
Astro.props.playerID
? <Game
playerID={Astro.props.playerID}
matchID={Astro.props.matchID}
debug={Astro.props.debug}
client:only
/>
: <Game
matchID={Astro.props.matchID}
debug={Astro.props.debug}
client:only
/>
}
<!-- <GameStatus playerID={Astro.props.playerID} matchID={Astro.props.matchID} /> -->

@ -0,0 +1,11 @@
---
import Game from "./Game.tsx";
export type Props = {
debug: boolean
};
---
<Game playerID="0" solo debug={Astro.props.debug} client:only />
<Game playerID="1" solo client:only />

@ -2,3 +2,4 @@
export const PLAYER_COLORS = ["#f06040", "#4050f0"]; export const PLAYER_COLORS = ["#f06040", "#4050f0"];
export const GHOST_COLORS = ["#c05a47", "#4750c0"]; export const GHOST_COLORS = ["#c05a47", "#4750c0"];
export const BASE_URL = "/"; export const BASE_URL = "/";
export const SOCKETIO_SERVER = "localhost:3100";

@ -81,6 +81,28 @@ export const Buildings = {
} }
} }
}, },
laser: {
name: "laser",
description: "A long-range defensive unit, able to cause moderate damage to 3 tiles in a row",
humanName: "Laser",
cost: 5,
hp: 2,
placedOn: ["road", "defender"],
attack: {
power: 2,
cost: 2,
maxMoves: 1,
targets(_cells, x, y) {
return [...adjacentCells(x, y)];
},
damageZone(_cells, x, y, targetX, targetY) {
const dx = targetX - x;
const dy = targetY - y;
return new Array(3).fill(null).map((_, dist) => [x + dx * (dist + 1), y + dy * (dist + 1)]);
}
}
},
factory: { factory: {
name: "factory", name: "factory",
description: "Gives a steady income of resources, but will explode the adjacent tiles if destroyed.", description: "Gives a steady income of resources, but will explode the adjacent tiles if destroyed.",
@ -118,13 +140,14 @@ export type Moves = Partial<MapMoves<{
attack: typeof attack, attack: typeof attack,
placeBuilding: typeof placeBuilding, placeBuilding: typeof placeBuilding,
setReady: (state: {}) => void, setReady: (state: {}) => void,
setConnected: (state: {}) => void,
}>>; }>>;
export const AcrossTheHex: Game<State, Record<string, unknown>, { export const AcrossTheHex: Game<State, Record<string, unknown>, {
size?: number, size?: number,
initialResources?: number initialResources?: number
}> = { }> = {
setup({ ctx }, { size = 3, initialResources = 2 } = {}) { setup({ ctx }, { size = 4, initialResources = 2 } = {}) {
const cells = initGrid(size); const cells = initGrid(size);
const players = new Array(ctx.numPlayers).fill(null).map((_, id) => id.toString()); const players = new Array(ctx.numPlayers).fill(null).map((_, id) => id.toString());
@ -146,7 +169,6 @@ export const AcrossTheHex: Game<State, Record<string, unknown>, {
moves: emptyMoves(players), moves: emptyMoves(players),
}; };
}, },
turn: { turn: {
onBegin(ctx) { onBegin(ctx) {
// Dispatch all players into the prepare stage // Dispatch all players into the prepare stage
@ -169,11 +191,20 @@ export const AcrossTheHex: Game<State, Record<string, unknown>, {
prepare: { prepare: {
next: "ready", next: "ready",
moves: { moves: {
setReady(ctx) { setReady: {
move (ctx) {
ctx.events.endStage(); ctx.events.endStage();
}, },
placeBuilding, ignoreStaleStateID: true,
attack, },
placeBuilding: {
move: placeBuilding,
ignoreStaleStateID: true
},
attack: {
move: attack,
ignoreStaleStateID: true
},
}, },
}, },
ready: { ready: {
@ -183,6 +214,20 @@ export const AcrossTheHex: Game<State, Record<string, unknown>, {
} }
} as const; } as const;
function getCostForBuilding(
existingCell: Cell,
building: PlaceableBuildings
): number {
const buildingRules = Buildings[building];
if (!existingCell.owner) return buildingRules.cost;
const existingBuilding = Buildings[existingCell.building];
if (existingBuilding.name === "road" || !("cost" in existingBuilding)) return buildingRules.cost;
return buildingRules.cost - existingBuilding.cost;
}
export function canPlaceBuilding( export function canPlaceBuilding(
cells: Record<string, Cell>, cells: Record<string, Cell>,
resources: number, resources: number,
@ -213,7 +258,8 @@ export function canPlaceBuilding(
} }
// Cannot place a building without enough resources // Cannot place a building without enough resources
if (resources < buildingRules.cost) return false; const cost = getCostForBuilding(existingCell, buildingRules.name);
if (resources < cost) return false;
return true; return true;
} }
@ -226,7 +272,7 @@ function placeBuilding({ G, playerID }: { G: State, playerID: string }, x: numbe
if (!canPlaceBuilding( if (!canPlaceBuilding(
G.cells, G.cells,
remainingResources(G.resources, G.moves, playerID), remainingResources(G.cells, G.resources, G.moves, playerID),
playerID, playerID,
x, x,
y, y,
@ -292,7 +338,7 @@ export function canAttack(
function attack({ G, playerID }: { G: State, playerID: string }, x: number, y: number, targetX: number, targetY: number) { function attack({ G, playerID }: { G: State, playerID: string }, x: number, y: number, targetX: number, targetY: number) {
if (!canAttack( if (!canAttack(
G.cells, G.cells,
remainingResources(G.resources, G.moves, playerID), remainingResources(G.cells, G.resources, G.moves, playerID),
playerID, playerID,
x, x,
y, y,
@ -331,12 +377,15 @@ export function getBuilding(cells: Record<string, Cell>, x: number, y: number, e
return Buildings[cell.building]; return Buildings[cell.building];
} }
export function remainingResources(resources: Record<string, number>, moves: Record<string, Move[]>, playerID: string): number { export function remainingResources(cells: Record<string, Cell>, resources: Record<string, number>, moves: Record<string, Move[]>, playerID: string): number {
let result = resources[playerID] ?? 0; let result = resources[playerID] ?? 0;
for (const move of moves[playerID] ?? []) { for (const move of moves[playerID] ?? []) {
if (move.type === "placeBuilding") { if (move.type === "placeBuilding") {
result -= Buildings[move.building].cost; const existingCell = getCell(cells, move.x, move.y);
if (existingCell) {
result -= getCostForBuilding(existingCell, move.building);
}
} else if (move.type === "attack") { } else if (move.type === "attack") {
const building = Buildings[move.building]; const building = Buildings[move.building];
if ("attack" in building) { if ("attack" in building) {
@ -498,7 +547,7 @@ function applyMoves(state: State) {
} }
const order = orders[0]!; const order = orders[0]!;
const cost = Buildings[order[1].building].cost; const cost = getCostForBuilding(getCell(previousCells, x, y)!, order[1].building);
setCell(state.cells, x, y, { setCell(state.cells, x, y, {
owner: order[0], owner: order[0],
@ -587,7 +636,5 @@ export function getLocalState(state: State, playerID: string): State["cells"] {
} }
} }
console.log(res);
return res; return res;
} }

@ -3,9 +3,9 @@ import Document from "../components/Document.astro";
import Game from "../components/Game.tsx"; import Game from "../components/Game.tsx";
export const prerender = true; export const prerender = true;
--- ---
<Document> <Document>
<Game playerID="0" solo client:only /> <h1>Across the Hex</h1>
<Game playerID="1" solo client:only />
</Document> </Document>

@ -0,0 +1,14 @@
---
import Document from "../components/Document.astro";
import Multiplayer from "../components/Multiplayer.astro";
import Singleplayer from "../components/Singleplayer.astro";
const playerID = Astro.url.searchParams.get("player") ?? undefined;
const matchID = Astro.url.searchParams.get("match");
const debug = Astro.url.searchParams.has("debug");
---
<Document>
{matchID ? <Multiplayer playerID={playerID} matchID={matchID} debug={debug} /> : <Singleplayer debug={debug} />}
</Document>

@ -4,6 +4,7 @@
"strictNullChecks": true, "strictNullChecks": true,
"jsx": "preserve", "jsx": "preserve",
"jsxImportSource": "solid-js", "jsxImportSource": "solid-js",
"moduleResolution": "NodeNext" "moduleResolution": "NodeNext",
"allowImportingTsExtensions": true
} }
} }

Loading…
Cancel
Save