import { GameContextInstance } from '../core/context'; import type { Command, CommandRunner, CommandRunnerContext } from '../utils/command'; import type { Part } from '../core/part'; import type { Region } from '../core/region'; import type { Context } from '../core/context'; import { parseCommandSchema } from '../utils/command/schema-parse'; export type TicTacToeState = Context & { type: 'tic-tac-toe'; currentPlayer: 'X' | 'O'; winner: 'X' | 'O' | 'draw' | null; moveCount: number; }; type TurnResult = { winner: 'X' | 'O' | 'draw' | null; }; function getBoardRegion(host: GameContextInstance) { return host.regions.get('board'); } function isCellOccupied(host: GameContextInstance, row: number, col: number): boolean { const board = getBoardRegion(host); return board.value.children.some( (child: { value: { position: number[] } }) => child.value.position[0] === row && child.value.position[1] === col ); } function checkWinner(host: GameContextInstance): 'X' | 'O' | 'draw' | null { const parts = Object.values(host.parts.collection.value).map((s: { value: Part }) => s.value); const xPositions = parts.filter((_: Part, i: number) => i % 2 === 0).map((p: Part) => p.position); const oPositions = parts.filter((_: Part, i: number) => i % 2 === 1).map((p: Part) => p.position); if (hasWinningLine(xPositions)) return 'X'; if (hasWinningLine(oPositions)) return 'O'; return null; } function hasWinningLine(positions: number[][]): boolean { const lines = [ [[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]], ]; return lines.some(line => line.every(([r, c]) => positions.some(([pr, pc]) => pr === r && pc === c) ) ); } function placePiece(host: GameContextInstance, row: number, col: number, moveCount: number) { const board = getBoardRegion(host); const piece: Part = { id: `piece-${moveCount}`, sides: 1, side: 0, region: board, position: [row, col], }; host.parts.add(piece); board.value.children.push(host.parts.get(piece.id)); } export function createSetupCommand(): CommandRunner { return { schema: parseCommandSchema('start'), run: async function(this: CommandRunnerContext) { this.context.pushContext({ type: 'tic-tac-toe', currentPlayer: 'X', winner: null, moveCount: 0, } as TicTacToeState); this.context.regions.add({ id: 'board', axes: [ { name: 'x', min: 0, max: 2 }, { name: 'y', min: 0, max: 2 }, ], children: [], } as Region); let currentPlayer: 'X' | 'O' = 'X'; let turnResult: TurnResult | undefined; while (true) { const turnOutput = await this.run(`turn ${currentPlayer}`); if (!turnOutput.success) throw new Error(turnOutput.error); turnResult = turnOutput.result as TurnResult; if (turnResult?.winner) break; currentPlayer = currentPlayer === 'X' ? 'O' : 'X'; const state = this.context.latestContext('tic-tac-toe')!; state.value.currentPlayer = currentPlayer; } const state = this.context.latestContext('tic-tac-toe')!; state.value.winner = turnResult?.winner ?? null; return { winner: state.value.winner }; }, }; } export function createTurnCommand(): CommandRunner { return { schema: parseCommandSchema('turn '), run: async function(this: CommandRunnerContext, cmd: Command) { while (true) { const playCmd = await this.prompt('play '); const row = Number(playCmd.params[1]); const col = Number(playCmd.params[2]); if (isNaN(row) || isNaN(col) || row < 0 || row > 2 || col < 0 || col > 2) continue; if (isCellOccupied(this.context, row, col)) continue; const state = this.context.latestContext('tic-tac-toe')!; if (state.value.winner) continue; placePiece(this.context, row, col, state.value.moveCount); state.value.moveCount++; const winner = checkWinner(this.context); if (winner) return { winner }; if (state.value.moveCount >= 9) return { winner: 'draw' as const }; } }, }; } export function registerTicTacToeCommands(game: GameContextInstance) { game.registerCommand('start', createSetupCommand()); game.registerCommand('turn', createTurnCommand()); } export function startTicTacToe(game: GameContextInstance) { game.dispatchCommand('start'); }