export type Mark = "X" | "O"; export type Winner = Mark | "draw" | null; export type Move = { cell: number; mark: Mark; }; export type GameState = { board: Array; turn: Mark; history: Move[]; }; export class TicTacToeGame { board: Array; turn: Mark; history: Move[]; constructor(state?: GameState) { const parsed = state ? parseGameState(state) : initialState(); this.board = parsed.board; this.turn = parsed.turn; this.history = parsed.history; } play(cell: number, mark: Mark): boolean { if (!Number.isInteger(cell) || cell < 0 || cell > 8 || mark !== this.turn || this.board[cell] || this.winner()) return false; this.board[cell] = mark; this.history.push({ cell, mark }); this.turn = mark === "X" ? "O" : "X"; return true; } legalMoves(): number[] { if (this.winner()) return []; return this.board.flatMap((mark, cell) => mark ? [] : [cell]); } winner(): Winner { const winners = winnersFor(this.board); if (winners.length) return winners[0]; return this.board.every(Boolean) ? "draw" : null; } status(): string { const winner = this.winner(); if (winner === "draw") return "Draw"; if (winner) return `${winner} wins`; return `${this.turn} to move`; } toState(): GameState { return { board: this.board.slice(), turn: this.turn, history: this.history.slice() }; } static fromState(state: unknown): TicTacToeGame { return new TicTacToeGame(parseGameState(state)); } } export function boardPrompt(game: TicTacToeGame): string { const cells = game.board.map((mark, i) => mark ?? String(i + 1)); return [ `${cells[0]} | ${cells[1]} | ${cells[2]}`, `${cells[3]} | ${cells[4]} | ${cells[5]}`, `${cells[6]} | ${cells[7]} | ${cells[8]}`, ].join("\n"); } export function parseGameState(value: unknown): GameState { if (!isRecord(value)) throw new Error("Invalid state"); if (!Array.isArray(value.board) || value.board.length !== 9) throw new Error("Invalid board"); if (value.turn !== "X" && value.turn !== "O") throw new Error("Invalid turn"); if (!Array.isArray(value.history)) throw new Error("Invalid history"); const board = value.board.map(parseCell); const turn: Mark = value.turn; const history = value.history.map(parseMove); const state = { board, turn, history }; validateState(state); return state; } function initialState(): GameState { return { board: Array(9).fill(null), turn: "X", history: [] }; } function parseCell(value: unknown): Mark | null { if (value === null || value === "X" || value === "O") return value; throw new Error("Invalid board"); } function parseMove(value: unknown): Move { if (!isRecord(value) || (value.mark !== "X" && value.mark !== "O")) throw new Error("Invalid history"); const cell = value.cell; if (typeof cell !== "number" || !Number.isInteger(cell) || cell < 0 || cell > 8) { throw new Error("Invalid history"); } return { cell, mark: value.mark }; } function validateState(state: GameState): void { const xs = state.board.filter((mark) => mark === "X").length; const os = state.board.filter((mark) => mark === "O").length; if (os > xs || xs > os + 1) throw new Error("Invalid mark counts"); const winners = winnersFor(state.board); const winnerSet = new Set(winners); if (winnerSet.size > 1) throw new Error("Invalid board: mixed winners"); if (state.turn !== (xs === os ? "X" : "O")) throw new Error("Invalid turn"); if (winners[0] === "X" && xs !== os + 1) throw new Error("Invalid winner"); if (winners[0] === "O" && xs !== os) throw new Error("Invalid winner"); } function winnersFor(board: Array): Mark[] { return LINES.flatMap(([a, b, c]) => board[a] && board[a] === board[b] && board[a] === board[c] ? [board[a]] : []); } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } export const LINES = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ] as const;