|
|
@ -68,6 +68,25 @@ describe("chase game model", () => { |
|
|
expect(model.getAvailableMoves()).toEqual(["A", "C"]); |
|
|
expect(model.getAvailableMoves()).toEqual(["A", "C"]); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
it("getState returns a defensive deep copy of snapshot", () => { |
|
|
|
|
|
mockedGenerateChaseRound.mockReturnValue(makeRound(123, "normal")); |
|
|
|
|
|
const model = ChaseGameModel.createWithSeed({ seed: 123, difficulty: "normal" }); |
|
|
|
|
|
|
|
|
|
|
|
const state = model.getState(); |
|
|
|
|
|
state.snapshot.thiefNodeId = "A"; |
|
|
|
|
|
state.snapshot.graph.nodes.B.neighbors.push("D"); |
|
|
|
|
|
state.snapshot.graph.edgeList.push(["A", "D"]); |
|
|
|
|
|
|
|
|
|
|
|
const afterMutation = model.getState(); |
|
|
|
|
|
expect(afterMutation.snapshot.thiefNodeId).toBe("B"); |
|
|
|
|
|
expect(afterMutation.snapshot.graph.nodes.B.neighbors).toEqual(["A", "C"]); |
|
|
|
|
|
expect(afterMutation.snapshot.graph.edgeList).toEqual([ |
|
|
|
|
|
["A", "B"], |
|
|
|
|
|
["B", "C"], |
|
|
|
|
|
["C", "D"], |
|
|
|
|
|
]); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
it("thief move to exit wins and increments winCount", () => { |
|
|
it("thief move to exit wins and increments winCount", () => { |
|
|
mockedGenerateChaseRound.mockReturnValue(makeRound(11, "normal")); |
|
|
mockedGenerateChaseRound.mockReturnValue(makeRound(11, "normal")); |
|
|
const model = ChaseGameModel.createWithSeed({ seed: 11, difficulty: "normal" }); |
|
|
const model = ChaseGameModel.createWithSeed({ seed: 11, difficulty: "normal" }); |
|
|
@ -172,6 +191,101 @@ describe("chase game model", () => { |
|
|
expect(state.turn).toBe(0); |
|
|
expect(state.turn).toBe(0); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
it("retryRound resets rng so same move sequence is reproducible", () => { |
|
|
|
|
|
const easyRound = { |
|
|
|
|
|
snapshot: { |
|
|
|
|
|
seed: 1, |
|
|
|
|
|
difficulty: "easy" as const, |
|
|
|
|
|
graph: { |
|
|
|
|
|
nodes: { |
|
|
|
|
|
A: { id: "A", q: 0, r: 0, neighbors: ["B"] }, |
|
|
|
|
|
B: { id: "B", q: 1, r: 0, neighbors: ["A", "C", "E"] }, |
|
|
|
|
|
C: { id: "C", q: 2, r: 0, neighbors: ["B", "D"] }, |
|
|
|
|
|
D: { id: "D", q: 3, r: 0, neighbors: ["C"] }, |
|
|
|
|
|
E: { id: "E", q: 1, r: 1, neighbors: ["B"] }, |
|
|
|
|
|
}, |
|
|
|
|
|
edgeList: [ |
|
|
|
|
|
["A", "B"], |
|
|
|
|
|
["B", "C"], |
|
|
|
|
|
["C", "D"], |
|
|
|
|
|
["B", "E"], |
|
|
|
|
|
], |
|
|
|
|
|
}, |
|
|
|
|
|
thiefStartNodeId: "D", |
|
|
|
|
|
guardStartNodeId: "B", |
|
|
|
|
|
thiefNodeId: "D", |
|
|
|
|
|
guardNodeId: "B", |
|
|
|
|
|
exitNodeId: "A", |
|
|
|
|
|
status: "playing" as const, |
|
|
|
|
|
}, |
|
|
|
|
|
meta: { hasEscapePath: true, pathLength: 3, attemptsUsed: 1 }, |
|
|
|
|
|
}; |
|
|
|
|
|
mockedGenerateChaseRound.mockReturnValue(easyRound); |
|
|
|
|
|
const model = ChaseGameModel.createWithSeed({ seed: 1, difficulty: "easy" }); |
|
|
|
|
|
|
|
|
|
|
|
model.moveThief("C"); |
|
|
|
|
|
const guardAfterFirstRun = model.getState().snapshot.guardNodeId; |
|
|
|
|
|
|
|
|
|
|
|
model.retryRound(); |
|
|
|
|
|
model.moveThief("C"); |
|
|
|
|
|
const guardAfterRetry = model.getState().snapshot.guardNodeId; |
|
|
|
|
|
|
|
|
|
|
|
expect(guardAfterFirstRun).toBe("A"); |
|
|
|
|
|
expect(guardAfterRetry).toBe("A"); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
it("newRound reseeds rng so guard behavior follows new seed", () => { |
|
|
|
|
|
const roundForSeed1 = { |
|
|
|
|
|
snapshot: { |
|
|
|
|
|
seed: 1, |
|
|
|
|
|
difficulty: "easy" as const, |
|
|
|
|
|
graph: { |
|
|
|
|
|
nodes: { |
|
|
|
|
|
A: { id: "A", q: 0, r: 0, neighbors: ["B"] }, |
|
|
|
|
|
B: { id: "B", q: 1, r: 0, neighbors: ["A", "C", "E"] }, |
|
|
|
|
|
C: { id: "C", q: 2, r: 0, neighbors: ["B", "D"] }, |
|
|
|
|
|
D: { id: "D", q: 3, r: 0, neighbors: ["C"] }, |
|
|
|
|
|
E: { id: "E", q: 1, r: 1, neighbors: ["B"] }, |
|
|
|
|
|
}, |
|
|
|
|
|
edgeList: [ |
|
|
|
|
|
["A", "B"], |
|
|
|
|
|
["B", "C"], |
|
|
|
|
|
["C", "D"], |
|
|
|
|
|
["B", "E"], |
|
|
|
|
|
], |
|
|
|
|
|
}, |
|
|
|
|
|
thiefStartNodeId: "D", |
|
|
|
|
|
guardStartNodeId: "B", |
|
|
|
|
|
thiefNodeId: "D", |
|
|
|
|
|
guardNodeId: "B", |
|
|
|
|
|
exitNodeId: "A", |
|
|
|
|
|
status: "playing" as const, |
|
|
|
|
|
}, |
|
|
|
|
|
meta: { hasEscapePath: true, pathLength: 3, attemptsUsed: 1 }, |
|
|
|
|
|
}; |
|
|
|
|
|
const roundForSeed12 = { |
|
|
|
|
|
...roundForSeed1, |
|
|
|
|
|
snapshot: { |
|
|
|
|
|
...roundForSeed1.snapshot, |
|
|
|
|
|
seed: 12, |
|
|
|
|
|
}, |
|
|
|
|
|
}; |
|
|
|
|
|
mockedGenerateChaseRound |
|
|
|
|
|
.mockReturnValueOnce(roundForSeed1) |
|
|
|
|
|
.mockReturnValueOnce(roundForSeed12); |
|
|
|
|
|
|
|
|
|
|
|
const model = ChaseGameModel.createWithSeed({ seed: 1, difficulty: "easy" }); |
|
|
|
|
|
model.moveThief("C"); |
|
|
|
|
|
const guardWithSeed1 = model.getState().snapshot.guardNodeId; |
|
|
|
|
|
|
|
|
|
|
|
model.newRound(12, "easy"); |
|
|
|
|
|
model.moveThief("C"); |
|
|
|
|
|
const guardWithSeed12 = model.getState().snapshot.guardNodeId; |
|
|
|
|
|
|
|
|
|
|
|
expect(guardWithSeed1).toBe("A"); |
|
|
|
|
|
expect(guardWithSeed12).toBe("C"); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
it("guard strategy follows difficulty probability rule", () => { |
|
|
it("guard strategy follows difficulty probability rule", () => { |
|
|
const baseRound = { |
|
|
const baseRound = { |
|
|
snapshot: { |
|
|
snapshot: { |
|
|
@ -226,4 +340,57 @@ describe("chase game model", () => { |
|
|
easyModel.moveThief("C"); |
|
|
easyModel.moveThief("C"); |
|
|
expect(easyModel.getState().snapshot.guardNodeId).toBe("E"); |
|
|
expect(easyModel.getState().snapshot.guardNodeId).toBe("E"); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
it("normal difficulty boundary uses shortest at 0.85 and non-shortest above", () => { |
|
|
|
|
|
const round = { |
|
|
|
|
|
snapshot: { |
|
|
|
|
|
seed: 88, |
|
|
|
|
|
difficulty: "normal" as const, |
|
|
|
|
|
graph: { |
|
|
|
|
|
nodes: { |
|
|
|
|
|
A: { id: "A", q: 0, r: 0, neighbors: ["B"] }, |
|
|
|
|
|
B: { id: "B", q: 1, r: 0, neighbors: ["A", "C", "E"] }, |
|
|
|
|
|
C: { id: "C", q: 2, r: 0, neighbors: ["B", "D"] }, |
|
|
|
|
|
D: { id: "D", q: 3, r: 0, neighbors: ["C"] }, |
|
|
|
|
|
E: { id: "E", q: 1, r: 1, neighbors: ["B"] }, |
|
|
|
|
|
}, |
|
|
|
|
|
edgeList: [ |
|
|
|
|
|
["A", "B"], |
|
|
|
|
|
["B", "C"], |
|
|
|
|
|
["C", "D"], |
|
|
|
|
|
["B", "E"], |
|
|
|
|
|
], |
|
|
|
|
|
}, |
|
|
|
|
|
thiefStartNodeId: "D", |
|
|
|
|
|
guardStartNodeId: "B", |
|
|
|
|
|
thiefNodeId: "D", |
|
|
|
|
|
guardNodeId: "B", |
|
|
|
|
|
exitNodeId: "A", |
|
|
|
|
|
status: "playing" as const, |
|
|
|
|
|
}, |
|
|
|
|
|
meta: { hasEscapePath: true, pathLength: 3, attemptsUsed: 1 }, |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
mockedGenerateChaseRound.mockReturnValue(round); |
|
|
|
|
|
const atBoundary = ChaseGameModel.createWithSeed( |
|
|
|
|
|
{ seed: 88, difficulty: "normal" }, |
|
|
|
|
|
{ random: () => 0.85 }, |
|
|
|
|
|
); |
|
|
|
|
|
atBoundary.moveThief("C"); |
|
|
|
|
|
expect(atBoundary.getState().snapshot.guardNodeId).toBe("C"); |
|
|
|
|
|
|
|
|
|
|
|
mockedGenerateChaseRound.mockReturnValue(round); |
|
|
|
|
|
let call = 0; |
|
|
|
|
|
const aboveBoundary = ChaseGameModel.createWithSeed( |
|
|
|
|
|
{ seed: 88, difficulty: "normal" }, |
|
|
|
|
|
{ |
|
|
|
|
|
random: () => { |
|
|
|
|
|
call += 1; |
|
|
|
|
|
return call === 1 ? 0.8501 : 0.9; |
|
|
|
|
|
}, |
|
|
|
|
|
}, |
|
|
|
|
|
); |
|
|
|
|
|
aboveBoundary.moveThief("C"); |
|
|
|
|
|
expect(aboveBoundary.getState().snapshot.guardNodeId).toBe("E"); |
|
|
|
|
|
}); |
|
|
}); |
|
|
}); |
|
|
|