({
@@ -91,6 +92,7 @@ export default function Game(props: GameProps) {
state,
getTransform,
hoveredCell,
+ gameOver,
selectedTool,
getAnimationState,
playerID: props.playerID,
@@ -115,7 +117,7 @@ export default function Game(props: GameProps) {
getTransform,
clickTile(x: number, y: number) {
if (!props.playerID) return;
- if (animationRunning()) return;
+ if (animationRunning() || gameOver()) return;
const tool = selectedTool();
@@ -159,6 +161,8 @@ export default function Game(props: GameProps) {
startAnimation();
}
+ setGameOver(state.ctx.gameover ?? false);
+
// For some ungodly reason, I need to clone the state, because otherwise the frozenness sporadically gets
// 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
@@ -196,7 +200,7 @@ export default function Game(props: GameProps) {
}}
/>
- Turn {turn()}
+ Turn {turn()}{gameOver() ? "- Game over" : ""}
diff --git a/src/components/Game/useDrawBoard.ts b/src/components/Game/useDrawBoard.ts
index c70840e..104ecfe 100644
--- a/src/components/Game/useDrawBoard.ts
+++ b/src/components/Game/useDrawBoard.ts
@@ -14,6 +14,7 @@ export type UseDrawBoardOptions = {
getAnimationState: Accessor>,
playerID?: string | undefined,
targetTiles: Accessor<[x: number, y: number][]>,
+ gameOver: Accessor,
};
export default function useDrawBoard(options: UseDrawBoardOptions) {
@@ -25,7 +26,8 @@ export default function useDrawBoard(options: UseDrawBoardOptions) {
selectedTool,
getAnimationState,
playerID,
- targetTiles
+ targetTiles,
+ gameOver
} = options;
const [tileImages] = createResource(loadTileImages);
@@ -54,7 +56,7 @@ export default function useDrawBoard(options: UseDrawBoardOptions) {
const transform = getTransform();
const transformedCtx = new draw.TransformedCanvas2DCtx(ctx, transform);
- if (hoveredCell() && !animationRunning()) {
+ if (hoveredCell() && !animationRunning() && !gameOver()) {
const [x, y] = hoveredCell()!;
const tool = selectedTool();
let valid = true;
diff --git a/src/game.ts b/src/game.ts
index 438ac3f..62370f3 100644
--- a/src/game.ts
+++ b/src/game.ts
@@ -1,5 +1,6 @@
import type { Game } from "boardgame.io";
import { INVALID_MOVE } from "boardgame.io/core";
+import hexNeighborhood from "./utils/hexNeighborhood.ts";
// TODO: partial information
@@ -60,7 +61,7 @@ export const Buildings = {
description: "Your first line of defense: has good HP and can attack a single adjacent tile to prevent enemy expansion to the border of your territory.",
humanName: "Defender",
cost: 2,
- hp: 2,
+ hp: 3,
placedOn: ["road"],
attack: {
power: 1,
@@ -85,23 +86,27 @@ export const Buildings = {
},
laser: {
name: "laser",
- description: "A long-range defensive unit, able to cause moderate damage to 3 tiles in a row",
+ description: "A long-range defensive unit, able to cause moderate damage to 2 tiles in a row. Does not pierce",
humanName: "Laser",
- cost: 5,
- hp: 2,
+ cost: 4,
+ hp: 3,
placedOn: ["road", "defender"],
attack: {
power: 2,
- cost: 2,
+ cost: 1,
maxMoves: 1,
targets(_cells, x, y) {
return [...adjacentCells(x, y)];
},
- damageZone(_cells, x, y, targetX, targetY) {
+ damageZone(cells, x, y, targetX, targetY) {
const dx = targetX - x;
const dy = targetY - y;
+ const MAX_DIST = 2;
+
+ const squares = new Array(MAX_DIST).fill(null).map((_, dist): [number, number] => [x + dx * (dist + 1), y + dy * (dist + 1)]);
+ const maxDist = squares.findIndex(([x, y]) => !!getCell(cells, x, y)?.owner);
- return new Array(3).fill(null).map((_, dist) => [x + dx * (dist + 1), y + dy * (dist + 1)]);
+ return squares.slice(0, maxDist === -1 ? MAX_DIST : maxDist + 1);
}
}
},
@@ -113,7 +118,37 @@ export const Buildings = {
gain: 2,
hp: 2,
placedOn: ["road"]
- }
+ },
+ wall: {
+ name: "wall",
+ description: "A strong defensive unit, that is unable to attack.",
+ humanName: "Wall",
+ cost: 3,
+ hp: 5,
+ placedOn: ["road", "defender"]
+ },
+ artillery: {
+ name: "artillery",
+ description: "A long-range unit, capable of shooting over defenses for moderate damage up to 3 tiles away.",
+ humanName: "Artillery",
+ cost: 6,
+ hp: 1,
+ placedOn: ["road", "defender"],
+ attack: {
+ cost: 2,
+ power: 2,
+ maxMoves: 1,
+ targets(_cells, x, y) {
+ return [
+ ...hexNeighborhood(x, y, 2),
+ ...hexNeighborhood(x, y, 3),
+ ];
+ },
+ damageZone(_cells, _x, _y, targetX, targetY) {
+ return [[targetX, targetY]];
+ }
+ }
+ },
} as const satisfies Readonly, SetupData> = {
}
applyMoves(ctx.G);
+
+ if (isGameOver(ctx.G)) {
+ ctx.events.endGame();
+ }
},
endIf(ctx) {
if (!ctx.ctx.activePlayers) return false;
@@ -752,3 +791,13 @@ export function getLocalState(state: State, playerID: string): State["cells"] {
return res;
}
+
+export function isGameOver(state: State): boolean {
+ const playersAlive: string[] = [];
+ for (const [_x, _y, cell] of iterateCells(state.cells)) {
+ if (cell.owner && cell.building === "base") {
+ if (!playersAlive.includes(cell.owner)) playersAlive.push(cell.owner);
+ }
+ }
+ return playersAlive.length <= 1;
+}
diff --git a/src/utils/hexNeighborhood.ts b/src/utils/hexNeighborhood.ts
new file mode 100644
index 0000000..8ecd64a
--- /dev/null
+++ b/src/utils/hexNeighborhood.ts
@@ -0,0 +1,39 @@
+import { adjacentCells } from "../game.ts";
+
+type Neighborhood = [x: number, y: number][];
+
+const neighborhoodCache: Neighborhood[] = [];
+
+function offset(neighborhood: Neighborhood, cx: number, cy: number): Neighborhood {
+ return neighborhood.map(([x2, y2]) => [x2 + cx, y2 + cy]);
+}
+
+export default function hexNeighborhood(cx: number, cy: number, radius: number): Neighborhood {
+ if (neighborhoodCache[radius]) {
+ return offset(neighborhoodCache[radius]!, cx, cy);
+ } else {
+ const open: [x: number, y: number, hop: number][] = [[0, 0, 0]];
+ const closed: Set = new Set();
+ const result: Neighborhood = [];
+
+ while (open.length > 0) {
+ const [x, y, hop] = open.shift()!;
+ const key = `${x}:${y}`;
+ if (closed.has(key)) continue;
+ closed.add(key);
+
+ if (hop === radius) {
+ result.push([x, y]);
+ }
+
+ if (hop < radius) {
+ for (const [nx, ny] of adjacentCells(x, y)) {
+ open.push([nx, ny, hop + 1]);
+ }
+ }
+ }
+
+ neighborhoodCache[radius] = result;
+ return offset(result, cx, cy);
+ }
+}