|
|
@ -9,17 +9,14 @@ import type { Difficulty, NodeId } from "./types"; |
|
|
|
|
|
|
|
|
type GameStatus = "playing" | "win" | "lose"; |
|
|
type GameStatus = "playing" | "win" | "lose"; |
|
|
|
|
|
|
|
|
interface ChaseGameState { |
|
|
type ModelSnapshot = Omit<ChaseRound["snapshot"], "status"> & { |
|
|
seed: number; |
|
|
|
|
|
difficulty: Difficulty; |
|
|
|
|
|
status: GameStatus; |
|
|
status: GameStatus; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
interface ChaseGameState { |
|
|
|
|
|
snapshot: ModelSnapshot; |
|
|
winCount: number; |
|
|
winCount: number; |
|
|
thiefNodeId: NodeId; |
|
|
turn: number; |
|
|
guardNodeId: NodeId; |
|
|
|
|
|
thiefStartNodeId: NodeId; |
|
|
|
|
|
guardStartNodeId: NodeId; |
|
|
|
|
|
exitNodeId: NodeId; |
|
|
|
|
|
graph: ChaseRound["snapshot"]["graph"]; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
interface ChaseGameModelOptions { |
|
|
interface ChaseGameModelOptions { |
|
|
@ -34,25 +31,26 @@ const GUARD_SHORTEST_RATE: Record<Difficulty, number> = { |
|
|
|
|
|
|
|
|
function cloneRound(round: ChaseRound): ChaseGameState { |
|
|
function cloneRound(round: ChaseRound): ChaseGameState { |
|
|
return { |
|
|
return { |
|
|
seed: round.snapshot.seed, |
|
|
snapshot: { |
|
|
difficulty: round.snapshot.difficulty, |
|
|
...round.snapshot, |
|
|
status: round.snapshot.status, |
|
|
status: "playing", |
|
|
|
|
|
}, |
|
|
winCount: 0, |
|
|
winCount: 0, |
|
|
thiefNodeId: round.snapshot.thiefNodeId, |
|
|
turn: 0, |
|
|
guardNodeId: round.snapshot.guardNodeId, |
|
|
|
|
|
thiefStartNodeId: round.snapshot.thiefStartNodeId, |
|
|
|
|
|
guardStartNodeId: round.snapshot.guardStartNodeId, |
|
|
|
|
|
exitNodeId: round.snapshot.exitNodeId, |
|
|
|
|
|
graph: round.snapshot.graph, |
|
|
|
|
|
}; |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export class ChaseGameModel { |
|
|
export class ChaseGameModel { |
|
|
private state: ChaseGameState; |
|
|
private state: ChaseGameState; |
|
|
|
|
|
private initialSnapshot: ModelSnapshot; |
|
|
private random: () => number; |
|
|
private random: () => number; |
|
|
|
|
|
|
|
|
private constructor(state: ChaseGameState, random: () => number) { |
|
|
private constructor(state: ChaseGameState, random: () => number) { |
|
|
this.state = state; |
|
|
this.state = state; |
|
|
|
|
|
this.initialSnapshot = { |
|
|
|
|
|
...state.snapshot, |
|
|
|
|
|
graph: state.snapshot.graph, |
|
|
|
|
|
}; |
|
|
this.random = random; |
|
|
this.random = random; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -62,29 +60,32 @@ export class ChaseGameModel { |
|
|
): ChaseGameModel { |
|
|
): ChaseGameModel { |
|
|
const round = generateChaseRound(input); |
|
|
const round = generateChaseRound(input); |
|
|
const state = cloneRound(round); |
|
|
const state = cloneRound(round); |
|
|
const rng = options.random ?? createRng(normalizeSeed(state.seed)); |
|
|
const rng = options.random ?? createRng(normalizeSeed(state.snapshot.seed)); |
|
|
return new ChaseGameModel(state, rng); |
|
|
return new ChaseGameModel(state, rng); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
getState(): ChaseGameState { |
|
|
getState(): ChaseGameState { |
|
|
return { ...this.state }; |
|
|
return { |
|
|
|
|
|
...this.state, |
|
|
|
|
|
snapshot: { ...this.state.snapshot }, |
|
|
|
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
getAvailableMoves(): NodeId[] { |
|
|
getAvailableMoves(): NodeId[] { |
|
|
if (this.state.status !== "playing") { |
|
|
if (this.state.snapshot.status !== "playing") { |
|
|
return []; |
|
|
return []; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const node = this.state.graph.nodes[this.state.thiefNodeId]; |
|
|
const node = this.state.snapshot.graph.nodes[this.state.snapshot.thiefNodeId]; |
|
|
if (!node) { |
|
|
if (!node) { |
|
|
return []; |
|
|
return []; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return node.neighbors.filter((id) => id !== this.state.guardNodeId); |
|
|
return node.neighbors.filter((id) => id !== this.state.snapshot.guardNodeId); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
moveThief(target: NodeId): ChaseGameState { |
|
|
moveThief(target: NodeId): ChaseGameState { |
|
|
if (this.state.status !== "playing") { |
|
|
if (this.state.snapshot.status !== "playing") { |
|
|
return this.getState(); |
|
|
return this.getState(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -93,43 +94,55 @@ export class ChaseGameModel { |
|
|
return this.getState(); |
|
|
return this.getState(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
this.state.thiefNodeId = target; |
|
|
this.state.turn += 1; |
|
|
if (this.state.thiefNodeId === this.state.exitNodeId) { |
|
|
this.state.snapshot.thiefNodeId = target; |
|
|
this.state.status = "win"; |
|
|
if (this.state.snapshot.thiefNodeId === this.state.snapshot.exitNodeId) { |
|
|
|
|
|
this.state.snapshot.status = "win"; |
|
|
this.state.winCount += 1; |
|
|
this.state.winCount += 1; |
|
|
return this.getState(); |
|
|
return this.getState(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
this.moveGuardOneStep(); |
|
|
this.moveGuardOneStep(); |
|
|
|
|
|
|
|
|
if (this.state.guardNodeId === this.state.thiefNodeId) { |
|
|
if (this.state.snapshot.guardNodeId === this.state.snapshot.thiefNodeId) { |
|
|
this.state.status = "lose"; |
|
|
this.state.snapshot.status = "lose"; |
|
|
return this.getState(); |
|
|
return this.getState(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (this.getAvailableMoves().length === 0) { |
|
|
if (this.getAvailableMoves().length === 0) { |
|
|
this.state.status = "lose"; |
|
|
this.state.snapshot.status = "lose"; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return this.getState(); |
|
|
return this.getState(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
retryRound(): ChaseGameState { |
|
|
retryRound(): ChaseGameState { |
|
|
return this.newRound(this.state.seed, this.state.difficulty); |
|
|
const winCount = this.state.winCount; |
|
|
|
|
|
this.state = { |
|
|
|
|
|
snapshot: { |
|
|
|
|
|
...this.initialSnapshot, |
|
|
|
|
|
graph: this.initialSnapshot.graph, |
|
|
|
|
|
}, |
|
|
|
|
|
winCount, |
|
|
|
|
|
turn: 0, |
|
|
|
|
|
}; |
|
|
|
|
|
return this.getState(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
newRound(seed: number | string, difficulty: Difficulty): ChaseGameState { |
|
|
newRound(seed: number | string, difficulty: Difficulty): ChaseGameState { |
|
|
const round = generateChaseRound({ seed, difficulty }); |
|
|
const round = generateChaseRound({ seed, difficulty }); |
|
|
const winCount = this.state.winCount; |
|
|
const winCount = this.state.winCount; |
|
|
this.state = { |
|
|
const newState = cloneRound(round); |
|
|
...cloneRound(round), |
|
|
this.state = { ...newState, winCount }; |
|
|
winCount, |
|
|
this.initialSnapshot = { |
|
|
|
|
|
...newState.snapshot, |
|
|
|
|
|
graph: newState.snapshot.graph, |
|
|
}; |
|
|
}; |
|
|
return this.getState(); |
|
|
return this.getState(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private moveGuardOneStep(): void { |
|
|
private moveGuardOneStep(): void { |
|
|
const guardNode = this.state.graph.nodes[this.state.guardNodeId]; |
|
|
const guardNode = this.state.snapshot.graph.nodes[this.state.snapshot.guardNodeId]; |
|
|
if (!guardNode || guardNode.neighbors.length === 0) { |
|
|
if (!guardNode || guardNode.neighbors.length === 0) { |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
@ -137,9 +150,9 @@ export class ChaseGameModel { |
|
|
const movesWithDistance = guardNode.neighbors.map((neighborId) => ({ |
|
|
const movesWithDistance = guardNode.neighbors.map((neighborId) => ({ |
|
|
nodeId: neighborId, |
|
|
nodeId: neighborId, |
|
|
distance: shortestPathLength( |
|
|
distance: shortestPathLength( |
|
|
this.state.graph, |
|
|
this.state.snapshot.graph, |
|
|
neighborId, |
|
|
neighborId, |
|
|
this.state.thiefNodeId, |
|
|
this.state.snapshot.thiefNodeId, |
|
|
), |
|
|
), |
|
|
})); |
|
|
})); |
|
|
|
|
|
|
|
|
@ -153,13 +166,13 @@ export class ChaseGameModel { |
|
|
const shortestMoves = finiteMoves.filter((item) => item.distance === shortestDistance); |
|
|
const shortestMoves = finiteMoves.filter((item) => item.distance === shortestDistance); |
|
|
const nonShortestMoves = finiteMoves.filter((item) => item.distance !== shortestDistance); |
|
|
const nonShortestMoves = finiteMoves.filter((item) => item.distance !== shortestDistance); |
|
|
|
|
|
|
|
|
const shortestRate = GUARD_SHORTEST_RATE[this.state.difficulty]; |
|
|
const shortestRate = GUARD_SHORTEST_RATE[this.state.snapshot.difficulty]; |
|
|
let nextMove = shortestMoves[0]; |
|
|
let nextMove = shortestMoves[0]; |
|
|
if (shortestRate < 1 && this.random() > shortestRate && nonShortestMoves.length > 0) { |
|
|
if (shortestRate < 1 && this.random() > shortestRate && nonShortestMoves.length > 0) { |
|
|
const idx = Math.floor(this.random() * nonShortestMoves.length); |
|
|
const idx = Math.floor(this.random() * nonShortestMoves.length); |
|
|
nextMove = nonShortestMoves[idx]; |
|
|
nextMove = nonShortestMoves[idx]; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
this.state.guardNodeId = nextMove.nodeId; |
|
|
this.state.snapshot.guardNodeId = nextMove.nodeId; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|