commit
fce9712698
@ -0,0 +1,3 @@
|
||||
.env
|
||||
node_modules/
|
||||
src/env.d.ts
|
@ -0,0 +1,18 @@
|
||||
import { fileURLToPath } from "url";
|
||||
import { defineConfig } from "astro/config";
|
||||
import solidJs from "@astrojs/solid-js";
|
||||
|
||||
import node from "@astrojs/node";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: "https://www.shadamethyst.xyz/",
|
||||
integrations: [solidJs()],
|
||||
// This seems to still put everything in `workspace:dist/website/dist/`,
|
||||
// but I can live with the appended `dist` folder
|
||||
outDir: fileURLToPath(new URL(import.meta.env["OUT_DIR"] ?? "./dist/", import.meta.url)),
|
||||
output: "server",
|
||||
adapter: node({
|
||||
mode: "middleware"
|
||||
})
|
||||
});
|
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "across-the-hex",
|
||||
"version": "1.0.0",
|
||||
"description": "A multiplayer strategy game played on a hex grid, focused on expansion and extermination",
|
||||
"main": "index.js",
|
||||
"author": "Amethyst System",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"lint": "astro check",
|
||||
"build": "astro build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^5.1.2",
|
||||
"@astrojs/solid-js": "^2.1.1",
|
||||
"astro": "^2.3.3",
|
||||
"boardgame.io": "^0.50.2",
|
||||
"sass": "^1.62.1",
|
||||
"solid-js": "^1.4.3",
|
||||
"@shadryx/pptk": "latest"
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
---
|
||||
|
||||
export type Props = {
|
||||
title?: string,
|
||||
};
|
||||
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Across the Hex{Astro.props.title ? "- " + Astro.props.title : ""}</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100svh;
|
||||
overflow-x: hidden;
|
||||
background: #202020;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,271 @@
|
||||
import { createEffect, createMemo, createSignal, onCleanup } from "solid-js";
|
||||
import { Client } from "boardgame.io/client";
|
||||
import { Local } from "boardgame.io/multiplayer";
|
||||
import { AcrossTheHex, Cell, State, adjacentCells, cellIn, getLocalState, iterateCells } from "../game.js";
|
||||
// import type { State as GIOState } from "boardgame.io";
|
||||
import { createStore, reconcile } from "solid-js/store";
|
||||
import { PixelPerfectCanvas } from "@shadryx/pptk/solid";
|
||||
|
||||
export type GameProps = {
|
||||
playerID: string
|
||||
};
|
||||
|
||||
const PLAYER_COLORS = ["#f06040", "#4050f0"];
|
||||
|
||||
export default function Game(props: GameProps) {
|
||||
const client = Client({
|
||||
game: AcrossTheHex,
|
||||
multiplayer: Local(),
|
||||
playerID: props.playerID
|
||||
});
|
||||
const [state, setState] = createStore<State | {}>(client.getState() ?? {});
|
||||
const [stage, setStage] = createSignal<string>();
|
||||
const cells = createMemo(() => "cells" in state ? getLocalState(state, props.playerID) : {});
|
||||
const [hoveredCell, setHoveredCell] = createSignal<[number, number] | null>(null);
|
||||
|
||||
const [canvas, setCanvas] = createSignal<HTMLCanvasElement>();
|
||||
|
||||
const [width, setWidth] = createSignal(0);
|
||||
const [height, setHeight] = createSignal(0);
|
||||
|
||||
const getTransform = createMemo(() => {
|
||||
if (!("cells" in state)) return {
|
||||
scale: 1,
|
||||
sx: 0,
|
||||
sy: 0,
|
||||
};
|
||||
|
||||
const bounds = getBounds(state.cells);
|
||||
|
||||
const scale = Math.min(width() / (bounds.maxX - bounds.minX), height() / (bounds.maxY - bounds.minY));
|
||||
const sx = width() / 2 - (bounds.maxX - bounds.minX) * scale / 2 - bounds.minX * scale;
|
||||
const sy = height() / 2 - (bounds.maxY - bounds.minY) * scale / 2 - bounds.minY * scale;
|
||||
|
||||
return {
|
||||
scale,
|
||||
sx,
|
||||
sy
|
||||
};
|
||||
}, {
|
||||
scale: 1,
|
||||
sx: 0,
|
||||
sy: 0,
|
||||
},
|
||||
{
|
||||
equals(prev, next) {
|
||||
return prev.scale === next.scale && prev.sx === next.sx && prev.sy === next.sy
|
||||
}
|
||||
});
|
||||
|
||||
function draw() {
|
||||
const ctx = canvas()?.getContext("2d");
|
||||
if (!ctx || !("cells" in state)) return;
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
const transform = getTransform();
|
||||
|
||||
ctx.setTransform(transform.scale, 0, 0, transform.scale, transform.sx, transform.sy);
|
||||
|
||||
for (const [x, y, cell] of iterateCells(cells())) {
|
||||
drawCell(ctx, cell, x, y);
|
||||
}
|
||||
|
||||
if (hoveredCell()) {
|
||||
const [x, y] = hoveredCell()!;
|
||||
drawCellOutline(ctx, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
function clickTile(x: number, y: number) {
|
||||
client.moves.placeBuilding?.(x, y, "pawn");
|
||||
}
|
||||
|
||||
client.start();
|
||||
client.subscribe((state) => {
|
||||
if (state) {
|
||||
setState(reconcile(state.G));
|
||||
setStage(state.ctx.activePlayers![props.playerID]);
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
draw();
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const c = canvas();
|
||||
|
||||
if (!c) return;
|
||||
|
||||
function getTileBelowMouse(event: MouseEvent): [number, number] | null {
|
||||
const transform = getTransform();
|
||||
const bounds = c!.getBoundingClientRect();
|
||||
|
||||
const x = ((event.clientX - bounds.left) / window.devicePixelRatio - transform.sx) / transform.scale;
|
||||
const y = ((event.clientY - bounds.top) / window.devicePixelRatio - transform.sy) / transform.scale;
|
||||
|
||||
const [hexX, hexY] = inverseHexPosition(x, y);
|
||||
if ("cells" in state && cellIn(state.cells, hexX, hexY)) {
|
||||
return [hexX, hexY];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function mouseDown(event: MouseEvent) {
|
||||
const tile = getTileBelowMouse(event);
|
||||
if (tile) {
|
||||
clickTile(tile[0], tile[1]);
|
||||
}
|
||||
}
|
||||
|
||||
function mouseMove(event: MouseEvent) {
|
||||
const tile = getTileBelowMouse(event);
|
||||
setHoveredCell(tile);
|
||||
}
|
||||
|
||||
c.addEventListener("mousedown", mouseDown);
|
||||
c.addEventListener("mousemove", mouseMove);
|
||||
c.addEventListener("mouseleave", () => setHoveredCell(null));
|
||||
onCleanup(() => {
|
||||
c.removeEventListener("mousedown", mouseDown);
|
||||
c.removeEventListener("mousemove", mouseMove);
|
||||
});
|
||||
});
|
||||
|
||||
return <div>
|
||||
<PixelPerfectCanvas
|
||||
onAttach={setCanvas}
|
||||
onResize={(_, width, height) => {
|
||||
setWidth(width);
|
||||
setHeight(height);
|
||||
draw();
|
||||
}}
|
||||
style={{
|
||||
width: "400px",
|
||||
height: "400px",
|
||||
}}
|
||||
/>
|
||||
<button onClick={() => client.moves.setReady?.()}>{stage() === "ready" ? "Waiting for opponent" : "Done"}</button>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const HEX_BASIS = [
|
||||
[2 * Math.cos(Math.PI / 6), Math.cos(Math.PI * 5 / 6)],
|
||||
[0, 1 + Math.sin(Math.PI * 5 / 6)]
|
||||
] as const;
|
||||
const INVERSE_HEX_BASIS = inverse2DMatrix(HEX_BASIS);
|
||||
const HEX_BOUNDS = {
|
||||
left: 1 + Math.cos(Math.PI * 5 / 6),
|
||||
top: 0,
|
||||
bottom: 2,
|
||||
right: 1 + Math.cos(Math.PI / 6),
|
||||
};
|
||||
|
||||
function getHexPosition(x: number, y: number) {
|
||||
return [
|
||||
HEX_BASIS[0][0] * x + HEX_BASIS[0][1] * y,
|
||||
HEX_BASIS[1][0] * x + HEX_BASIS[1][1] * y,
|
||||
] as const;
|
||||
}
|
||||
|
||||
function inverseHexPosition(spatialX: number, spatialY: number): [hexX: number, hexY: number] {
|
||||
// I can't be bothered to properly do this, so this just brute-forces the neighbors to find which is closest,
|
||||
// using the property that the voronoi cell texture of a 2D close packing is a hex grid
|
||||
|
||||
const guessHexX = Math.floor(spatialX * INVERSE_HEX_BASIS[0][0] + spatialY * INVERSE_HEX_BASIS[0][1]);
|
||||
const guessHexY = Math.floor(spatialX * INVERSE_HEX_BASIS[1][0] + spatialY * INVERSE_HEX_BASIS[1][1]);
|
||||
const hexCenterX = (HEX_BOUNDS.right + HEX_BOUNDS.left) / 2;
|
||||
const hexCenterY = (HEX_BOUNDS.top + HEX_BOUNDS.bottom) / 2;
|
||||
|
||||
function distance(hexX: number, hexY: number) {
|
||||
const [x2, y2] = getHexPosition(hexX, hexY);
|
||||
|
||||
return (x2 + hexCenterX - spatialX) ** 2 + (y2 + hexCenterY - spatialY) ** 2;
|
||||
}
|
||||
|
||||
let closest = [guessHexX, guessHexY, distance(guessHexX, guessHexY)] as const;
|
||||
|
||||
for (const [neighborX, neighborY] of adjacentCells(guessHexX, guessHexY)) {
|
||||
const dist = distance(neighborX, neighborY);
|
||||
if (dist < closest[2]) {
|
||||
closest = [neighborX, neighborY, dist];
|
||||
}
|
||||
}
|
||||
|
||||
return [closest[0], closest[1]];
|
||||
}
|
||||
|
||||
function getBounds(grid: Record<string, Cell>) {
|
||||
let res = {
|
||||
minX: Infinity,
|
||||
maxX: -Infinity,
|
||||
minY: Infinity,
|
||||
maxY: -Infinity
|
||||
};
|
||||
|
||||
for (const [x, y] of iterateCells(grid)) {
|
||||
const [x2, y2] = getHexPosition(x, y);
|
||||
res.minX = Math.min(res.minX, x2 + HEX_BOUNDS.left);
|
||||
res.maxX = Math.max(res.maxX, x2 + HEX_BOUNDS.right);
|
||||
res.minY = Math.min(res.minY, y2 + HEX_BOUNDS.top);
|
||||
res.maxY = Math.max(res.maxY, y2 + HEX_BOUNDS.bottom);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function drawHexagon(ctx: CanvasRenderingContext2D, x: number, y: number, line_offset: number) {
|
||||
ctx.beginPath();
|
||||
// Start at top corner, and rotate clockwise
|
||||
ctx.moveTo(x + 1, y + line_offset);
|
||||
for (let n = 1; n < 6; n++) {
|
||||
ctx.lineTo(
|
||||
x + 1 + Math.cos(Math.PI * n / 3 - Math.PI / 2) * (1 - line_offset),
|
||||
y + 1 + Math.sin(Math.PI * n / 3 - Math.PI / 2) * (1 - line_offset),
|
||||
);
|
||||
}
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function drawCell(ctx: CanvasRenderingContext2D, cell: Cell, x: number, y: number) {
|
||||
const [x2, y2] = getHexPosition(x, y);
|
||||
const LINE_WIDTH = 0.2;
|
||||
const SPACING = 0.2;
|
||||
|
||||
let line_offset = (LINE_WIDTH + SPACING) / 2;
|
||||
if (cell.owner === null) {
|
||||
line_offset = LINE_WIDTH / 2;
|
||||
}
|
||||
ctx.strokeStyle = "#202020";
|
||||
ctx.lineWidth = LINE_WIDTH;
|
||||
|
||||
drawHexagon(ctx, x2, y2, line_offset);
|
||||
|
||||
if (cell.owner === null) {
|
||||
ctx.fillStyle = "#505050";
|
||||
ctx.fill();
|
||||
} else {
|
||||
ctx.strokeStyle = PLAYER_COLORS[+cell.owner % PLAYER_COLORS.length]!;
|
||||
ctx.fillStyle = "#707070";
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
function drawCellOutline(ctx: CanvasRenderingContext2D, x: number, y: number) {
|
||||
const [x2, y2] = getHexPosition(x, y);
|
||||
ctx.lineWidth = 0.05;
|
||||
ctx.strokeStyle = "white";
|
||||
drawHexagon(ctx, x2, y2, 0.05);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
type Matrix2 = readonly [readonly [number, number], readonly [number, number]];
|
||||
function inverse2DMatrix(matrix: Matrix2): Matrix2 {
|
||||
const determinant = matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0];
|
||||
return [
|
||||
[matrix[1][1] / determinant, -matrix[0][1] / determinant],
|
||||
[-matrix[1][0] / determinant, matrix[0][0] / determinant],
|
||||
];
|
||||
}
|
@ -0,0 +1,293 @@
|
||||
import type { Game } from "boardgame.io";
|
||||
import { INVALID_MOVE } from "boardgame.io/core";
|
||||
|
||||
// TODO: partial information
|
||||
|
||||
export type Cell = {
|
||||
owner: null
|
||||
} | {
|
||||
owner: string,
|
||||
building: keyof typeof Buildings,
|
||||
};
|
||||
|
||||
export type Move = {
|
||||
type: "placeBuilding",
|
||||
x: number,
|
||||
y: number,
|
||||
building: keyof typeof Buildings,
|
||||
};
|
||||
|
||||
export type State = {
|
||||
cells: Record<string, Cell>,
|
||||
resources: Record<string, number>,
|
||||
moves: Record<string, Move[]>
|
||||
}
|
||||
|
||||
export const Buildings = {
|
||||
base: {
|
||||
cost: Infinity,
|
||||
gain: 3,
|
||||
},
|
||||
pawn: {
|
||||
cost: 1,
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const AcrossTheHex: Game<State, Record<string, unknown>, {
|
||||
size?: number,
|
||||
initialResources?: number
|
||||
}> = {
|
||||
setup({ ctx }, { size = 3, initialResources = 1 } = {}) {
|
||||
const cells = initGrid(size);
|
||||
const players = new Array(ctx.numPlayers).fill(null).map((_, id) => id.toString());
|
||||
|
||||
setCell(cells, 0, 0, {
|
||||
owner: "0",
|
||||
building: "base"
|
||||
});
|
||||
|
||||
setCell(cells, size * 2 - 2, size * 2 - 2, {
|
||||
owner: "1",
|
||||
building: "base"
|
||||
});
|
||||
|
||||
return {
|
||||
cells,
|
||||
resources: initResources(players, initialResources),
|
||||
moves: emptyMoves(players),
|
||||
};
|
||||
},
|
||||
|
||||
turn: {
|
||||
onBegin(ctx) {
|
||||
// Dispatch all players into the prepare stage
|
||||
ctx.events.setActivePlayers({
|
||||
all: "prepare"
|
||||
});
|
||||
},
|
||||
onEnd(ctx) {
|
||||
if (!ctx.ctx.activePlayers || !areAllPlayersReady(ctx.ctx.activePlayers)) {
|
||||
throw new Error("Assertion error: not all players were ready when turn ended");
|
||||
}
|
||||
|
||||
applyMoves(ctx.G);
|
||||
},
|
||||
endIf(ctx) {
|
||||
if (!ctx.ctx.activePlayers) return false;
|
||||
return areAllPlayersReady(ctx.ctx.activePlayers);
|
||||
},
|
||||
stages: {
|
||||
prepare: {
|
||||
next: "ready",
|
||||
moves: {
|
||||
setReady(ctx) {
|
||||
ctx.events.endStage();
|
||||
},
|
||||
placeBuilding,
|
||||
},
|
||||
},
|
||||
ready: {
|
||||
moves: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
} as const;
|
||||
|
||||
function placeBuilding({ G, playerID }: { G: State, playerID: string }, x: number, y: number, building: keyof typeof Buildings) {
|
||||
// Cannot place a building outside the field
|
||||
if (!cellIn(G.cells, x, y)) return INVALID_MOVE;
|
||||
// Must place a building next to an already-established friendly building
|
||||
if (!hasAdjacentBuilding(G.cells, x, y, playerID)) return INVALID_MOVE;
|
||||
// Cannot place a building on an existing one
|
||||
const targetOwner = getCell(G.cells, x, y)?.owner;
|
||||
if (targetOwner && targetOwner !== playerID) return INVALID_MOVE;
|
||||
// Cannot place a building without enough resources
|
||||
if (remainingResources(playerID, G.resources, G.moves) < Buildings[building].cost) return INVALID_MOVE;
|
||||
|
||||
// Cannot place two buildings at the same place
|
||||
if (G.moves[playerID]!.find((move) => {
|
||||
return move.type === "placeBuilding" && move.x === x && move.y === y;
|
||||
})) return INVALID_MOVE;
|
||||
|
||||
G.moves[playerID]!.push({
|
||||
type: "placeBuilding",
|
||||
x,
|
||||
y,
|
||||
building,
|
||||
});
|
||||
|
||||
// G.cells[`${y}:${x}`] = {
|
||||
// owner: playerID
|
||||
// };
|
||||
// G.resources[playerID] -= Buildings[building].cost;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function remainingResources(playerID: string, resources: Record<string, number>, moves: Record<string, Move[]>): number {
|
||||
let result = resources[playerID] ?? 0;
|
||||
|
||||
for (const move of moves[playerID] ?? []) {
|
||||
if (move.type === "placeBuilding") {
|
||||
result -= Buildings[move.building].cost;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function initGrid(size: number): Record<string, Cell> {
|
||||
const res: Record<string, Cell> = {};
|
||||
|
||||
for (let y = 0; y < size; y++) {
|
||||
for (let x = 0; x < size + y; x++) {
|
||||
res[`${y}:${x}`] = emptyCell();
|
||||
}
|
||||
}
|
||||
|
||||
for (let y = size; y < size * 2 - 1; y++) {
|
||||
for (let x = y - size + 1; x < size * 2 - 1; x++) {
|
||||
res[`${y}:${x}`] = emptyCell();
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function emptyCell(): Cell {
|
||||
return {
|
||||
owner: null
|
||||
};
|
||||
}
|
||||
|
||||
function emptyMoves(players: string[]) {
|
||||
let res: Record<string, Move[]> = {};
|
||||
|
||||
for (const player of players) {
|
||||
res[player] = [];
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function initResources(players: string[], resources: number = 0): Record<string, number> {
|
||||
return Object.fromEntries(players.map((id) => [id, resources] as const));
|
||||
}
|
||||
|
||||
export function cellIn(grid: Record<string, Cell>, x: number, y: number): boolean {
|
||||
if (x < 0 || y < 0 || !Number.isInteger(x) || !Number.isInteger(y)) return false;
|
||||
return `${y}:${x}` in grid;
|
||||
}
|
||||
|
||||
function getCell(grid: Record<string, Cell>, x: number, y: number): Cell | null {
|
||||
if (!cellIn(grid, x, y)) return null;
|
||||
|
||||
return grid[`${y}:${x}`]!;
|
||||
}
|
||||
|
||||
function setCell(grid: Record<string, Cell>, x: number, y: number, cell: Cell) {
|
||||
if (!cellIn(grid, x, y)) return;
|
||||
|
||||
grid[`${y}:${x}`] = cell;
|
||||
}
|
||||
|
||||
function hasAdjacentBuilding(grid: Record<string, Cell>, x: number, y: number, playerID: string): boolean {
|
||||
for (const [x2, y2] of adjacentCells(x, y)) {
|
||||
const cell = getCell(grid, x2, y2);
|
||||
if (cell?.owner === playerID) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function areAllPlayersReady(activePlayers: Record<string, string>): boolean {
|
||||
for (const player in activePlayers) {
|
||||
if (activePlayers[player] !== "ready") return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function* adjacentCells(x: number, y: number): Iterable<[x: number, y: number]> {
|
||||
yield [x - 1, y - 1];
|
||||
yield [x, y - 1];
|
||||
yield [x - 1, y];
|
||||
yield [x + 1, y];
|
||||
yield [x, y + 1];
|
||||
yield [x + 1, y + 1];
|
||||
}
|
||||
|
||||
export function* iterateCells(grid: Record<string, Cell>) {
|
||||
const regexp = /^(\d+):(\d+)$/;
|
||||
for (let key in grid) {
|
||||
const match = regexp.exec(key);
|
||||
if (!match) continue;
|
||||
|
||||
yield [+match[2]!, +match[1]!, grid[key]!] as const;
|
||||
}
|
||||
}
|
||||
|
||||
function applyMoves(state: State) {
|
||||
const players = Object.keys(state.moves);
|
||||
const moves = state.moves;
|
||||
state.moves = emptyMoves(players);
|
||||
|
||||
const extractedResources = initResources(players, 0);
|
||||
|
||||
// Resource extracting step
|
||||
for (const [_x, _y, cell] of iterateCells(state.cells)) {
|
||||
if (cell.owner !== null && cell.owner in state.resources) {
|
||||
const building = Buildings[cell.building];
|
||||
if ("gain" in building) {
|
||||
extractedResources[cell.owner] += building.gain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Building placement step
|
||||
const placeBuildingMoves = Object.entries(moves).flatMap(([player, moves]) => moves.map((move) => [player, move] as const));
|
||||
for (const [x, y, _cell] of iterateCells(state.cells)) {
|
||||
const orders = placeBuildingMoves.filter(([, move]) => move.x === x && move.y === y);
|
||||
|
||||
if (orders.length === 0) continue;
|
||||
|
||||
// If two players try to build on the same tile, then nothing happens
|
||||
if (orders.length > 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const order = orders[0]!;
|
||||
const cost = Buildings[order[1].building].cost;
|
||||
|
||||
if (state.resources[order[0]]! < cost) continue;
|
||||
// TODO: fully check that the building can be placed
|
||||
|
||||
setCell(state.cells, x, y, {
|
||||
owner: order[0],
|
||||
building: order[1].building
|
||||
});
|
||||
|
||||
state.resources[order[0]] -= cost;
|
||||
}
|
||||
|
||||
// Resource gathering step
|
||||
for (const player of players) {
|
||||
state.resources[player] += extractedResources[player]!;
|
||||
}
|
||||
}
|
||||
|
||||
export function getLocalState(state: State, playerID: string): State["cells"] {
|
||||
const res: State["cells"] = {
|
||||
...state.cells
|
||||
};
|
||||
|
||||
for (const move of state.moves[playerID] ?? []) {
|
||||
if (move.type === "placeBuilding") {
|
||||
res[`${move.y}:${move.x}`] = {
|
||||
owner: playerID,
|
||||
building: move.building
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
---
|
||||
import Document from "../components/Document.astro";
|
||||
import Game from "../components/Game.tsx";
|
||||
|
||||
export const prerender = true;
|
||||
---
|
||||
|
||||
<Document>
|
||||
<Game playerID="0" client:only />
|
||||
<Game playerID="1" client:only />
|
||||
</Document>
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strictest",
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"moduleResolution": "NodeNext"
|
||||
}
|
||||
}
|
Loading…
Reference in new issue