boardgame-core/src/samples/boop/commands.ts

243 lines
8.2 KiB
TypeScript
Raw Normal View History

2026-04-04 21:53:37 +08:00
import {
BOARD_SIZE,
BoopState,
PieceType,
PlayerType,
WinnerType,
WIN_LENGTH,
MAX_PIECES_PER_PLAYER, BoopGame
} from "./data";
import {createGameCommandRegistry} from "@/core/game";
import {moveToRegion} from "@/core/region";
import {
findPartAtPosition,
findPartInRegion,
getLineCandidates,
getNeighborPositions,
isCellOccupied,
isInBounds
} from "./utils";
export const registry = createGameCommandRegistry<BoopState>();
/**
*
*/
async function place(game: BoopGame, row: number, col: number, player: PlayerType, type: PieceType) {
const value = game.value;
// 从玩家supply中找到对应类型的棋子
const part = findPartInRegion(game, player, type);
if (!part) {
throw new Error(`No ${type} available in ${player}'s supply`);
}
2026-04-04 22:11:02 +08:00
const partId = part.id;
2026-04-04 21:53:37 +08:00
game.produce(state => {
// 将棋子从supply移动到棋盘
2026-04-04 22:11:02 +08:00
const part = state.pieces[partId];
2026-04-04 21:53:37 +08:00
moveToRegion(part, state.regions[player], state.regions.board, [row, col]);
});
2026-04-04 22:11:02 +08:00
return { row, col, player, type, partId };
2026-04-04 21:53:37 +08:00
}
const placeCommand = registry.register( 'place <row:number> <col:number> <player> <type>', place);
/**
* boop -
*/
async function boop(game: BoopGame, row: number, col: number, type: PieceType) {
const booped: string[] = [];
game.produce(state => {
// 按照远离放置位置的方向推动
for (const [dr, dc] of getNeighborPositions()) {
const nr = row + dr;
const nc = col + dc;
2026-04-04 22:11:02 +08:00
2026-04-04 21:53:37 +08:00
if (!isInBounds(nr, nc)) continue;
2026-04-04 22:11:02 +08:00
// 从 state 中查找,而不是 game
const part = findPartAtPosition(state, nr, nc);
2026-04-04 21:53:37 +08:00
if (!part) continue;
2026-04-04 22:11:02 +08:00
2026-04-04 21:53:37 +08:00
// 小猫不能推动猫
if (type === 'kitten' && part.type === 'cat') continue;
// 计算推动后的位置
const newRow = nr + dr;
const newCol = nc + dc;
// 检查新位置是否为空或在棋盘外
if (!isInBounds(newRow, newCol)) {
// 棋子被推出棋盘,返回玩家supply
booped.push(part.id);
moveToRegion(part, state.regions.board, state.regions[part.player]);
2026-04-04 22:11:02 +08:00
} else if (!isCellOccupied(state, newRow, newCol)) {
2026-04-04 21:53:37 +08:00
// 新位置为空,移动过去
booped.push(part.id);
moveToRegion(part, state.regions.board, state.regions.board, [newRow, newCol]);
}
// 如果新位置被占用,则不移动(两个棋子都保持原位)
}
});
return { booped };
}
const boopCommand = registry.register('boop <row:number> <col:number> <type>', boop);
/**
* (线)
*/
async function checkWin(game: BoopGame) {
for(const line of getLineCandidates()){
let whites = 0;
let blacks = 0;
for(const [row, col] of line){
const part = findPartAtPosition(game, row, col);
if(part?.type !== 'cat') continue;
if (part.player === 'white') whites++;
else blacks++;
}
if(whites >= WIN_LENGTH) {
return 'white';
}
if(blacks >= WIN_LENGTH) {
return 'black';
}
}
return null;
}
const checkWinCommand = registry.register('check-win', checkWin);
/**
* (线)
*/
async function checkGraduates(game: BoopGame){
const toUpgrade = new Set<string>();
for(const line of getLineCandidates()){
let whites = 0;
let blacks = 0;
for(const [row, col] of line){
const part = findPartAtPosition(game, row, col);
if (part?.player === 'white') whites++;
else if(part?.player === 'black') blacks++;
}
const player = whites >= WIN_LENGTH ? 'white' : blacks >= WIN_LENGTH ? 'black' : null;
if(!player) continue;
2026-04-04 22:11:02 +08:00
2026-04-04 21:53:37 +08:00
for(const [row, col] of line){
const part = findPartAtPosition(game, row, col);
part && toUpgrade.add(part.id);
}
}
2026-04-04 22:11:02 +08:00
2026-04-04 21:53:37 +08:00
game.produce(state => {
2026-04-04 22:11:02 +08:00
// 预先收集所有可用的猫(在盒子里的)
2026-04-04 21:53:37 +08:00
for(const partId of toUpgrade){
const part = state.pieces[partId];
const [row, col] = part.position;
const player = part.player;
moveToRegion(part, state.regions.board, null);
2026-04-04 22:11:02 +08:00
// 使用下一个可用的猫
const newPart = findPartInRegion(state, '', 'kitten', player);
2026-04-04 21:53:37 +08:00
moveToRegion(newPart || part, null, state.regions[player], [row, col]);
}
});
}
const checkGraduatesCommand = registry.register('check-graduates', checkGraduates);
async function setup(game: BoopGame) {
while (true) {
const currentPlayer = game.value.currentPlayer;
const turnOutput = await turnCommand(game, currentPlayer);
if (!turnOutput.success) throw new Error(turnOutput.error);
game.produce(state => {
state.winner = turnOutput.result.winner;
if (!state.winner) {
state.currentPlayer = state.currentPlayer === 'white' ? 'black' : 'white';
}
});
if (game.value.winner) break;
}
return game.value;
}
registry.register('setup', setup);
async function checkFullBoard(game: BoopGame, turnPlayer: PlayerType){
// 检查8-piece规则: 如果玩家所有8个棋子都在棋盘上且没有获胜,强制升级一个小猫
const playerPieces = Object.values(game.value.pieces).filter(
p => p.player === turnPlayer && p.regionId === 'board'
);
if(playerPieces.length < MAX_PIECES_PER_PLAYER || game.value.winner !== null){
return;
}
const partId = await game.prompt(
'choose <player> <row:number> <col:number>',
(command) => {
const [player, row, col] = command.params as [PlayerType, number, number];
if (player !== turnPlayer) {
throw `Invalid player: ${player}. Expected ${turnPlayer}.`;
}
if (!isInBounds(row, col)) {
throw `Invalid position: (${row}, ${col}). Must be between 0 and ${BOARD_SIZE - 1}.`;
}
const part = findPartAtPosition(game, row, col);
if (!part || part.player !== turnPlayer) {
throw `No ${player} piece at (${row}, ${col}).`;
}
return part.id;
}
);
game.produce(state => {
const part = state.pieces[partId];
moveToRegion(part, state.regions.board, null);
const cat = findPartInRegion(state, '', 'cat');
moveToRegion(cat || part, null, state.regions[turnPlayer]);
});
}
async function turn(game: BoopGame, turnPlayer: PlayerType) {
const {row, col, type} = await game.prompt(
'play <player> <row:number> <col:number> [type:string]',
(command) => {
const [player, row, col, type] = command.params as [PlayerType, number, number, PieceType?];
const pieceType = type === 'cat' ? 'cat' : 'kitten';
if (player !== turnPlayer) {
throw `Invalid player: ${player}. Expected ${turnPlayer}.`;
}
if (!isInBounds(row, col)) {
throw `Invalid position: (${row}, ${col}). Must be between 0 and ${BOARD_SIZE - 1}.`;
}
if (isCellOccupied(game, row, col)) {
throw `Cell (${row}, ${col}) is already occupied.`;
}
const found = findPartInRegion(game, player, pieceType);
if (!found) {
throw `No ${pieceType}s left in ${player}'s supply.`;
}
return {player, row,col,type};
},
game.value.currentPlayer
);
const pieceType = type === 'cat' ? 'cat' : 'kitten';
await placeCommand(game, row, col, turnPlayer, pieceType);
await boopCommand(game, row, col, pieceType);
const winner = await checkWinCommand(game);
if(winner.success) return { winner: winner.result as WinnerType };
await checkGraduatesCommand(game);
return { winner: null };
}
const turnCommand = registry.register('turn <player>', turn);