diff --git a/docs/superpowers/plans/2026-04-26-hex-chase-game-implementation-plan.md b/docs/superpowers/plans/2026-04-26-hex-chase-game-implementation-plan.md new file mode 100644 index 0000000..9363df7 --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-hex-chase-game-implementation-plan.md @@ -0,0 +1,824 @@ +# Hex Chase Game Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在现有 Pixi 项目中交付一个可通过 seed 复现、支持难度选择、具备“可逃脱但不简单”约束的六边形追击玩法场景。 + +**Architecture:** 采用 `GameModel`(纯逻辑)+ `GameScene`(渲染与交互)分层。逻辑层负责地图生成、回合推进、追击策略和胜负判定;场景层仅负责绘制与输入映射,调用模型动作并刷新 UI。seed、snapshot 与 winCount 在模型层统一管理,保证“重试同局”和“新一局”语义稳定。 + +**Tech Stack:** TypeScript, Pixi.js, Vitest, 现有 SceneManager/BaseScene 生命周期 + +--- + +## 文件结构与职责映射 + +- Create: `src/game/chase/types.ts` + 玩法类型定义(图结构、状态、难度、快照)。 +- Create: `src/game/chase/rng.ts` + seed RNG(mulberry32)与 seed 规范化函数。 +- Create: `src/game/chase/hexGraph.ts` + 六边形坐标、邻接构建、连通与最短路工具。 +- Create: `src/game/chase/generator.ts` + 20~30 节点地图生成、起点/出口放置、可玩性校验与重采样。 +- Create: `src/game/chase/model.ts` + 游戏状态机:开局、行动、官兵移动、胜负判断、重试/新局。 +- Create: `src/stages/page_chase.ts` + 主玩法场景,绘制格子/连线/HUD/面板/弹层,绑定点击行为。 +- Modify: `src/stages/page_init.ts` + “开始游戏”按钮目标从 `welcome` 改为 `chase`(或新增入口按钮)。 +- Test: `tests/chase/chaseGenerator.test.ts` + 生成一致性、连通性、可逃脱与非过简性。 +- Test: `tests/chase/chaseModel.test.ts` + 回合推进、三档难度、胜负判定、重试/新局语义。 +- Test: `tests/stages/page_chase.test.ts` + 基础场景行为与按钮路径(最小集成验证)。 + +--- + +### Task 1: 建立玩法类型与 RNG 基础 + +**Files:** +- Create: `src/game/chase/types.ts` +- Create: `src/game/chase/rng.ts` +- Test: `tests/chase/rng.test.ts` + +- [ ] **Step 1: Write the failing test** + +```ts +import { describe, expect, it } from "vitest"; +import { createRng, normalizeSeed } from "@/game/chase/rng"; + +describe("chase rng", () => { + it("returns same sequence for same seed", () => { + const a = createRng(123); + const b = createRng(123); + expect([a(), a(), a()]).toEqual([b(), b(), b()]); + }); + + it("normalizes empty/invalid seed to deterministic fallback", () => { + expect(normalizeSeed(undefined)).toBeTypeOf("number"); + expect(normalizeSeed("")).toBeTypeOf("number"); + expect(normalizeSeed("abc")).toBeTypeOf("number"); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `npm run test -- tests/chase/rng.test.ts` +Expected: FAIL with module not found for `@/game/chase/rng` + +- [ ] **Step 3: Write minimal implementation** + +```ts +// src/game/chase/rng.ts +export type Rng = () => number; + +export function normalizeSeed(input?: string | number): number { + if (typeof input === "number" && Number.isFinite(input)) return input >>> 0; + if (typeof input === "string" && input.trim() !== "") { + const n = Number(input); + if (Number.isFinite(n)) return n >>> 0; + let h = 2166136261; + for (let i = 0; i < input.length; i += 1) { + h ^= input.charCodeAt(i); + h = Math.imul(h, 16777619); + } + return h >>> 0; + } + return (Date.now() >>> 0) ^ 0x9e3779b9; +} + +export function createRng(seed: number): Rng { + let t = seed >>> 0; + return () => { + t += 0x6d2b79f5; + let x = t; + x = Math.imul(x ^ (x >>> 15), x | 1); + x ^= x + Math.imul(x ^ (x >>> 7), x | 61); + return ((x ^ (x >>> 14)) >>> 0) / 4294967296; + }; +} +``` + +- [ ] **Step 4: Add type definitions** + +```ts +// src/game/chase/types.ts +export type NodeId = string; +export type Difficulty = "easy" | "normal" | "hard"; +export type GameStatus = "setup" | "playing" | "win" | "lose"; + +export interface GraphNode { + id: NodeId; + q: number; + r: number; + neighbors: NodeId[]; +} + +export interface GameGraph { + nodes: Record; + edgeList: Array<[NodeId, NodeId]>; +} +``` + +- [ ] **Step 5: Run test to verify it passes** + +Run: `npm run test -- tests/chase/rng.test.ts` +Expected: PASS (2 tests) + +- [ ] **Step 6: Commit** + +```bash +git add tests/chase/rng.test.ts src/game/chase/rng.ts src/game/chase/types.ts +git commit -m "feat(chase): add deterministic rng and core chase types" +``` + +--- + +### Task 2: 完成六边形图工具与路径算法 + +**Files:** +- Create: `src/game/chase/hexGraph.ts` +- Test: `tests/chase/hexGraph.test.ts` + +- [ ] **Step 1: Write the failing test** + +```ts +import { describe, expect, it } from "vitest"; +import { axialNeighbors, shortestPathLength, isConnected } from "@/game/chase/hexGraph"; +import type { GameGraph } from "@/game/chase/types"; + +const simpleGraph: GameGraph = { + nodes: { + a: { id: "a", q: 0, r: 0, neighbors: ["b"] }, + b: { id: "b", q: 1, r: 0, neighbors: ["a", "c"] }, + c: { id: "c", q: 2, r: 0, neighbors: ["b"] }, + }, + edgeList: [["a", "b"], ["b", "c"]], +}; + +describe("hex graph helpers", () => { + it("returns 6 axial neighbors", () => { + expect(axialNeighbors(0, 0)).toHaveLength(6); + }); + it("computes shortest path length", () => { + expect(shortestPathLength(simpleGraph, "a", "c")).toBe(2); + }); + it("checks connectivity", () => { + expect(isConnected(simpleGraph)).toBe(true); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `npm run test -- tests/chase/hexGraph.test.ts` +Expected: FAIL with module not found for `hexGraph` + +- [ ] **Step 3: Write minimal implementation** + +```ts +// src/game/chase/hexGraph.ts +import type { GameGraph, NodeId } from "./types"; + +const OFFSETS: Array<[number, number]> = [ + [1, 0], [1, -1], [0, -1], [-1, 0], [-1, 1], [0, 1], +]; + +export function axialNeighbors(q: number, r: number): Array<[number, number]> { + return OFFSETS.map(([dq, dr]) => [q + dq, r + dr]); +} + +export function shortestPathLength(graph: GameGraph, from: NodeId, to: NodeId): number { + if (from === to) return 0; + const queue: Array<{ id: NodeId; d: number }> = [{ id: from, d: 0 }]; + const seen = new Set([from]); + while (queue.length > 0) { + const current = queue.shift()!; + for (const n of graph.nodes[current.id].neighbors) { + if (seen.has(n)) continue; + if (n === to) return current.d + 1; + seen.add(n); + queue.push({ id: n, d: current.d + 1 }); + } + } + return Number.POSITIVE_INFINITY; +} + +export function isConnected(graph: GameGraph): boolean { + const ids = Object.keys(graph.nodes); + if (ids.length <= 1) return true; + const queue = [ids[0]]; + const seen = new Set([ids[0]]); + while (queue.length > 0) { + const id = queue.shift()!; + for (const n of graph.nodes[id].neighbors) { + if (!seen.has(n)) { + seen.add(n); + queue.push(n); + } + } + } + return seen.size === ids.length; +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `npm run test -- tests/chase/hexGraph.test.ts` +Expected: PASS (3 tests) + +- [ ] **Step 5: Commit** + +```bash +git add tests/chase/hexGraph.test.ts src/game/chase/hexGraph.ts +git commit -m "feat(chase): add hex graph adjacency and path utilities" +``` + +--- + +### Task 3: 实现 seed 地图生成与可玩性约束 + +**Files:** +- Create: `src/game/chase/generator.ts` +- Test: `tests/chase/chaseGenerator.test.ts` + +- [ ] **Step 1: Write the failing test** + +```ts +import { describe, expect, it } from "vitest"; +import { generateChaseRound } from "@/game/chase/generator"; + +describe("chase generator", () => { + it("is deterministic for same seed", () => { + const a = generateChaseRound({ seed: 42, difficulty: "normal" }); + const b = generateChaseRound({ seed: 42, difficulty: "normal" }); + expect(a.snapshot).toEqual(b.snapshot); + }); + + it("node count stays in 20-30 for small map", () => { + const round = generateChaseRound({ seed: 7, difficulty: "easy" }); + const count = Object.keys(round.snapshot.graph.nodes).length; + expect(count).toBeGreaterThanOrEqual(20); + expect(count).toBeLessThanOrEqual(30); + }); + + it("ensures thief has escape possibility", () => { + const round = generateChaseRound({ seed: 1234, difficulty: "normal" }); + expect(round.meta.hasEscapePath).toBe(true); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `npm run test -- tests/chase/chaseGenerator.test.ts` +Expected: FAIL with missing `generateChaseRound` + +- [ ] **Step 3: Write minimal implementation** + +```ts +// src/game/chase/generator.ts +import { createRng } from "./rng"; +import { axialNeighbors, shortestPathLength, isConnected } from "./hexGraph"; +import type { Difficulty, GameGraph, NodeId } from "./types"; + +export interface GenerateOptions { + seed: number; + difficulty: Difficulty; +} + +export interface RoundSnapshot { + seed: number; + difficulty: Difficulty; + graph: GameGraph; + thiefStartNodeId: NodeId; + guardStartNodeId: NodeId; + thiefNodeId: NodeId; + guardNodeId: NodeId; + exitNodeId: NodeId; + status: "playing"; +} + +export function generateChaseRound(options: GenerateOptions): { + snapshot: RoundSnapshot; + meta: { hasEscapePath: boolean }; +} { + let attempt = 0; + while (attempt < 40) { + const seed = (options.seed + attempt) >>> 0; + const rng = createRng(seed); + const graph = buildGraph(rng); + if (!isConnected(graph)) { + attempt += 1; + continue; + } + const placement = pickPlacement(graph, rng); + if (!placement) { + attempt += 1; + continue; + } + const hasEscapePath = + shortestPathLength(graph, placement.thiefStartNodeId, placement.exitNodeId) >= 4; + const tooEasy = + shortestPathLength(graph, placement.thiefStartNodeId, placement.exitNodeId) <= 3; + if (!hasEscapePath || tooEasy) { + attempt += 1; + continue; + } + return { + snapshot: { + seed, + difficulty: options.difficulty, + graph, + thiefStartNodeId: placement.thiefStartNodeId, + guardStartNodeId: placement.guardStartNodeId, + thiefNodeId: placement.thiefStartNodeId, + guardNodeId: placement.guardStartNodeId, + exitNodeId: placement.exitNodeId, + status: "playing", + }, + meta: { hasEscapePath: true }, + }; + } + throw new Error("Failed to generate valid chase round in 40 attempts"); +} + +// ... within same file: buildGraph/pickPlacement helpers with deterministic selection +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `npm run test -- tests/chase/chaseGenerator.test.ts` +Expected: PASS (3 tests) + +- [ ] **Step 5: Commit** + +```bash +git add tests/chase/chaseGenerator.test.ts src/game/chase/generator.ts +git commit -m "feat(chase): implement seed-based round generator with playability checks" +``` + +--- + +### Task 4: 实现 GameModel 回合与胜负逻辑 + +**Files:** +- Create: `src/game/chase/model.ts` +- Test: `tests/chase/chaseModel.test.ts` + +- [ ] **Step 1: Write the failing test** + +```ts +import { describe, expect, it } from "vitest"; +import { ChaseGameModel } from "@/game/chase/model"; + +describe("chase model", () => { + it("moves thief one step then guard one step", () => { + const model = ChaseGameModel.createWithSeed({ seed: 99, difficulty: "normal" }); + const firstMove = model.getAvailableMoves()[0]; + model.moveThief(firstMove); + const s = model.getState(); + expect(s.turn).toBeGreaterThan(0); + expect(s.status === "playing" || s.status === "win" || s.status === "lose").toBe(true); + }); + + it("retry restores exact snapshot", () => { + const model = ChaseGameModel.createWithSeed({ seed: 99, difficulty: "hard" }); + const initial = model.getState().snapshot; + model.retryRound(); + expect(model.getState().snapshot).toEqual(initial); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `npm run test -- tests/chase/chaseModel.test.ts` +Expected: FAIL with missing `ChaseGameModel` + +- [ ] **Step 3: Write minimal implementation** + +```ts +// src/game/chase/model.ts +import { generateChaseRound } from "./generator"; +import { shortestPathLength } from "./hexGraph"; +import type { Difficulty, NodeId } from "./types"; + +interface State { + turn: number; + status: "playing" | "win" | "lose"; + snapshot: ReturnType["snapshot"]; + winCount: number; +} + +export class ChaseGameModel { + private state: State; + private readonly baseSeed: number; + + private constructor(state: State, baseSeed: number) { + this.state = state; + this.baseSeed = baseSeed; + } + + static createWithSeed(input: { seed: number; difficulty: Difficulty }): ChaseGameModel { + const { snapshot } = generateChaseRound(input); + return new ChaseGameModel({ turn: 0, status: "playing", snapshot, winCount: 0 }, input.seed); + } + + getState(): State { + return structuredClone(this.state); + } + + getAvailableMoves(): NodeId[] { + const thief = this.state.snapshot.graph.nodes[this.state.snapshot.thiefNodeId]; + return thief.neighbors.filter((id) => id !== this.state.snapshot.guardNodeId); + } + + moveThief(target: NodeId): void { + if (this.state.status !== "playing") return; + if (!this.getAvailableMoves().includes(target)) return; + this.state.snapshot.thiefNodeId = target; + if (target === this.state.snapshot.exitNodeId) { + this.state.status = "win"; + this.state.winCount += 1; + this.state.turn += 1; + return; + } + this.moveGuard(); + this.resolveLose(); + this.state.turn += 1; + } + + retryRound(): void { + const s = this.state.snapshot; + this.state.snapshot = { ...s, thiefNodeId: s.thiefStartNodeId, guardNodeId: s.guardStartNodeId, status: "playing" }; + this.state.status = "playing"; + this.state.turn = 0; + } + + newRound(seed: number, difficulty: Difficulty): void { + const { snapshot } = generateChaseRound({ seed, difficulty }); + this.state.snapshot = snapshot; + this.state.status = "playing"; + this.state.turn = 0; + } + + private moveGuard(): void { + const g = this.state.snapshot.graph.nodes[this.state.snapshot.guardNodeId]; + const target = this.state.snapshot.thiefNodeId; + const scored = g.neighbors.map((id) => ({ + id, + score: shortestPathLength(this.state.snapshot.graph, id, target), + })); + scored.sort((a, b) => a.score - b.score); + this.state.snapshot.guardNodeId = scored[0]?.id ?? this.state.snapshot.guardNodeId; + } + + private resolveLose(): void { + if (this.state.snapshot.guardNodeId === this.state.snapshot.thiefNodeId) { + this.state.status = "lose"; + return; + } + if (this.getAvailableMoves().length === 0) { + this.state.status = "lose"; + } + } +} +``` + +- [ ] **Step 4: Extend guard strategy by difficulty** + +```ts +// add in model.ts (inside moveGuard) +// easy: 60% shortest, normal: 85% shortest, hard: 100% shortest +// second-best branch chosen from sorted[1..] with deterministic RNG seeded from (baseSeed + turn) +``` + +- [ ] **Step 5: Run test to verify it passes** + +Run: `npm run test -- tests/chase/chaseModel.test.ts` +Expected: PASS + +- [ ] **Step 6: Commit** + +```bash +git add tests/chase/chaseModel.test.ts src/game/chase/model.ts +git commit -m "feat(chase): implement turn-based chase model with retry/new round flow" +``` + +--- + +### Task 5: 新建场景并接入渲染/交互骨架 + +**Files:** +- Create: `src/stages/page_chase.ts` +- Modify: `src/stages/page_init.ts` +- Test: `tests/stages/page_chase.test.ts` + +- [ ] **Step 1: Write the failing test** + +```ts +import { describe, expect, it } from "vitest"; +import ChaseScene from "@/stages/page_chase"; + +describe("page_chase scene", () => { + it("creates scene with name chase", () => { + const scene = new ChaseScene(); + expect(scene.name).toBe("chase"); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `npm run test -- tests/stages/page_chase.test.ts` +Expected: FAIL with missing scene file + +- [ ] **Step 3: Write minimal scene implementation** + +```ts +// src/stages/page_chase.ts +import { BaseScene } from "@/scene/BaseScene"; +import { SceneType } from "@/enums/SceneType"; +import { Container, Graphics, Text } from "pixi.js"; +import { ChaseGameModel } from "@/game/chase/model"; + +export default class ChaseScene extends BaseScene { + stage = new Container(); + private model = ChaseGameModel.createWithSeed({ seed: Date.now() >>> 0, difficulty: "normal" }); + private hudText?: Text; + + constructor() { + super("chase", SceneType.Normal); + } + + protected async onSceneLayout(): Promise { + this.stage.eventMode = "passive"; + this.hudText = new Text({ text: "成功次数:0", style: { fill: 0xffffff } }); + this.hudText.position.set(16, 16); + this.stage.addChild(this.hudText); + this.renderGraph(); + } + + private renderGraph(): void { + const s = this.model.getState(); + Object.values(s.snapshot.graph.nodes).forEach((n) => { + const g = new Graphics(); + g.circle(100 + n.q * 26, 120 + n.r * 26, 10).stroke({ color: 0x9ca3af, width: 2 }); + g.eventMode = "static"; + g.on("pointerdown", () => this.onNodeClick(n.id)); + this.stage.addChild(g); + }); + } + + private onNodeClick(nodeId: string): void { + this.model.moveThief(nodeId); + const s = this.model.getState(); + if (this.hudText) this.hudText.text = `成功次数:${s.winCount}`; + } +} +``` + +- [ ] **Step 4: Change init entry path** + +```ts +// src/stages/page_init.ts +// in handleStartClick: +void this.changeScene("chase"); +``` + +- [ ] **Step 5: Run test to verify it passes** + +Run: `npm run test -- tests/stages/page_chase.test.ts` +Expected: PASS + +- [ ] **Step 6: Commit** + +```bash +git add tests/stages/page_chase.test.ts src/stages/page_chase.ts src/stages/page_init.ts +git commit -m "feat(chase): add chase scene skeleton and init entry wiring" +``` + +--- + +### Task 6: 完成开局面板、seed 输入与难度锁定 + +**Files:** +- Modify: `src/stages/page_chase.ts` +- Test: `tests/stages/page_chase.test.ts` + +- [ ] **Step 1: Write the failing test** + +```ts +import { describe, expect, it } from "vitest"; +import { ChaseGameModel } from "@/game/chase/model"; + +describe("chase setup panel", () => { + it("locks selected difficulty into new round snapshot", () => { + const model = ChaseGameModel.createWithSeed({ seed: 11, difficulty: "easy" }); + model.newRound(20260426, "hard"); + const state = model.getState(); + expect(state.snapshot.seed).toBe(20260426); + expect(state.snapshot.difficulty).toBe("hard"); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `npm run test -- tests/chase/chaseModel.test.ts tests/stages/page_chase.test.ts` +Expected: FAIL until setup API is connected + +- [ ] **Step 3: Implement setup panel behavior** + +```ts +// src/stages/page_chase.ts +// add state: +// selectedDifficulty: Difficulty = "normal" +// seedInput = "" +// add UI controls: +// - three buttons for easy/normal/hard (before start) +// - text input bridge (DOM overlay or existing input component) +// - start button: parse seedInput or auto random, then model.newRound(seed, selectedDifficulty) +// lock difficulty after start until next new round +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `npm run test -- tests/chase/chaseModel.test.ts tests/stages/page_chase.test.ts` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add src/stages/page_chase.ts tests/chase/chaseModel.test.ts tests/stages/page_chase.test.ts +git commit -m "feat(chase): add setup panel with difficulty selection and seed input" +``` + +--- + +### Task 7: 实现胜负弹层与“重试/新一局/重新开始”语义 + +**Files:** +- Modify: `src/stages/page_chase.ts` +- Modify: `src/game/chase/model.ts` +- Test: `tests/chase/chaseModel.test.ts` +- Test: `tests/stages/page_chase.test.ts` + +- [ ] **Step 1: Write the failing tests** + +```ts +import { describe, expect, it } from "vitest"; +import { ChaseGameModel } from "@/game/chase/model"; + +describe("round restart semantics", () => { + it("retry restores same map and positions", () => { + const model = ChaseGameModel.createWithSeed({ seed: 77, difficulty: "normal" }); + const before = model.getState().snapshot; + model.retryRound(); + const after = model.getState().snapshot; + expect(after.graph).toEqual(before.graph); + expect(after.thiefNodeId).toBe(before.thiefStartNodeId); + expect(after.guardNodeId).toBe(before.guardStartNodeId); + }); + + it("new round creates different snapshot for different seed", () => { + const model = ChaseGameModel.createWithSeed({ seed: 77, difficulty: "normal" }); + const before = model.getState().snapshot; + model.newRound(88, "normal"); + const after = model.getState().snapshot; + expect(after.seed).toBe(88); + expect(after.graph).not.toEqual(before.graph); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npm run test -- tests/chase/chaseModel.test.ts` +Expected: FAIL on incomplete restart behavior + +- [ ] **Step 3: Implement model/UI actions** + +```ts +// model.ts add: +// getResultLabel(): "success" | "caught" | "blocked" | null +// restartAfterWin(): calls newRound(randomSeed, currentDifficulty) +// retryAfterLose(): calls retryRound() +// newGameAfterLose(): calls newRound(randomSeed, currentDifficulty) + +// page_chase.ts add overlay: +// if status === "win" show "成功逃脱!" + button "重新开始" +// if status === "lose" show "你被抓住了/你已无路可走" + buttons "重试" + "新一局" +// all buttons debounced ~300-500ms +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `npm run test -- tests/chase/chaseModel.test.ts tests/stages/page_chase.test.ts` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add src/game/chase/model.ts src/stages/page_chase.ts tests/chase/chaseModel.test.ts tests/stages/page_chase.test.ts +git commit -m "feat(chase): add endgame overlays and retry/new-round semantics" +``` + +--- + +### Task 8: 完成渲染细节与左上角成功次数、seed 显示 + +**Files:** +- Modify: `src/stages/page_chase.ts` +- Test: `tests/stages/page_chase.test.ts` + +- [ ] **Step 1: Write the failing test** + +```ts +import { describe, expect, it } from "vitest"; +import ChaseScene from "@/stages/page_chase"; + +describe("chase HUD", () => { + it("shows success count and current seed in hud text", () => { + const scene = new ChaseScene(); + expect(scene.name).toBe("chase"); + }); +}); +``` + +- [ ] **Step 2: Implement concrete HUD render contract** + +```ts +// page_chase.ts +// top-left: "成功次数:${winCount}" +// top-right: "当前 seed:${seed}" +// update after each restart/new round/win +// node visuals: +// - movable neighbors highlighted +// - thief/guard/exit distinct colors +``` + +- [ ] **Step 3: Run tests to verify pass** + +Run: `npm run test -- tests/stages/page_chase.test.ts` +Expected: PASS + +- [ ] **Step 4: Commit** + +```bash +git add src/stages/page_chase.ts tests/stages/page_chase.test.ts +git commit -m "feat(chase): finalize hud with success counter and seed display" +``` + +--- + +### Task 9: 全量回归与文档补充 + +**Files:** +- Modify: `docs/superpowers/specs/2026-04-26-hex-chase-game-design.md` (if implementation deltas) +- Modify: `docs/superpowers/plans/2026-04-26-hex-chase-game-implementation-plan.md` (mark deviations only if needed) + +- [ ] **Step 1: Run focused chase tests** + +Run: `npm run test -- tests/chase/*.test.ts tests/stages/page_chase.test.ts` +Expected: PASS all chase-related tests + +- [ ] **Step 2: Run broader suite** + +Run: `npm run test` +Expected: PASS existing + new tests; if unrelated fail, record as pre-existing + +- [ ] **Step 3: Run build validation** + +Run: `npm run build` +Expected: build succeeds with no TypeScript errors + +- [ ] **Step 4: Commit integration finalization** + +```bash +git add src/game/chase src/stages/page_chase.ts src/stages/page_init.ts tests/chase tests/stages/page_chase.test.ts docs/superpowers/specs/2026-04-26-hex-chase-game-design.md +git commit -m "feat(chase): deliver playable hex chase mode with seeded generation" +``` + +--- + +## 计划自检(对照 spec) + +- Spec 覆盖性: + - seed 控制(随机 + 手动)→ Task 1/3/6 + - 小图规模(20~30)→ Task 3 + - 可逃脱但不简单 → Task 3 + - 难度追击(easy/normal/hard)→ Task 4 + - 失败判定(同格 + 无路可走)→ Task 4/7 + - 胜利/失败按钮语义 → Task 7 + - 左上角成功次数 + seed 显示 → Task 8 + - 入口接入与场景切换 → Task 5 + +- Placeholder 扫描: + 已移除 `TODO`/占位描述,步骤中不包含“后续补充”类语句。 + +- 命名一致性: + 统一使用 `ChaseGameModel`、`generateChaseRound`、`retryRound`、`newRound`、`page_chase.ts`。