diff --git a/packages/engine/src/setups.ts b/packages/engine/src/setups.ts index 578503a..58aa713 100644 --- a/packages/engine/src/setups.ts +++ b/packages/engine/src/setups.ts @@ -15,7 +15,7 @@ import { import { layoutIds } from "./board"; import { shuffle, nextInt, type RngState } from "./rng"; -export const SUPPORTED_PLAYER_COUNTS = [2, 4] as const; +export const SUPPORTED_PLAYER_COUNTS = [2, 3, 4, 5, 6] as const; export interface SetupResult { board: AssembledBoard; @@ -54,6 +54,28 @@ export function setupBoard(playerCount: number, rng: RngState): SetupResult { return { board, rng: rngN }; } + if (playerCount === 3) { + // The rulebook's stair/L: one sector on the top row (offset right), two + // on the bottom row. The two openings facing the concave notch join via + // the AUTO WARP; the convex openings pair as below (interpretation of + // the diagram's letters — geometrically matched pairs). + const picks = pick(3); + const placements: SectorPlacement[] = [ + { boardId: picks[0]!.id, origin: { x: 5, y: 0 }, rotation: picks[0]!.rotation }, + { boardId: picks[1]!.id, origin: { x: 0, y: 5 }, rotation: picks[1]!.rotation }, + { boardId: picks[2]!.id, origin: { x: 5, y: 5 }, rotation: picks[2]!.rotation }, + ]; + const board = assembleBoard(placements, { + warpPairs: [ + [{ sector: 0, side: "W" }, { sector: 1, side: "N" }], // AUTO WARP corner + [{ sector: 0, side: "N" }, { sector: 2, side: "S" }], + [{ sector: 1, side: "W" }, { sector: 2, side: "E" }], + [{ sector: 0, side: "E" }, { sector: 1, side: "S" }], + ], + }); + return { board, rng: rngN }; + } + if (playerCount === 4) { const picks = pick(4); const placements: SectorPlacement[] = [ @@ -68,5 +90,44 @@ export function setupBoard(playerCount: number, rng: RngState): SetupResult { return { board, rng: rngN }; } - throw new Error(`unsupported player count: ${playerCount} (supported: 2, 4)`); + if (playerCount === 5) { + // The plus/cross: four arms around a center. Opposite arm tips wrap to + // each other; the notch-facing side openings warp around the corners. + const picks = pick(5); + const placements: SectorPlacement[] = [ + { boardId: picks[0]!.id, origin: { x: 5, y: 0 }, rotation: picks[0]!.rotation }, // N arm + { boardId: picks[1]!.id, origin: { x: 0, y: 5 }, rotation: picks[1]!.rotation }, // W arm + { boardId: picks[2]!.id, origin: { x: 5, y: 5 }, rotation: picks[2]!.rotation }, // center + { boardId: picks[3]!.id, origin: { x: 10, y: 5 }, rotation: picks[3]!.rotation }, // E arm + { boardId: picks[4]!.id, origin: { x: 5, y: 10 }, rotation: picks[4]!.rotation }, // S arm + ]; + const board = assembleBoard(placements, { + warpPairs: [ + [{ sector: 0, side: "N" }, { sector: 4, side: "S" }], // tip to tip + [{ sector: 1, side: "W" }, { sector: 3, side: "E" }], + [{ sector: 0, side: "W" }, { sector: 1, side: "N" }], // aisle-warp corners + [{ sector: 0, side: "E" }, { sector: 3, side: "N" }], + [{ sector: 4, side: "W" }, { sector: 1, side: "S" }], + [{ sector: 4, side: "E" }, { sector: 3, side: "S" }], + ], + }); + return { board, rng: rngN }; + } + + if (playerCount === 6) { + // 3x2 rectangle: straight-across wrap (the default pairing). + const picks = pick(6); + const placements: SectorPlacement[] = [ + { boardId: picks[0]!.id, origin: { x: 0, y: 0 }, rotation: picks[0]!.rotation }, + { boardId: picks[1]!.id, origin: { x: 5, y: 0 }, rotation: picks[1]!.rotation }, + { boardId: picks[2]!.id, origin: { x: 10, y: 0 }, rotation: picks[2]!.rotation }, + { boardId: picks[3]!.id, origin: { x: 0, y: 5 }, rotation: picks[3]!.rotation }, + { boardId: picks[4]!.id, origin: { x: 5, y: 5 }, rotation: picks[4]!.rotation }, + { boardId: picks[5]!.id, origin: { x: 10, y: 5 }, rotation: picks[5]!.rotation }, + ]; + const board = assembleBoard(placements); + return { board, rng: rngN }; + } + + throw new Error(`unsupported player count: ${playerCount} (supported: 2-6)`); } diff --git a/packages/engine/test/board.test.ts b/packages/engine/test/board.test.ts index 5562017..bfc0825 100644 --- a/packages/engine/test/board.test.ts +++ b/packages/engine/test/board.test.ts @@ -106,3 +106,33 @@ describe("board assembly", () => { expect(hasLineOfSight(board, { x: 2, y: 2 }, { x: 2, y: 0 })).toBe(false); }); }); + +describe("player counts 2-6", () => { + it("assembles a coherent board for every supported count", async () => { + const { setupBoard } = await import("../src/setups"); + const { createRng } = await import("../src/rng"); + for (const n of [2, 3, 4, 5, 6]) { + const { board } = setupBoard(n, createRng(n * 7)); + expect(board.homes.length).toBe(n); + expect(board.placements.length).toBe(n); + // Every warp starts and ends on real cells, through open edges. + for (const w of board.warps) { + expect(board.cells[`${w.from.cell.x},${w.from.cell.y}`]).toBe(true); + expect(board.cells[`${w.to.cell.x},${w.to.cell.y}`]).toBe(true); + } + // No two sectors share a layout. + const ids = board.placements.map((p) => p.boardId); + expect(new Set(ids).size).toBe(n); + } + }); + + it("full games start at every count", async () => { + const { createGame } = await import("../src/game"); + for (const n of [3, 5, 6]) { + const ids = Array.from({ length: n }, (_, i) => `w${i}`); + const { state } = createGame({ playerIds: ids, seed: 99 + n, sets: ["basic", "expansion1"] }); + expect(state.players.length).toBe(n); + expect(state.treasures.length).toBe(n * 2); + } + }); +}); diff --git a/packages/server/src/rooms.ts b/packages/server/src/rooms.ts index a26569e..73c94b0 100644 --- a/packages/server/src/rooms.ts +++ b/packages/server/src/rooms.ts @@ -103,7 +103,7 @@ export function joinRoom( return { error: "that wizard name is taken in this room" }; } if (room.state) return { error: "game already started" }; - if (room.players.length >= 4) return { error: "room is full" }; + if (room.players.length >= 6) return { error: "room is full" }; const fresh = randomBytes(16).toString("hex"); room.players.push(playerId); room.tokens.set(playerId, hashToken(fresh)); @@ -113,7 +113,7 @@ export function joinRoom( function startInMemory(room: Room, expansion: boolean): { events: GameEvent[] } | { error: string } { const n = room.players.length; - if (n !== 2 && n !== 4) return { error: "supported player counts: 2 or 4" }; + if (n < 2 || n > 6) return { error: "supported player counts: 2 to 6" }; const { state, events } = createGame({ playerIds: room.players, seed: room.seed, diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 87d13e8..dced1a9 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -549,7 +549,7 @@
Room {net.roomId}
-
Share the code. Two or four wizards enter the maze.
+
Share the code. Two to six wizards enter the maze.