Browse Source

fix(chase): harden endgame action lock and loseReason coverage

Made-with: Cursor
master
npmrun 2 weeks ago
parent
commit
059605e6c3
  1. 29
      src/stages/page_chase.ts
  2. 43
      tests/chase/chaseModel.test.ts
  3. 20
      tests/stages/page_chase.test.ts

29
src/stages/page_chase.ts

@ -7,6 +7,7 @@ import { Container, Graphics, Text, TextStyle, type Ticker } from "pixi.js";
const NODE_RADIUS = 18;
const NODE_GAP = 58;
const END_ACTION_COOLDOWN_MS = 250;
type ChaseModelLike = Pick<
ChaseGameModel,
@ -52,6 +53,7 @@ export default class ChaseScene extends BaseScene {
private inputAttached = false;
private unsubscribeStageChange?: () => void;
private endActionLocked = false;
private lastEndActionAt = 0;
constructor(options: ChaseSceneOptions = {}) {
super("chase", SceneType.Normal);
@ -223,30 +225,42 @@ export default class ChaseScene extends BaseScene {
}
private handleRetryClick(): void {
if (!this.model || this.endActionLocked) {
if (!this.model || this.endActionLocked || !this.canRunEndAction()) {
return;
}
this.endActionLocked = true;
try {
this.model.retryRound();
this.refreshView();
} finally {
this.endActionLocked = false;
}
}
private handleNewGameClick(): void {
if (!this.model || this.endActionLocked) {
if (!this.model || this.endActionLocked || !this.canRunEndAction()) {
return;
}
this.endActionLocked = true;
try {
this.resetSetup();
this.startGame();
} finally {
this.endActionLocked = false;
}
}
private handleRestartClick(): void {
if (!this.model || this.endActionLocked) {
if (!this.model || this.endActionLocked || !this.canRunEndAction()) {
return;
}
this.endActionLocked = true;
try {
this.resetSetup();
this.startGame();
} finally {
this.endActionLocked = false;
}
}
private buildSetupControls(): void {
@ -457,4 +471,13 @@ export default class ChaseScene extends BaseScene {
this.retryButton.visible = true;
this.newGameButton.visible = true;
}
private canRunEndAction(): boolean {
const now = Date.now();
if (now - this.lastEndActionAt < END_ACTION_COOLDOWN_MS) {
return false;
}
this.lastEndActionAt = now;
return true;
}
}

43
tests/chase/chaseModel.test.ts

@ -190,6 +190,26 @@ describe("chase game model", () => {
expect(retried.guardNodeId).toBe(initial.guardNodeId);
});
it("clears loseReason after retryRound", () => {
mockedGenerateChaseRound.mockReturnValueOnce({
...makeRound(99, "normal"),
snapshot: {
...makeRound(99, "normal").snapshot,
thiefStartNodeId: "A",
thiefNodeId: "A",
guardStartNodeId: "C",
guardNodeId: "C",
exitNodeId: "D",
},
});
const model = ChaseGameModel.createWithSeed({ seed: 99, difficulty: "normal" });
model.moveThief("B");
expect(model.getState().loseReason).toBe("caught");
model.retryRound();
expect(model.getState().loseReason).toBeUndefined();
});
it("newRound switches seed/difficulty and keeps winCount", () => {
mockedGenerateChaseRound
.mockReturnValueOnce(makeRound(55, "easy"))
@ -244,6 +264,29 @@ describe("chase game model", () => {
expect(after.thiefStartNodeId).toBe("A");
});
it("clears loseReason after newRound", () => {
mockedGenerateChaseRound
.mockReturnValueOnce({
...makeRound(120, "hard"),
snapshot: {
...makeRound(120, "hard").snapshot,
thiefStartNodeId: "A",
thiefNodeId: "A",
guardStartNodeId: "C",
guardNodeId: "C",
exitNodeId: "D",
},
})
.mockReturnValueOnce(makeRound(121, "normal"));
const model = ChaseGameModel.createWithSeed({ seed: 120, difficulty: "hard" });
model.moveThief("B");
expect(model.getState().loseReason).toBe("caught");
model.newRound(121, "normal");
expect(model.getState().loseReason).toBeUndefined();
});
it("retryRound resets rng so same move sequence is reproducible", () => {
const easyRound = {
snapshot: {

20
tests/stages/page_chase.test.ts

@ -347,4 +347,24 @@ describe("chase stage skeleton", () => {
expect(newRound).toHaveBeenCalledTimes(1);
expect(newRound.mock.calls[0][1]).toBe("easy");
});
it("shows caught lose message text", () => {
const scene = new ChaseScene({
model: {
getState: vi.fn().mockReturnValue({
...createSceneState(),
snapshot: {
...createSceneState().snapshot,
status: "lose",
},
loseReason: "caught",
}),
moveThief: vi.fn(),
newRound: vi.fn(),
retryRound: vi.fn(),
} as any,
});
void (scene as any).onSceneLayout();
expect((scene as any).resultText.text).toContain("你被抓住了");
});
});

Loading…
Cancel
Save