import { EndReason, GameRecord, GameResult, MoveRecord } from "@mancala/core"; import { CommonMancalaGame, GameState, GameStep } from "mancala.js"; import { BackendGame, PlayerInfo } from "../models/Game"; import { publicUserId } from "./publicIds"; import { startClock } from "./GameClock"; type HistoryEntry = { playerId: string, moveIndex: number, boardSnapshot: number[], gameSteps: GameStep[] }; // What is kept in Valkey for a live game; enough to rebuild it after a restart. export interface LiveGameSnapshot { version: 1; id: string; player1Id: string; player2Id: string; turnPlayerId: string; state: GameState; board: number[]; history: HistoryEntry[]; players: Record; startedAt: number; moveTimes: number[]; peakSpectators: number; allSpectatorIds: string[]; } export interface GameEnding { endReason: EndReason; // Undefined when the game ends without a winner. winnerId?: string; leftUserId?: string; } export function toSnapshot(game: BackendGame): LiveGameSnapshot { const { mancalaGame } = game; return { version: 1, id: game.id, player1Id: mancalaGame.player1Id, player2Id: mancalaGame.player2Id, turnPlayerId: mancalaGame.turnPlayerId, state: mancalaGame.state, board: mancalaGame.board.getStoneArray(), history: mancalaGame.history as unknown as HistoryEntry[], players: game.players, startedAt: game.startedAt, moveTimes: game.moveTimes, peakSpectators: game.peakSpectators, allSpectatorIds: game.allSpectatorIds, }; } export function isLiveGameSnapshot(value: unknown): value is LiveGameSnapshot { const snapshot = value as LiveGameSnapshot; return typeof snapshot === "object" && snapshot !== null && snapshot.version === 1 && typeof snapshot.id === "string" && Array.isArray(snapshot.board) && snapshot.board.length === 14 && Array.isArray(snapshot.history) && typeof snapshot.players === "object"; } // Rebuilds a live game with real engine objects (rules, pits), so it can be played on. export function fromSnapshot(snapshot: LiveGameSnapshot, params: { now: number, gameClockMs: number }): BackendGame { const mancalaGame = new CommonMancalaGame(snapshot.id, snapshot.player1Id, snapshot.player2Id); snapshot.board.forEach((stoneCount, index) => (mancalaGame.board.pits[index].stoneCount = stoneCount)); mancalaGame.turnPlayerId = snapshot.turnPlayerId; mancalaGame.state = snapshot.state; mancalaGame.history.push(...snapshot.history); const player = (id: string): PlayerInfo => snapshot.players[id] ?? { userId: id, isGuest: true }; return { id: snapshot.id, mancalaGame, gameUsersConnectionInfo: { user1ConnectionInfo: { ...player(snapshot.player1Id), isOnline: false }, user2ConnectionInfo: { ...player(snapshot.player2Id), isOnline: false }, }, spectatorIds: [], allSpectatorIds: snapshot.allSpectatorIds, peakSpectators: snapshot.peakSpectators, players: snapshot.players, startedAt: snapshot.startedAt, moveTimes: snapshot.moveTimes, clock: startClock(params.gameClockMs, params.now), moveRemainingMs: [], }; } export function getResult(game: BackendGame, ending: GameEnding): GameResult | null { const { player1Id, player2Id } = game.mancalaGame; if (ending.winnerId === player1Id) return "player1"; if (ending.winnerId === player2Id) return "player2"; return ending.endReason === "completed" ? "draw" : null; } // The record saved for a finished game; every id in it is a public id. export function toGameRecord(game: BackendGame, ending: GameEnding, endedAt: number): GameRecord { const { mancalaGame } = game; const history = mancalaGame.history as unknown as HistoryEntry[]; const playerSummary = (id: string) => { const player = game.players[id]; const summary = { userId: publicUserId(id), isGuest: player?.isGuest ?? true }; return player?.name === undefined ? summary : { ...summary, name: player.name }; }; const moves: MoveRecord[] = history.map((entry, i) => ({ playerId: publicUserId(entry.playerId), moveIndex: entry.moveIndex, boardSnapshot: entry.boardSnapshot, gameSteps: entry.gameSteps, at: game.moveTimes[i] ?? endedAt, })); const record: GameRecord = { id: game.id, player1: playerSummary(mancalaGame.player1Id), player2: playerSummary(mancalaGame.player2Id), board: mancalaGame.board.getStoneArray(), result: getResult(game, ending), endReason: ending.endReason, startedAt: game.startedAt, endedAt, moveCount: moves.length, peakSpectators: game.peakSpectators, totalSpectators: game.allSpectatorIds.length, moves, }; if (ending.leftUserId !== undefined) record.leftUserId = publicUserId(ending.leftUserId); return record; }