import { describe, expect, it } from "vitest"; import { shortestPathLength } from "../../src/game/chase/hexGraph"; import { generateChaseRound } from "../../src/game/chase/generator"; import type { Difficulty } from "../../src/game/chase/types"; describe("chase round generator", () => { it("is deterministic for same seed and difficulty", () => { const options = { seed: 20260426, difficulty: "normal" as Difficulty }; const roundA = generateChaseRound(options); const roundB = generateChaseRound(options); expect(roundA).toEqual(roundB); }); it("matches golden snapshot for fixed seed", () => { const round = generateChaseRound({ seed: 424242, difficulty: "easy" }); expect({ seed: round.snapshot.seed, difficulty: round.snapshot.difficulty, thiefStartNodeId: round.snapshot.thiefStartNodeId, guardStartNodeId: round.snapshot.guardStartNodeId, thiefNodeId: round.snapshot.thiefNodeId, guardNodeId: round.snapshot.guardNodeId, exitNodeId: round.snapshot.exitNodeId, status: round.snapshot.status, hasEscapePath: round.meta.hasEscapePath, nodeCount: Object.keys(round.snapshot.graph.nodes).length, edgeCount: round.snapshot.graph.edgeList.length, firstFiveNodeIds: Object.keys(round.snapshot.graph.nodes).slice(0, 5), }).toMatchInlineSnapshot(` { "difficulty": "easy", "edgeCount": 19, "exitNodeId": "0,-1", "firstFiveNodeIds": [ "0,0", "1,-1", "2,-2", "2,-1", "1,-2", ], "guardNodeId": "0,0", "guardStartNodeId": "0,0", "hasEscapePath": true, "nodeCount": 20, "seed": 424242, "status": "playing", "thiefNodeId": "4,-5", "thiefStartNodeId": "4,-5", } `); }); it("generates graph node count between 20 and 30", () => { const round = generateChaseRound({ seed: 11, difficulty: "normal" }); const nodeCount = Object.keys(round.snapshot.graph.nodes).length; expect(nodeCount).toBeGreaterThanOrEqual(20); expect(nodeCount).toBeLessThanOrEqual(30); }); it("ensures an escape path exists", () => { const round = generateChaseRound({ seed: 99, difficulty: "hard" }); const pathLength = shortestPathLength( round.snapshot.graph, round.snapshot.thiefNodeId, round.snapshot.exitNodeId, ); expect(round.meta.hasEscapePath).toBe(true); expect(pathLength).toBeGreaterThan(0); expect(pathLength).toBeLessThan(Infinity); }); it("ensures thief and guard do not start on the same node", () => { const round = generateChaseRound({ seed: 123456, difficulty: "normal" }); expect(round.snapshot.thiefStartNodeId).not.toBe(round.snapshot.guardStartNodeId); }); it("stays playable and stable across many seeds", () => { for (let seed = 1; seed <= 60; seed += 1) { const round = generateChaseRound({ seed, difficulty: "normal" }); const nodeCount = Object.keys(round.snapshot.graph.nodes).length; expect(nodeCount).toBeGreaterThanOrEqual(20); expect(nodeCount).toBeLessThanOrEqual(30); expect(round.meta.hasEscapePath).toBe(true); expect(round.snapshot.thiefStartNodeId).not.toBe( round.snapshot.guardStartNodeId, ); } }); it("throws after retry limit when all attempts are unplayable", () => { let attemptCount = 0; const unplayableStrategy = (seed: number, difficulty: Difficulty) => { attemptCount += 1; return { snapshot: { seed, difficulty, graph: { nodes: {}, edgeList: [] }, thiefStartNodeId: "A", guardStartNodeId: "A", thiefNodeId: "A", guardNodeId: "A", exitNodeId: "A", status: "playing" as const, }, meta: { hasEscapePath: false, pathLength: 0, attemptsUsed: 1, }, }; }; expect(() => generateChaseRound( { seed: 7, difficulty: "normal" }, { generateAttempt: unplayableStrategy }, ), ).toThrow("Unable to generate playable chase round within retry limit"); expect(attemptCount).toBe(40); }); });