Support 2-6 players: the L, the cross, and the 3x2 from the rulebook
Three player counts join the square and the column: 3 players get the stair/L with the AUTO WARP joining the two openings that face the concave notch (convex openings pair geometrically — flagged as interpretation of the diagram's letters); 5 players get the plus/cross with tip-to-tip wraps and aisle-warp corners; 6 players get the 3x2 rectangle with straight-across wraps. All six verified layouts are drawn on, so no count repeats a sector. Rooms accept up to six seats; the lobby copy follows. 122 tests passing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8d10dfee67
commit
85b40be113
@@ -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)`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -549,7 +549,7 @@
|
||||
<section class="boxlid">
|
||||
<div class="boxlid-inner">
|
||||
<div class="boxlid-title small">Room {net.roomId}</div>
|
||||
<div class="boxlid-tag">Share the code. Two or four wizards enter the maze.</div>
|
||||
<div class="boxlid-tag">Share the code. Two to six wizards enter the maze.</div>
|
||||
<ul class="roster">
|
||||
{#each net.players as p (p)}
|
||||
<li><span class="dot" style:background={playerColor(p)}></span>{p}{p === net.hostId ? " — host" : ""}</li>
|
||||
@@ -562,10 +562,10 @@
|
||||
</label>
|
||||
<button
|
||||
class="stamp big"
|
||||
disabled={net.players.length !== 2 && net.players.length !== 4}
|
||||
disabled={net.players.length < 2 || net.players.length > 6}
|
||||
onclick={() => net.start(withExpansion)}
|
||||
>
|
||||
Flip the boards ({net.players.length} of 2 or 4)
|
||||
Flip the boards ({net.players.length} wizard{net.players.length === 1 ? "" : "s"})
|
||||
</button>
|
||||
{:else}
|
||||
<p class="waiting">Waiting for {net.hostId} to flip the boards…</p>
|
||||
|
||||
Reference in New Issue
Block a user