boardgame-phaser/packages/boop-game/src/game/commands.ts

258 lines
8.8 KiB
TypeScript
Raw Normal View History

import {
BOARD_SIZE,
BoopState,
2026-04-04 23:25:43 +08:00
BoopPart,
PieceType,
PlayerType,
WinnerType,
WIN_LENGTH,
2026-04-04 23:25:43 +08:00
MAX_PIECES_PER_PLAYER,
BoopGame,
} from "./data";
2026-04-04 23:25:43 +08:00
import {createGameCommandRegistry, Command, moveToRegion} from "boardgame-core";
import {
findPartAtPosition,
findPartInRegion,
getLineCandidates,
getNeighborPositions,
isCellOccupied,
isInBounds
} from "./utils";
export const registry = createGameCommandRegistry<BoopState>();
/**
*
*/
2026-04-06 11:22:33 +08:00
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) {
2026-04-05 00:30:21 +08:00
throw new Error(`${player} 的 supply 中没有可用的 ${type}`);
}
const partId = part.id;
2026-04-04 23:25:43 +08:00
await game.produceAsync((state: BoopState) => {
// 将棋子从supply移动到棋盘
const part = state.pieces[partId];
moveToRegion(part, state.regions[player], state.regions.board, [row, col]);
});
return { row, col, player, type, partId };
}
2026-04-06 11:22:33 +08:00
const place = registry.register( 'place <row:number> <col:number> <player> <type>', handlePlace);
/**
* boop -
*/
2026-04-06 11:22:33 +08:00
async function handleBoop(game: BoopGame, row: number, col: number, type: PieceType) {
const booped: string[] = [];
2026-04-05 09:45:01 +08:00
const toRemove = new Set<string>();
2026-04-04 23:25:43 +08:00
await game.produceAsync((state: BoopState) => {
// 按照远离放置位置的方向推动
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
2026-04-05 09:45:01 +08:00
toRemove.add(part.id);
booped.push(part.id);
2026-04-05 09:45:01 +08:00
moveToRegion(part, state.regions.board, state.regions.board, [newRow, newCol]);
} else if (!isCellOccupied(state, newRow, newCol)) {
// 新位置为空,移动过去
booped.push(part.id);
moveToRegion(part, state.regions.board, state.regions.board, [newRow, newCol]);
}
// 如果新位置被占用,则不移动(两个棋子都保持原位)
}
});
2026-04-05 09:45:01 +08:00
await game.produceAsync((state: BoopState) => {
// 移除被吃掉的棋子
for (const partId of toRemove) {
const part = state.pieces[partId];
moveToRegion(part, state.regions.board, state.regions[part.player]);
}
});
return { booped };
}
2026-04-06 11:22:33 +08:00
const boop = registry.register('boop <row:number> <col:number> <type>', handleBoop);
/**
* (线)
*/
2026-04-06 11:22:33 +08:00
async function handleCheckWin(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;
}
2026-04-06 11:22:33 +08:00
const checkWin = registry.register('check-win', handleCheckWin);
/**
* (线)
*/
2026-04-06 11:22:33 +08:00
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);
}
}
2026-04-04 23:25:43 +08:00
await game.produceAsync((state: BoopState) => {
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]);
}
});
}
2026-04-06 11:22:33 +08:00
const checkGraduates = registry.register('check-graduates', handleCheckGraduates);
2026-04-06 11:22:33 +08:00
export async function start(game: BoopGame) {
while (true) {
const currentPlayer = game.value.currentPlayer;
2026-04-06 11:22:33 +08:00
const { winner } = await turn(game, currentPlayer);
2026-04-04 23:25:43 +08:00
await game.produceAsync((state: BoopState) => {
2026-04-05 00:01:26 +08:00
state.winner = winner;
if (!state.winner) {
state.currentPlayer = state.currentPlayer === 'white' ? 'black' : 'white';
}
});
if (game.value.winner) break;
}
return game.value;
}
2026-04-06 11:22:33 +08:00
async function handleCheckFullBoard(game: BoopGame, turnPlayer: PlayerType){
// 检查8-piece规则: 如果玩家所有8个棋子都在棋盘上且没有获胜,强制升级一个小猫
const playerPieces = Object.values(game.value.pieces).filter(
2026-04-04 23:25:43 +08:00
(p: BoopPart) => p.player === turnPlayer && p.regionId === 'board'
);
if(playerPieces.length < MAX_PIECES_PER_PLAYER || game.value.winner !== null){
return;
}
const partId = await game.prompt(
2026-04-04 23:25:43 +08:00
'play <player> <row:number> <col:number> [type:string]',
2026-04-06 11:22:33 +08:00
(player: PlayerType, row: number, col: number, type?: PieceType) => {
if (player !== turnPlayer) {
2026-04-05 00:30:21 +08:00
throw `无效的玩家: ${player},期望的是 ${turnPlayer}`;
}
if (!isInBounds(row, col)) {
2026-04-05 00:30:21 +08:00
throw `无效的位置: (${row}, ${col}),必须在 0 到 ${BOARD_SIZE - 1} 之间。`;
}
2026-04-04 23:25:43 +08:00
const part = findPartAtPosition(game, row, col);
if (!part || part.player !== turnPlayer) {
2026-04-05 00:30:21 +08:00
throw `(${row}, ${col}) 位置没有 ${player} 的棋子。`;
}
2026-04-04 23:25:43 +08:00
return part.id;
}
);
2026-04-04 23:25:43 +08:00
await game.produceAsync((state: BoopState) => {
const part = state.pieces[partId];
moveToRegion(part, state.regions.board, null);
const cat = findPartInRegion(state, '', 'cat');
moveToRegion(cat || part, null, state.regions[turnPlayer]);
});
}
2026-04-06 11:22:33 +08:00
const checkFullBoard = registry.register('check-full-board', handleCheckFullBoard);
2026-04-06 11:22:33 +08:00
async function handleTurn(game: BoopGame, turnPlayer: PlayerType) {
const {row, col, type} = await game.prompt(
'play <player> <row:number> <col:number> [type:string]',
2026-04-06 11:22:33 +08:00
(player: PlayerType, row: number, col: number, type?: PieceType) => {
const pieceType = type === 'cat' ? 'cat' : 'kitten';
if (player !== turnPlayer) {
2026-04-05 00:30:21 +08:00
throw `无效的玩家: ${player},期望的是 ${turnPlayer}`;
}
if (!isInBounds(row, col)) {
2026-04-05 00:30:21 +08:00
throw `无效的位置: (${row}, ${col}),必须在 0 到 ${BOARD_SIZE - 1} 之间。`;
}
if (isCellOccupied(game, row, col)) {
2026-04-05 00:30:21 +08:00
throw `单元格 (${row}, ${col}) 已被占用。`;
}
const found = findPartInRegion(game, player, pieceType);
if (!found) {
2026-04-05 00:30:21 +08:00
throw `${player} 的 supply 中没有 ${pieceType === 'cat' ? '大猫' : '小猫'} 了。`;
}
return {player, row,col,type};
},
game.value.currentPlayer
);
const pieceType = type === 'cat' ? 'cat' : 'kitten';
2026-04-06 11:22:33 +08:00
await place(game, row, col, turnPlayer, pieceType);
await boop(game, row, col, pieceType);
const winner = await checkWin(game);
2026-04-05 00:01:26 +08:00
if(winner) return { winner: winner as WinnerType };
2026-04-06 11:22:33 +08:00
await checkGraduates(game);
await handleCheckFullBoard(game, turnPlayer);
return { winner: null };
}
2026-04-06 11:22:33 +08:00
const turn = registry.register('turn <player>', handleTurn);
export const prompts = {
2026-04-05 00:24:06 +08:00
play: (player: PlayerType, row: number, col: number, type?: PieceType) => {
if (type) {
return `play ${player} ${row} ${col} ${type}`;
}
return `play ${player} ${row} ${col}`;
},
2026-04-04 23:25:43 +08:00
};