Browse Source

fix(chase): clean graph rerender nodes and improve scene testability

Made-with: Cursor
master
npmrun 2 weeks ago
parent
commit
75ecb862b4
  1. 37
      src/stages/page_chase.ts
  2. 54
      tests/stages/page_chase.test.ts

37
src/stages/page_chase.ts

@ -7,14 +7,23 @@ import { Container, Graphics, Text, TextStyle } from "pixi.js";
const NODE_RADIUS = 18; const NODE_RADIUS = 18;
const NODE_GAP = 58; const NODE_GAP = 58;
type ChaseModelLike = Pick<ChaseGameModel, "getState" | "moveThief">;
type ChaseSceneOptions = {
model?: ChaseModelLike;
modelFactory?: () => ChaseModelLike;
};
export default class ChaseScene extends BaseScene { export default class ChaseScene extends BaseScene {
stage = new Container(); stage = new Container();
private model?: ChaseGameModel; private model?: ChaseModelLike;
private readonly modelFactory?: () => ChaseModelLike;
private hudText?: Text; private hudText?: Text;
private graphLayer = new Container(); private graphLayer = new Container();
constructor() { constructor(options: ChaseSceneOptions = {}) {
super("chase", SceneType.Normal); super("chase", SceneType.Normal);
this.model = options.model;
this.modelFactory = options.modelFactory;
} }
protected async onSceneLayout(): Promise<void> { protected async onSceneLayout(): Promise<void> {
@ -36,14 +45,18 @@ export default class ChaseScene extends BaseScene {
this.graphLayer.position.set(140, 140); this.graphLayer.position.set(140, 140);
this.stage.addChild(this.graphLayer); this.stage.addChild(this.graphLayer);
this.model = ChaseGameModel.createWithSeed({ if (!this.model) {
seed: 20260426, this.model =
difficulty: "normal", this.modelFactory?.() ??
}); ChaseGameModel.createWithSeed({
this.renderFromModel(); seed: 20260426,
difficulty: "normal",
});
}
this.refreshView();
} }
private renderFromModel(): void { public refreshView(): void {
if (!this.model || !this.hudText) { if (!this.model || !this.hudText) {
return; return;
} }
@ -51,7 +64,11 @@ export default class ChaseScene extends BaseScene {
const state = this.model.getState(); const state = this.model.getState();
this.hudText.text = `Turn ${state.turn} | ${state.snapshot.status} | thief=${state.snapshot.thiefNodeId} guard=${state.snapshot.guardNodeId}`; this.hudText.text = `Turn ${state.turn} | ${state.snapshot.status} | thief=${state.snapshot.thiefNodeId} guard=${state.snapshot.guardNodeId}`;
this.graphLayer.removeChildren(); const oldChildren = this.graphLayer.removeChildren();
for (const child of oldChildren) {
child.destroy({ children: true });
}
const nodes = Object.values(state.snapshot.graph.nodes); const nodes = Object.values(state.snapshot.graph.nodes);
for (const node of nodes) { for (const node of nodes) {
const nodeView = new Graphics(); const nodeView = new Graphics();
@ -79,6 +96,6 @@ export default class ChaseScene extends BaseScene {
return; return;
} }
this.model.moveThief(targetNodeId); this.model.moveThief(targetNodeId);
this.renderFromModel(); this.refreshView();
} }
} }

54
tests/stages/page_chase.test.ts

@ -1,6 +1,7 @@
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import ChaseScene from "../../src/stages/page_chase"; import ChaseScene from "../../src/stages/page_chase";
import InitScene from "../../src/stages/page_init"; import InitScene from "../../src/stages/page_init";
import { Text } from "pixi.js";
describe("chase stage skeleton", () => { describe("chase stage skeleton", () => {
it("can instantiate chase scene with name=chase", () => { it("can instantiate chase scene with name=chase", () => {
@ -17,6 +18,59 @@ describe("chase stage skeleton", () => {
expect(moveThief).toHaveBeenCalledWith("n-1"); expect(moveThief).toHaveBeenCalledWith("n-1");
}); });
it("cleans up old graph nodes when re-rendering", () => {
const model = {
moveThief: vi.fn(),
getState: vi
.fn()
.mockReturnValueOnce({
turn: 0,
snapshot: {
status: "playing",
thiefNodeId: "A",
guardNodeId: "B",
exitNodeId: "B",
graph: {
nodes: {
A: { id: "A", q: 0, r: 0, neighbors: ["B"] },
B: { id: "B", q: 1, r: 0, neighbors: ["A"] },
},
edgeList: [["A", "B"]],
},
},
})
.mockReturnValueOnce({
turn: 1,
snapshot: {
status: "playing",
thiefNodeId: "B",
guardNodeId: "A",
exitNodeId: "B",
graph: {
nodes: {
A: { id: "A", q: 0, r: 0, neighbors: ["B"] },
B: { id: "B", q: 1, r: 0, neighbors: ["A"] },
},
edgeList: [["A", "B"]],
},
},
}),
};
const scene = new ChaseScene({ model: model as any });
(scene as any).hudText = new Text({ text: "" });
scene.refreshView();
const firstChildren = [...(scene as any).graphLayer.children];
expect(firstChildren.length).toBe(2);
scene.refreshView();
const secondChildren = [...(scene as any).graphLayer.children];
expect(secondChildren.length).toBe(2);
expect(secondChildren[0]).not.toBe(firstChildren[0]);
expect((firstChildren[0] as any).destroyed).toBe(true);
expect((firstChildren[1] as any).destroyed).toBe(true);
});
it("init start entry routes to chase", () => { it("init start entry routes to chase", () => {
const scene = new InitScene(); const scene = new InitScene();
const changeScene = vi.fn(); const changeScene = vi.fn();

Loading…
Cancel
Save