Browse Source
- Add implementation plan for hex chase game including architecture and tech stack. - Create essential files for game mechanics: types, RNG, hex graph utilities, and generator. - Implement tests for RNG, hex graph functions, and game generator to ensure functionality and determinism. - Modify game initialization to include new chase gameplay entry point.master
1 changed files with 824 additions and 0 deletions
@ -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<NodeId, GraphNode>; |
|||
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<NodeId>([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<NodeId>([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<typeof generateChaseRound>["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<void> { |
|||
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`。 |
|||
Loading…
Reference in new issue