import {createGameCommandRegistry, Part, Entity, entity, RegionEntity} from '@/index'; const BOARD_SIZE = 6; const MAX_PIECES_PER_PLAYER = 8; const WIN_LENGTH = 3; const DIRECTIONS = [ [-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1], ]; export type PlayerType = 'white' | 'black'; export type PieceType = 'kitten' | 'cat'; export type WinnerType = PlayerType | 'draw' | null; type BoopPart = Part & { player: PlayerType; pieceType: PieceType }; export function createInitialState() { return { board: new RegionEntity('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: 'white' as PlayerType, winner: null as WinnerType, whiteKittensInSupply: MAX_PIECES_PER_PLAYER, blackKittensInSupply: MAX_PIECES_PER_PLAYER, whiteCatsInSupply: 0, blackCatsInSupply: 0, }; } export type BoopState = 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 turnOutput = await this.run<{winner: WinnerType}>(`turn ${currentPlayer}`); if (!turnOutput.success) throw new Error(turnOutput.error); context.produce(state => { state.winner = turnOutput.result.winner; if (!state.winner) { state.currentPlayer = state.currentPlayer === 'white' ? 'black' : 'white'; } }); if (context.value.winner) break; } return context.value; }); registration.add('turn ', async function(cmd) { const [turnPlayer] = cmd.params as [PlayerType]; const maxRetries = 50; 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; const state = this.context.value; const kittensInSupply = player === 'white' ? state.whiteKittensInSupply : state.blackKittensInSupply; if (kittensInSupply <= 0) continue; placeKitten(this.context, row, col, turnPlayer); applyBoops(this.context, row, col, 'kitten'); const graduatedRows = checkGraduation(this.context, turnPlayer); if (graduatedRows.length > 0) { processGraduation(this.context, turnPlayer, graduatedRows); } const winner = checkWinner(this.context); if (winner) return { winner }; 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.partsMap.value[`${row},${col}`] !== undefined; } export function getPartAt(host: Entity, row: number, col: number): Entity | null { const board = getBoardRegion(host); return (board.partsMap.value[`${row},${col}`] as Entity | undefined) || null; } export function placeKitten(host: Entity, row: number, col: number, player: PlayerType) { const board = getBoardRegion(host); const moveNumber = host.value.parts.length + 1; const piece: BoopPart = { id: `piece-${player}-${moveNumber}`, region: board, position: [row, col], player, pieceType: 'kitten', }; host.produce(state => { const e = entity(piece.id, piece); state.parts.push(e); if (player === 'white') state.whiteKittensInSupply--; else state.blackKittensInSupply--; board.produce(draft => { draft.children.push(e); }); }); } export function applyBoops(host: Entity, placedRow: number, placedCol: number, placedType: PieceType) { const board = getBoardRegion(host); const partsMap = board.partsMap.value; const piecesToBoop: { part: Entity; dr: number; dc: number }[] = []; for (const key in partsMap) { const part = partsMap[key] as Entity; const [r, c] = part.value.position; if (r === placedRow && c === placedCol) continue; const dr = Math.sign(r - placedRow); const dc = Math.sign(c - placedCol); if (Math.abs(r - placedRow) <= 1 && Math.abs(c - placedCol) <= 1) { const booperIsKitten = placedType === 'kitten'; const targetIsCat = part.value.pieceType === 'cat'; if (booperIsKitten && targetIsCat) continue; piecesToBoop.push({ part, dr, dc }); } } for (const { part, dr, dc } of piecesToBoop) { const [r, c] = part.value.position; const newRow = r + dr; const newCol = c + dc; if (newRow < 0 || newRow >= BOARD_SIZE || newCol < 0 || newCol >= BOARD_SIZE) { removePieceFromBoard(host, part); const player = part.value.player; host.produce(state => { if (player === 'white') state.whiteKittensInSupply++; else state.blackKittensInSupply++; }); continue; } if (isCellOccupied(host, newRow, newCol)) continue; part.produce(p => { p.position = [newRow, newCol]; }); } } export function removePieceFromBoard(host: Entity, part: Entity) { const board = getBoardRegion(host); host.produce(state => { state.parts = state.parts.filter(p => p.id !== part.id); board.produce(draft => { draft.children = draft.children.filter(p => p.id !== part.id); }); }); } export function checkGraduation(host: Entity, player: PlayerType): number[][][] { const parts = host.value.parts.filter(p => p.value.player === player && p.value.pieceType === 'kitten'); const positions = parts.map(p => p.value.position); const winningLines: number[][][] = []; for (let r = 0; r < BOARD_SIZE; r++) { for (let c = 0; c <= BOARD_SIZE - WIN_LENGTH; c++) { const line = []; for (let i = 0; i < WIN_LENGTH; i++) { line.push([r, c + i]); } if (line.every(([lr, lc]) => positions.some(([pr, pc]) => pr === lr && pc === lc))) { winningLines.push(line); } } } for (let c = 0; c < BOARD_SIZE; c++) { for (let r = 0; r <= BOARD_SIZE - WIN_LENGTH; r++) { const line = []; for (let i = 0; i < WIN_LENGTH; i++) { line.push([r + i, c]); } if (line.every(([lr, lc]) => positions.some(([pr, pc]) => pr === lr && pc === lc))) { winningLines.push(line); } } } for (let r = 0; r <= BOARD_SIZE - WIN_LENGTH; r++) { for (let c = 0; c <= BOARD_SIZE - WIN_LENGTH; c++) { const line = []; for (let i = 0; i < WIN_LENGTH; i++) { line.push([r + i, c + i]); } if (line.every(([lr, lc]) => positions.some(([pr, pc]) => pr === lr && pc === lc))) { winningLines.push(line); } } } for (let r = WIN_LENGTH - 1; r < BOARD_SIZE; r++) { for (let c = 0; c <= BOARD_SIZE - WIN_LENGTH; c++) { const line = []; for (let i = 0; i < WIN_LENGTH; i++) { line.push([r - i, c + i]); } if (line.every(([lr, lc]) => positions.some(([pr, pc]) => pr === lr && pc === lc))) { winningLines.push(line); } } } return winningLines; } export function processGraduation(host: Entity, player: PlayerType, lines: number[][][]) { const allPositions = new Set(); for (const line of lines) { for (const [r, c] of line) { allPositions.add(`${r},${c}`); } } const board = getBoardRegion(host); const partsMap = board.partsMap.value; const partsToRemove: Entity[] = []; for (const key in partsMap) { const part = partsMap[key] as Entity; if (part.value.player === player && part.value.pieceType === 'kitten' && allPositions.has(`${part.value.position[0]},${part.value.position[1]}`)) { partsToRemove.push(part); } } for (const part of partsToRemove) { removePieceFromBoard(host, part); } const count = partsToRemove.length; host.produce(state => { const catsInSupply = player === 'white' ? state.whiteCatsInSupply : state.blackCatsInSupply; if (player === 'white') state.whiteCatsInSupply = catsInSupply + count; else state.blackCatsInSupply = catsInSupply + count; }); } export function checkWinner(host: Entity): WinnerType { for (const player of ['white', 'black'] as PlayerType[]) { const parts = host.value.parts.filter(p => p.value.player === player && p.value.pieceType === 'cat'); const positions = parts.map(p => p.value.position); if (hasWinningLine(positions)) return player; } const totalParts = host.value.parts.length; const whiteParts = host.value.parts.filter(p => p.value.player === 'white').length; const blackParts = host.value.parts.filter(p => p.value.player === 'black').length; if (whiteParts >= MAX_PIECES_PER_PLAYER && blackParts >= MAX_PIECES_PER_PLAYER) { return 'draw'; } return null; } export function hasWinningLine(positions: number[][]): boolean { for (let r = 0; r < BOARD_SIZE; r++) { for (let c = 0; c <= BOARD_SIZE - WIN_LENGTH; c++) { const line = []; for (let i = 0; i < WIN_LENGTH; i++) line.push([r, c + i]); if (line.every(([lr, lc]) => positions.some(([pr, pc]) => pr === lr && pc === lc))) return true; } } for (let c = 0; c < BOARD_SIZE; c++) { for (let r = 0; r <= BOARD_SIZE - WIN_LENGTH; r++) { const line = []; for (let i = 0; i < WIN_LENGTH; i++) line.push([r + i, c]); if (line.every(([lr, lc]) => positions.some(([pr, pc]) => pr === lr && pc === lc))) return true; } } for (let r = 0; r <= BOARD_SIZE - WIN_LENGTH; r++) { for (let c = 0; c <= BOARD_SIZE - WIN_LENGTH; c++) { const line = []; for (let i = 0; i < WIN_LENGTH; i++) line.push([r + i, c + i]); if (line.every(([lr, lc]) => positions.some(([pr, pc]) => pr === lr && pc === lc))) return true; } } for (let r = WIN_LENGTH - 1; r < BOARD_SIZE; r++) { for (let c = 0; c <= BOARD_SIZE - WIN_LENGTH; c++) { const line = []; for (let i = 0; i < WIN_LENGTH; i++) line.push([r - i, c + i]); if (line.every(([lr, lc]) => positions.some(([pr, pc]) => pr === lr && pc === lc))) return true; } } return false; }