import {createGameCommandRegistry} from '../core/game'; import type { Part } from '../core/part'; import {Entity, entity} from "../utils/entity"; import {Region} from "../core/region"; 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]], ]; type PlayerType = 'X' | 'O'; type WinnerType = PlayerType | 'draw' | null; type TicTacToePart = Part & { player: PlayerType }; export function createInitialState() { return { board: entity('board', { id: 'board', axes: [ { name: 'x', min: 0, max: BOARD_SIZE - 1 }, { name: 'y', min: 0, max: BOARD_SIZE - 1 }, ], children: [], }), parts: [] as Entity[], currentPlayer: 'X' as PlayerType, winner: null as WinnerType, turn: 0, }; } export type TicTacToeState = ReturnType; const registration = createGameCommandRegistry(); export const registry = registration.registry; registration.add('setup', async function() { const {context} = this; while (true) { const currentPlayer = context.value.currentPlayer; const turnNumber = context.value.turn + 1; const turnOutput = await this.run<{winner: WinnerType}>(`turn ${currentPlayer} ${turnNumber}`); if (!turnOutput.success) throw new Error(turnOutput.error); context.produce(state => { state.winner = turnOutput.result.winner; if (!state.winner) { state.currentPlayer = state.currentPlayer === 'X' ? 'O' : 'X'; state.turn = turnNumber; } }); if (context.value.winner) break; } return context.value; }); registration.add('turn ', async function(cmd) { const [turnPlayer, turnNumber] = cmd.params as [PlayerType, number]; const maxRetries = MAX_TURNS * 2; let retries = 0; while (retries < maxRetries) { retries++; const playCmd = await this.prompt('play '); const [player, row, col] = playCmd.params as [PlayerType, number, number]; if (player !== turnPlayer) continue; if (!isValidMove(row, col)) continue; if (isCellOccupied(this.context, row, col)) continue; placePiece(this.context, row, col, turnPlayer); const winner = checkWinner(this.context); if (winner) return { winner }; if (turnNumber >= MAX_TURNS) return { winner: 'draw' as WinnerType }; return { winner: null }; } throw new Error('Too many invalid attempts'); }); function isValidMove(row: number, col: number): boolean { return !isNaN(row) && !isNaN(col) && row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE; } export function getBoardRegion(host: Entity) { return host.value.board; } export function isCellOccupied(host: Entity, row: number, col: number): boolean { const board = getBoardRegion(host); return board.value.children.some( part => part.value.position[0] === row && part.value.position[1] === 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: Entity): WinnerType { const parts = host.value.parts.map((e: Entity) => e.value); const xPositions = parts.filter((p: TicTacToePart) => p.player === 'X').map((p: TicTacToePart) => p.position); const oPositions = parts.filter((p: TicTacToePart) => p.player === 'O').map((p: TicTacToePart) => p.position); if (hasWinningLine(xPositions)) return 'X'; if (hasWinningLine(oPositions)) return 'O'; if (parts.length >= MAX_TURNS) return 'draw'; return null; } export function placePiece(host: Entity, row: number, col: number, player: PlayerType) { const board = getBoardRegion(host); const moveNumber = host.value.parts.length + 1; const piece: TicTacToePart = { id: `piece-${player}-${moveNumber}`, region: board, position: [row, col], player, }; host.produce(state => { const e = entity(piece.id, piece) state.parts.push(e); board.produce(draft => { draft.children.push(e); }); }); }