Browse Source
- Relocate generator/model/graph/rng/types into logic/ - Add solvability helpers and tests - Fix vitest mock path for logic/generator after move - Fix page_chase stage test import (page_chase.ts) Made-with: Cursormaster
21 changed files with 1665 additions and 1324 deletions
@ -0,0 +1,18 @@ |
|||||
|
export { default as ChaseScene } from "../scene"; |
||||
|
export { ChaseGameModel } from "./model"; |
||||
|
export { createRng, normalizeSeed } from "./rng"; |
||||
|
export { axialNeighbors, isConnected, shortestPath, shortestPathLength } from "./hexGraph"; |
||||
|
export { generateChaseRound } from "./generator"; |
||||
|
export { optimalGuardStep, thiefHasWinningStrategy } from "./solvability"; |
||||
|
export type { |
||||
|
Difficulty, |
||||
|
GameGraph, |
||||
|
GameStatus, |
||||
|
GraphNode, |
||||
|
NodeId, |
||||
|
} from "./types"; |
||||
|
export type { |
||||
|
ChaseRound, |
||||
|
GenerateChaseRoundInput, |
||||
|
GenerateChaseRoundOptions, |
||||
|
} from "./generator"; |
||||
@ -0,0 +1,103 @@ |
|||||
|
import { shortestPathLength } from "./hexGraph"; |
||||
|
import type { GameGraph, NodeId } from "./types"; |
||||
|
|
||||
|
/** |
||||
|
* 官兵一步:始终走离小偷最短的邻格(与 ChaseGameModel hard 档一致,tie-break 按 nodeId)。 |
||||
|
*/ |
||||
|
export function optimalGuardStep(graph: GameGraph, guard: NodeId, thief: NodeId): NodeId { |
||||
|
const guardNode = graph.nodes[guard]; |
||||
|
if (!guardNode || guardNode.neighbors.length === 0) { |
||||
|
return guard; |
||||
|
} |
||||
|
|
||||
|
const movesWithDistance = guardNode.neighbors.map((neighborId) => ({ |
||||
|
nodeId: neighborId, |
||||
|
distance: shortestPathLength(graph, neighborId, thief), |
||||
|
})); |
||||
|
|
||||
|
const finiteMoves = movesWithDistance.filter((item) => Number.isFinite(item.distance)); |
||||
|
if (finiteMoves.length === 0) { |
||||
|
return guard; |
||||
|
} |
||||
|
|
||||
|
finiteMoves.sort((a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)); |
||||
|
return finiteMoves[0].nodeId; |
||||
|
} |
||||
|
|
||||
|
function stateKey(thief: NodeId, guard: NodeId): string { |
||||
|
return `${thief}|${guard}`; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 是否存在小偷的必胜策略(对最优追击官兵)。 |
||||
|
* 回合:小偷先走一步;若未到出口,官兵走一步;重复。 |
||||
|
* |
||||
|
* 用交替可达性的最小不动点处理状态环,避免递归栈溢出。 |
||||
|
*/ |
||||
|
export function thiefHasWinningStrategy( |
||||
|
graph: GameGraph, |
||||
|
thiefStart: NodeId, |
||||
|
guardStart: NodeId, |
||||
|
exitNodeId: NodeId, |
||||
|
): boolean { |
||||
|
if (thiefStart === guardStart) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
const nodeIds = Object.keys(graph.nodes); |
||||
|
const pairs: Array<[NodeId, NodeId]> = []; |
||||
|
for (const t of nodeIds) { |
||||
|
for (const g of nodeIds) { |
||||
|
if (t !== g) { |
||||
|
pairs.push([t, g]); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const winning = new Set<string>(); |
||||
|
|
||||
|
for (const [t, g] of pairs) { |
||||
|
const tNode = graph.nodes[t]; |
||||
|
if (!tNode) { |
||||
|
continue; |
||||
|
} |
||||
|
const canStepToExit = tNode.neighbors.some((m) => m !== g && m === exitNodeId); |
||||
|
if (canStepToExit) { |
||||
|
winning.add(stateKey(t, g)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let changed = true; |
||||
|
while (changed) { |
||||
|
changed = false; |
||||
|
for (const [t, g] of pairs) { |
||||
|
const key = stateKey(t, g); |
||||
|
if (winning.has(key)) { |
||||
|
continue; |
||||
|
} |
||||
|
const tNode = graph.nodes[t]; |
||||
|
if (!tNode) { |
||||
|
continue; |
||||
|
} |
||||
|
for (const m of tNode.neighbors) { |
||||
|
if (m === g) { |
||||
|
continue; |
||||
|
} |
||||
|
if (m === exitNodeId) { |
||||
|
continue; |
||||
|
} |
||||
|
const g2 = optimalGuardStep(graph, g, m); |
||||
|
if (g2 === m) { |
||||
|
continue; |
||||
|
} |
||||
|
if (winning.has(stateKey(m, g2))) { |
||||
|
winning.add(key); |
||||
|
changed = true; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return winning.has(stateKey(thiefStart, guardStart)); |
||||
|
} |
||||
@ -0,0 +1,53 @@ |
|||||
|
import { describe, expect, it } from "vitest"; |
||||
|
import { optimalGuardStep, thiefHasWinningStrategy } from "../../src/stages/games/chase/logic/solvability"; |
||||
|
import type { GameGraph } from "../../src/stages/games/chase/logic/types"; |
||||
|
|
||||
|
function lineGraph(ids: string[]): GameGraph { |
||||
|
const nodes: GameGraph["nodes"] = {}; |
||||
|
const edgeList: Array<[string, string]> = []; |
||||
|
for (let i = 0; i < ids.length; i += 1) { |
||||
|
const id = ids[i]; |
||||
|
const prev = ids[i - 1]; |
||||
|
const next = ids[i + 1]; |
||||
|
const neighbors: string[] = []; |
||||
|
if (prev) { |
||||
|
neighbors.push(prev); |
||||
|
} |
||||
|
if (next) { |
||||
|
neighbors.push(next); |
||||
|
} |
||||
|
nodes[id] = { id, q: i, r: 0, neighbors }; |
||||
|
} |
||||
|
for (let i = 0; i < ids.length - 1; i += 1) { |
||||
|
edgeList.push([ids[i], ids[i + 1]]); |
||||
|
} |
||||
|
return { nodes, edgeList }; |
||||
|
} |
||||
|
|
||||
|
describe("solvability (optimal guard)", () => { |
||||
|
it("returns false when guard blocks the only exit from a leaf", () => { |
||||
|
const graph = lineGraph(["L", "N", "E"]); |
||||
|
expect(thiefHasWinningStrategy(graph, "L", "N", "E")).toBe(false); |
||||
|
}); |
||||
|
|
||||
|
it("returns true when thief reaches exit in one step before guard reacts", () => { |
||||
|
const graph: GameGraph = { |
||||
|
nodes: { |
||||
|
T: { id: "T", q: 0, r: 0, neighbors: ["E", "G"] }, |
||||
|
E: { id: "E", q: 1, r: 0, neighbors: ["T"] }, |
||||
|
G: { id: "G", q: 0, r: 1, neighbors: ["T"] }, |
||||
|
}, |
||||
|
edgeList: [ |
||||
|
["T", "E"], |
||||
|
["T", "G"], |
||||
|
], |
||||
|
}; |
||||
|
expect(thiefHasWinningStrategy(graph, "T", "G", "E")).toBe(true); |
||||
|
}); |
||||
|
|
||||
|
it("optimalGuardStep matches shortest-path tie-break toward thief", () => { |
||||
|
const graph = lineGraph(["A", "B", "C"]); |
||||
|
expect(optimalGuardStep(graph, "A", "C")).toBe("B"); |
||||
|
expect(optimalGuardStep(graph, "C", "A")).toBe("B"); |
||||
|
}); |
||||
|
}); |
||||
Loading…
Reference in new issue