import { Part } from "@/core/part"; import { createRegion } from "@/core/region"; import { createGameCommandRegistry, createPromptDef, IGameContext, } from "@/core/game"; const BOARD_SIZE = 3; const MAX_TURNS = BOARD_SIZE * BOARD_SIZE; const WINNING_LINES: number[][][] = [ [ [0, 0], [0, 1], [0, 2], ], [ [1, 0], [1, 1], [1, 2], ], [ [2, 0], [2, 1], [2, 2], ], [ [0, 0], [1, 0], [2, 0], ], [ [0, 1], [1, 1], [2, 1], ], [ [0, 2], [1, 2], [2, 2], ], [ [0, 0], [1, 1], [2, 2], ], [ [0, 2], [1, 1], [2, 0], ], ]; export type PlayerType = "X" | "O"; export type WinnerType = PlayerType | "draw" | null; export type TicTacToePart = Part<{ player: PlayerType }>; export function createInitialState() { return { board: createRegion("board", [ { name: "x", min: 0, max: BOARD_SIZE - 1 }, { name: "y", min: 0, max: BOARD_SIZE - 1 }, ]), parts: {} as Record, currentPlayer: "X" as PlayerType, winner: null as WinnerType, turn: 0, }; } export type TicTacToeState = ReturnType; export type TicTacToeGame = IGameContext; export const registry = createGameCommandRegistry(); export const prompts = { play: createPromptDef<[PlayerType, number, number]>( "play ", ), }; export async function start(game: TicTacToeGame) { while (true) { const currentPlayer = game.value.currentPlayer; const turnNumber = game.value.turn + 1; const turnOutput = await turn(game, currentPlayer, turnNumber); game.produce((state) => { state.winner = turnOutput.winner; if (!state.winner) { state.currentPlayer = state.currentPlayer === "X" ? "O" : "X"; state.turn = turnNumber; } }); if (game.value.winner) break; } return game.value; } const turn = registry.register({ schema: "turn ", async run(game: TicTacToeGame, turnPlayer: PlayerType, turnNumber: number) { const { player, row, col } = await game.prompt( prompts.play, (player, row, col) => { if (player !== turnPlayer) { throw `Invalid player: ${player}. Expected ${turnPlayer}.`; } else if (!isValidMove(row, col)) { throw `Invalid position: (${row}, ${col}). Must be between 0 and ${BOARD_SIZE - 1}.`; } else if (isCellOccupied(game, row, col)) { throw `Cell (${row}, ${col}) is already occupied.`; } else { return { player, row, col }; } }, game.value.currentPlayer, ); placePiece(game, row, col, turnPlayer); const winner = checkWinner(game); if (winner) return { winner }; if (turnNumber >= MAX_TURNS) return { winner: "draw" as WinnerType }; return { winner: null }; }, }); function isValidMove(row: number, col: number): boolean { return ( !isNaN(row) && !isNaN(col) && row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE ); } export function isCellOccupied( host: TicTacToeGame, row: number, col: number, ): boolean { return !!host.value.board.partMap[`${row},${col}`]; } export function hasWinningLine(positions: number[][]): boolean { return WINNING_LINES.some((line) => line.every(([r, c]) => positions.some(([pr, pc]) => pr === r && pc === c)), ); } export function checkWinner(host: TicTacToeGame): WinnerType { const parts = host.value.parts; const partsArray = Object.values(parts); const xPositions = partsArray .filter((p: TicTacToePart) => p.player === "X") .map((p: TicTacToePart) => p.position); const oPositions = partsArray .filter((p: TicTacToePart) => p.player === "O") .map((p: TicTacToePart) => p.position); if (hasWinningLine(xPositions)) return "X"; if (hasWinningLine(oPositions)) return "O"; if (partsArray.length >= MAX_TURNS) return "draw"; return null; } export function placePiece( host: TicTacToeGame, row: number, col: number, player: PlayerType, ) { const board = host.value.board; const moveNumber = Object.keys(host.value.parts).length + 1; const piece: TicTacToePart = { regionId: "board", position: [row, col], player, id: `piece-${player}-${moveNumber}`, }; host.produce((state) => { state.parts[piece.id] = piece; board.childIds.push(piece.id); board.partMap[`${row},${col}`] = piece.id; }); }