import { GameContextInstance } from '../core/context'; import type { GameContextLike, RuleContext } from '../core/rule'; import { createRule, type InvokeYield, type RuleYield } 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; }; function getBoardRegion(game: GameContextLike) { return game.regions.get('board'); } function isCellOccupied(game: GameContextLike, row: number, col: number): boolean { const board = getBoardRegion(game); return board.value.children.some( (child: { value: { position: number[] } }) => child.value.position[0] === row && child.value.position[1] === col ); } function checkWinner(game: GameContextLike): 'X' | 'O' | 'draw' | null { const parts = Object.values(game.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(game: GameContextLike, row: number, col: number, moveCount: number) { const board = getBoardRegion(game); const piece: Part = { id: `piece-${moveCount}`, sides: 1, side: 0, region: board, position: [row, col], }; game.parts.add(piece); board.value.children.push(game.parts.get(piece.id)); } const playSchema = 'play '; export function createSetupRule() { return createRule('start', function*() { 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 as RuleYield; 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*(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'); }