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

259 lines
8.1 KiB
TypeScript

import {
BOARD_SIZE,
BoopState,
PieceType,
PlayerType,
WinnerType,
WIN_LENGTH,
MAX_PIECES_PER_PLAYER,
BoopGame,
prompts
} 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 handlePlace(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`);
}
const partId = part.id;
await game.produceAsync(state => {
// 将棋子从supply移动到棋盘
const part = state.pieces[partId];
moveToRegion(part, state.regions[player], state.regions.board, [row, col]);
});
return { row, col, player, type, partId };
}
const place = registry.register({
schema: 'place <row:number> <col:number> <player> <type>',
run: handlePlace
});
/**
* 执行boop - 推动周围棋子
*/
async function handleBoop(game: BoopGame, row: number, col: number, type: PieceType) {
const booped: string[] = [];
await game.produceAsync(state => {
// 按照远离放置位置的方向推动
for (const [dr, dc] of getNeighborPositions()) {
const nr = row + dr;
const nc = col + dc;
if (!isInBounds(nr, nc)) continue;
// 从 state 中查找,而不是 game
const part = findPartAtPosition(state, nr, nc);
if (!part) continue;
// 小猫不能推动猫
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]);
} else if (!isCellOccupied(state, newRow, newCol)) {
// 新位置为空,移动过去
booped.push(part.id);
moveToRegion(part, state.regions.board, state.regions.board, [newRow, newCol]);
}
// 如果新位置被占用,则不移动(两个棋子都保持原位)
}
});
return { booped };
}
const boop = registry.register({
schema: 'boop <row:number> <col:number> <type>',
run: handleBoop
});
/**
* 检查是否有玩家获胜(三个猫连线)
*/
async function handleCheckWin(game: BoopGame): Promise<WinnerType | null> {
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 checkWin = registry.register({
schema: 'check-win',
run: handleCheckWin
});
/**
* 检查并执行小猫升级(三个小猫连线变成猫)
*/
async function handleCheckGraduates(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;
for(const [row, col] of line){
const part = findPartAtPosition(game, row, col);
part && toUpgrade.add(part.id);
}
}
await game.produceAsync(state => {
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);
const newPart = findPartInRegion(state, '', 'cat', player);
moveToRegion(newPart || part, null, state.regions[player], [row, col]);
}
});
}
const checkGraduates = registry.register({
schema: 'check-graduates',
run: handleCheckGraduates
});
export async function start(game: BoopGame) {
while (true) {
const currentPlayer = game.value.currentPlayer;
const turnOutput = await turn(game, currentPlayer);
await game.produceAsync(state => {
state.winner = turnOutput.winner;
if (!state.winner) {
state.currentPlayer = state.currentPlayer === 'white' ? 'black' : 'white';
}
});
if (game.value.winner) break;
}
return game.value;
}
async function handleCheckFullBoard(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(
prompts.choose,
(player, row, col) => {
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;
}
);
await game.produceAsync(state => {
const part = state.pieces[partId];
moveToRegion(part, state.regions.board, null);
const cat = findPartInRegion(state, '', 'cat');
moveToRegion(cat || part, null, state.regions[turnPlayer]);
});
}
const checkFullBoard = registry.register({
schema: 'check-full-board <player:string>',
run: handleCheckFullBoard
});
async function handleTurn(game: BoopGame, turnPlayer: PlayerType) {
const {row, col, type} = await game.prompt(
prompts.play,
(player, row, col, type) => {
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 place(game, row, col, turnPlayer, pieceType);
await boop(game, row, col, pieceType);
const winner = await checkWin(game);
if(winner) return { winner: winner };
await checkGraduates(game);
await checkFullBoard(game, turnPlayer);
return { winner: null };
}
const turn = registry.register({
schema: 'turn <player:string>',
run: handleTurn
});