241 lines
8.1 KiB
TypeScript
241 lines
8.1 KiB
TypeScript
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`);
|
|
}
|
|
|
|
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 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[] = [];
|
|
|
|
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 boopCommand = registry.register('boop <row:number> <col:number> <type>', boop);
|
|
|
|
/**
|
|
* 检查是否有玩家获胜(三个猫连线)
|
|
*/
|
|
async function checkWin(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 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;
|
|
|
|
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 checkGraduatesCommand = registry.register('check-graduates', checkGraduates);
|
|
|
|
async function setup(game: BoopGame) {
|
|
while (true) {
|
|
const currentPlayer = game.value.currentPlayer;
|
|
const turnOutput = await turnCommand(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;
|
|
}
|
|
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;
|
|
}
|
|
);
|
|
|
|
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]);
|
|
});
|
|
}
|
|
|
|
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) return { winner: winner };
|
|
|
|
await checkGraduatesCommand(game);
|
|
await checkFullBoard(game, turnPlayer);
|
|
return { winner: null };
|
|
}
|
|
const turnCommand = registry.register('turn <player>', turn); |