import { GameContextInstance } from '../core/context'; import type { RuleEngineHost, RuleContext } from '../core/rule'; import { createRule, type InvokeYield, type SchemaYield } from '../core/rule'; import type { Command } from '../utils/command'; import type { Part } from '../core/part'; import type { Region } from '../core/region'; import type { Context } from '../core/context'; 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; }; type TicTacToeHost = RuleEngineHost & { pushContext: (context: Context) => any; latestContext: (type: string) => { value: T } | undefined; regions: { add: (...entities: any[]) => void; get: (id: string) => { value: { children: any[] } } }; parts: { add: (...entities: any[]) => void; get: (id: string) => any; collection: { value: Record } }; }; function getBoardRegion(host: TicTacToeHost) { return host.regions.get('board'); } function isCellOccupied(host: TicTacToeHost, 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: TicTacToeHost): '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: TicTacToeHost, 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)); } const playSchema: SchemaYield = { type: 'schema', value: 'play ' }; export function createSetupRule() { return createRule('start', function*(this: TicTacToeHost) { this.pushContext({ type: 'tic-tac-toe', currentPlayer: 'X', winner: null, moveCount: 0, } as TicTacToeState); this.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 yieldValue: InvokeYield = { type: 'invoke', rule: 'turn', command: { name: 'turn', params: [currentPlayer], flags: {}, options: {} } as Command, }; const ctx = yield yieldValue; turnResult = (ctx as RuleContext).resolution; if (turnResult?.winner) break; currentPlayer = currentPlayer === 'X' ? 'O' : 'X'; const state = this.latestContext('tic-tac-toe')!; state.value.currentPlayer = currentPlayer; } const state = this.latestContext('tic-tac-toe')!; state.value.winner = turnResult?.winner ?? null; return { winner: state.value.winner }; }); } export function createTurnRule() { return createRule('turn ', function*(this: TicTacToeHost, cmd) { while (true) { const received = yield playSchema; if ('resolution' in received) continue; const playCmd = received as Command; if (playCmd.name !== 'play') continue; const row = playCmd.params[1] as number; const col = playCmd.params[2] as number; if (isNaN(row) || isNaN(col) || row < 0 || row > 2 || col < 0 || col > 2) continue; if (isCellOccupied(this, row, col)) continue; const state = this.latestContext('tic-tac-toe')!; if (state.value.winner) continue; placePiece(this, row, col, state.value.moveCount); state.value.moveCount++; const winner = checkWinner(this); if (winner) return { winner }; if (state.value.moveCount >= 9) return { winner: 'draw' as const }; } }); } export function registerTicTacToeRules(game: GameContextInstance) { game.registerRule('start', createSetupRule()); game.registerRule('turn', createTurnRule()); } export function startTicTacToe(game: GameContextInstance) { game.dispatchCommand('start'); }