import { createGameCommandRegistry, Part, MutableSignal, createRegion, createPart, isCellOccupied as isCellOccupiedUtil, getPartAtPosition, } from 'boardgame-core'; const BOARD_SIZE = 6; const MAX_PIECES_PER_PLAYER = 8; const WIN_LENGTH = 3; export type PlayerType = 'white' | 'black'; export type PieceType = 'kitten' | 'cat'; export type WinnerType = PlayerType | 'draw' | null; export type BoopPart = Part<{ player: PlayerType; pieceType: PieceType }>; type PieceSupply = { supply: number; placed: number }; type Player = { id: PlayerType; kitten: PieceSupply; cat: PieceSupply; }; type PlayerData = Record; export function createInitialState() { return { board: createRegion('board', [ { name: 'x', min: 0, max: BOARD_SIZE - 1 }, { name: 'y', min: 0, max: BOARD_SIZE - 1 }, ]), pieces: {} as Record, currentPlayer: 'white' as PlayerType, winner: null as WinnerType, players: { white: createPlayer('white'), black: createPlayer('black'), }, }; } function createPlayer(id: PlayerType): Player { return { id, kitten: { supply: MAX_PIECES_PER_PLAYER, placed: 0 }, cat: { supply: 0, placed: 0 }, }; } export type BoopState = ReturnType; const registration = createGameCommandRegistry(); export const registry = registration.registry; export function getPlayer(host: MutableSignal, player: PlayerType): Player { return host.value.players[player]; } export function decrementSupply(player: Player, pieceType: PieceType) { player[pieceType].supply--; player[pieceType].placed++; } export function incrementSupply(player: Player, pieceType: PieceType, count?: number) { player[pieceType].supply += count ?? 1; } 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 playCmd = await this.prompt( 'play [type:string]', (command) => { const [player, row, col, type] = command.params as [PlayerType, number, number, PieceType?]; const pieceType = type === 'cat' ? 'cat' : 'kitten'; if (player !== turnPlayer) { return `Invalid player: ${player}. Expected ${turnPlayer}.`; } if (!isValidMove(row, col)) { return `Invalid position: (${row}, ${col}). Must be between 0 and ${BOARD_SIZE - 1}.`; } if (isCellOccupied(this.context, row, col)) { return `Cell (${row}, ${col}) is already occupied.`; } const playerData = getPlayer(this.context, player); const supply = playerData[pieceType].supply; if (supply <= 0) { return `No ${pieceType}s left in ${player}'s supply.`; } return null; }, this.context.value.currentPlayer ); const [player, row, col, type] = playCmd.params as [PlayerType, number, number, PieceType?]; const pieceType = type === 'cat' ? 'cat' : 'kitten'; placePiece(this.context, row, col, turnPlayer, pieceType); applyBoops(this.context, row, col, pieceType); const graduatedLines = checkGraduation(this.context, turnPlayer); if (graduatedLines.length > 0) { processGraduation(this.context, turnPlayer, graduatedLines); } if (countPiecesOnBoard(this.context, turnPlayer) >= MAX_PIECES_PER_PLAYER) { const pieces = this.context.value.pieces; const availableKittens = Object.values(pieces).filter( p => p.player === turnPlayer && p.pieceType === 'kitten' ); if (availableKittens.length > 0) { const graduateCmd = await this.prompt( 'graduate ', (command) => { const [row, col] = command.params as [number, number]; const posKey = `${row},${col}`; const part = availableKittens.find(p => `${p.position[0]},${p.position[1]}` === posKey); if (!part) return `No kitten at (${row}, ${col}).`; return null; }, this.context.value.currentPlayer ); const [row, col] = graduateCmd.params as [number, number]; const part = availableKittens.find(p => p.position[0] === row && p.position[1] === col)!; removePieceFromBoard(this.context, part); const playerData = getPlayer(this.context, turnPlayer); incrementSupply(playerData, 'cat', 1); } } const winner = checkWinner(this.context); if (winner) return { winner }; return { winner: null }; }); 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: MutableSignal) { return host.value.board; } export function isCellOccupied(host: MutableSignal, row: number, col: number): boolean { return isCellOccupiedUtil(host.value.pieces, 'board', [row, col]); } export function getPartAt(host: MutableSignal, row: number, col: number): BoopPart | null { return getPartAtPosition(host.value.pieces, 'board', [row, col]) || null; } export function placePiece(host: MutableSignal, row: number, col: number, player: PlayerType, pieceType: PieceType) { const board = getBoardRegion(host); const playerData = getPlayer(host, player); const count = playerData[pieceType].placed + 1; const piece = createPart<{ player: PlayerType; pieceType: PieceType }>( { regionId: 'board', position: [row, col], player, pieceType }, `${player}-${pieceType}-${count}` ); host.produce(s => { s.pieces[piece.id] = piece; board.childIds.push(piece.id); board.partMap[`${row},${col}`] = piece.id; }); decrementSupply(playerData, pieceType); } export function applyBoops(host: MutableSignal, placedRow: number, placedCol: number, placedType: PieceType) { const pieces = host.value.pieces; const piecesArray = Object.values(pieces); const piecesToBoop: { part: BoopPart; dr: number; dc: number }[] = []; for (const part of piecesArray) { const [r, c] = part.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.pieceType === 'cat'; if (booperIsKitten && targetIsCat) continue; piecesToBoop.push({ part, dr, dc }); } } host.produce(state => { const board = state.board; const currentPieces = state.pieces; for (const { part, dr, dc } of piecesToBoop) { const [r, c] = part.position; const newRow = r + dr; const newCol = c + dc; if (newRow < 0 || newRow >= BOARD_SIZE || newCol < 0 || newCol >= BOARD_SIZE) { const pt = part.pieceType; const pl = part.player; const playerData = state.players[pl]; // Remove piece from board board.childIds = board.childIds.filter(id => id !== part.id); delete board.partMap[part.position.join(',')]; delete currentPieces[part.id]; playerData[pt].placed--; playerData[pt].supply++; continue; } // Check if target cell is occupied const targetPosKey = `${newRow},${newCol}`; if (board.partMap[targetPosKey]) continue; // Move piece to new position delete board.partMap[part.position.join(',')]; part.position = [newRow, newCol]; board.partMap[targetPosKey] = part.id; } }); } export function removePieceFromBoard(host: MutableSignal, part: BoopPart) { host.produce(state => { const board = state.board; const playerData = state.players[part.player]; board.childIds = board.childIds.filter(id => id !== part.id); delete board.partMap[part.position.join(',')]; delete state.pieces[part.id]; playerData[part.pieceType].placed--; }); } const DIRECTIONS: [number, number][] = [ [0, 1], [1, 0], [1, 1], [1, -1], ]; export function* linesThrough(r: number, c: number): Generator { for (const [dr, dc] of DIRECTIONS) { const minStart = -(WIN_LENGTH - 1); for (let offset = minStart; offset <= 0; offset++) { const startR = r + offset * dr; const startC = c + offset * dc; const endR = startR + (WIN_LENGTH - 1) * dr; const endC = startC + (WIN_LENGTH - 1) * dc; if (startR < 0 || startR >= BOARD_SIZE || startC < 0 || startC >= BOARD_SIZE) continue; if (endR < 0 || endR >= BOARD_SIZE || endC < 0 || endC >= BOARD_SIZE) continue; const line: number[][] = []; for (let i = 0; i < WIN_LENGTH; i++) { line.push([startR + i * dr, startC + i * dc]); } yield line; } } } export function* allLines(): Generator { const seen = new Set(); for (let r = 0; r < BOARD_SIZE; r++) { for (let c = 0; c < BOARD_SIZE; c++) { for (const line of linesThrough(r, c)) { const key = line.map(p => p.join(',')).join(';'); if (!seen.has(key)) { seen.add(key); yield line; } } } } } export function hasWinningLine(positions: number[][]): boolean { const posSet = new Set(positions.map(p => `${p[0]},${p[1]}`)); for (const line of allLines()) { if (line.every(([lr, lc]) => posSet.has(`${lr},${lc}`))) return true; } return false; } export function checkGraduation(host: MutableSignal, player: PlayerType): number[][][] { const pieces = host.value.pieces; const piecesArray = Object.values(pieces); const posSet = new Set(); for (const part of piecesArray) { if (part.player === player && part.pieceType === 'kitten') { posSet.add(`${part.position[0]},${part.position[1]}`); } } const winningLines: number[][][] = []; for (const line of allLines()) { if (line.every(([lr, lc]) => posSet.has(`${lr},${lc}`))) { winningLines.push(line); } } return winningLines; } export function processGraduation(host: MutableSignal, 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 pieces = host.value.pieces; const partsToRemove = Object.values(pieces).filter( p => p.player === player && p.pieceType === 'kitten' && allPositions.has(`${p.position[0]},${p.position[1]}`) ); for (const part of partsToRemove) { removePieceFromBoard(host, part); } const count = partsToRemove.length; const playerData = getPlayer(host, player); incrementSupply(playerData, 'cat', count); } export function countPiecesOnBoard(host: MutableSignal, player: PlayerType): number { const pieces = host.value.pieces; return Object.values(pieces).filter(p => p.player === player).length; } export function checkWinner(host: MutableSignal): WinnerType { const pieces = host.value.pieces; const piecesArray = Object.values(pieces); for (const player of ['white', 'black'] as PlayerType[]) { const positions = piecesArray .filter(p => p.player === player && p.pieceType === 'cat') .map(p => p.position); if (hasWinningLine(positions)) return player; } return null; } // 命令构建器 export const commands = { play: (player: PlayerType, row: number, col: number, type?: PieceType) => `play ${player} ${row} ${col}${type ? ` ${type}` : ''}`, turn: (player: PlayerType) => `turn ${player}`, graduate: (row: number, col: number) => `graduate ${row} ${col}`, } as const; // 导出游戏模块 export const gameModule = { createInitialState, registry, commands, };