356 lines
11 KiB
TypeScript
356 lines
11 KiB
TypeScript
|
|
import {
|
|||
|
|
OnitamaGame,
|
|||
|
|
OnitamaState,
|
|||
|
|
PlayerType,
|
|||
|
|
prompts
|
|||
|
|
} from "./types";
|
|||
|
|
import {createGameCommandRegistry} from "@/core/game";
|
|||
|
|
import {moveToRegion} from "@/core/region";
|
|||
|
|
|
|||
|
|
export const registry = createGameCommandRegistry<OnitamaState>();
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查位置是否在棋盘范围内
|
|||
|
|
*/
|
|||
|
|
function isInBounds(x: number, y: number): boolean {
|
|||
|
|
return x >= 0 && x < 5 && y >= 0 && y < 5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查位置是否有棋子
|
|||
|
|
*/
|
|||
|
|
function getPawnAtPosition(state: OnitamaState, x: number, y: number) {
|
|||
|
|
const key = `${x},${y}`;
|
|||
|
|
const pawnId = state.regions.board.partMap[key];
|
|||
|
|
return pawnId ? state.pawns[pawnId] : null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查玩家是否拥有某张卡牌
|
|||
|
|
*/
|
|||
|
|
function playerHasCard(state: OnitamaState, player: PlayerType, cardName: string): boolean {
|
|||
|
|
const cardList = player === 'red' ? state.redCards : state.blackCards;
|
|||
|
|
return cardList.includes(cardName);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查移动是否合法
|
|||
|
|
*/
|
|||
|
|
function isValidMove(state: OnitamaState, cardName: string, fromX: number, fromY: number, toX: number, toY: number, player: PlayerType): string | null {
|
|||
|
|
// 检查玩家是否拥有该卡牌
|
|||
|
|
if (!playerHasCard(state, player, cardName)) {
|
|||
|
|
return `玩家 ${player} 不拥有卡牌 ${cardName}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查起始位置是否有玩家的棋子
|
|||
|
|
const fromPawn = getPawnAtPosition(state, fromX, fromY);
|
|||
|
|
if (!fromPawn) {
|
|||
|
|
return `位置 (${fromX}, ${fromY}) 没有棋子`;
|
|||
|
|
}
|
|||
|
|
if (fromPawn.owner !== player) {
|
|||
|
|
return `位置 (${fromX}, ${fromY}) 的棋子不属于玩家 ${player}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查卡牌是否存在
|
|||
|
|
const card = state.cards[cardName];
|
|||
|
|
if (!card) {
|
|||
|
|
return `卡牌 ${cardName} 不存在`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 计算移动偏移量
|
|||
|
|
const dx = toX - fromX;
|
|||
|
|
const dy = toY - fromY;
|
|||
|
|
|
|||
|
|
// 检查移动是否在卡牌的移动候选项中
|
|||
|
|
const isValid = card.moveCandidates.some(m => m.dx === dx && m.dy === dy);
|
|||
|
|
if (!isValid) {
|
|||
|
|
return `卡牌 ${cardName} 不支持移动 (${dx}, ${dy})`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查目标位置是否在棋盘内
|
|||
|
|
if (!isInBounds(toX, toY)) {
|
|||
|
|
return `目标位置 (${toX}, ${toY}) 超出棋盘范围`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查目标位置是否有己方棋子
|
|||
|
|
const toPawn = getPawnAtPosition(state, toX, toY);
|
|||
|
|
if (toPawn && toPawn.owner === player) {
|
|||
|
|
return `目标位置 (${toX}, ${toY}) 已有己方棋子`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 执行移动
|
|||
|
|
*/
|
|||
|
|
async function handleMove(game: OnitamaGame, player: PlayerType, cardName: string, fromX: number, fromY: number, toX: number, toY: number) {
|
|||
|
|
const state = game.value;
|
|||
|
|
|
|||
|
|
// 验证移动
|
|||
|
|
const error = isValidMove(state, cardName, fromX, fromY, toX, toY, player);
|
|||
|
|
if (error) {
|
|||
|
|
throw new Error(error);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const capturedPawnId = getPawnAtPosition(state, toX, toY)?.id || null;
|
|||
|
|
|
|||
|
|
await game.produceAsync(state => {
|
|||
|
|
const pawn = state.pawns[getPawnAtPosition(state, fromX, fromY)!.id];
|
|||
|
|
|
|||
|
|
// 如果目标位置有敌方棋子,将其移除(吃掉)
|
|||
|
|
if (capturedPawnId) {
|
|||
|
|
const capturedPawn = state.pawns[capturedPawnId];
|
|||
|
|
moveToRegion(capturedPawn, state.regions.board, null);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 移动棋子到目标位置
|
|||
|
|
moveToRegion(pawn, state.regions.board, state.regions.board, [toX, toY]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 交换卡牌
|
|||
|
|
await handleSwapCard(game, player, cardName);
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
from: { x: fromX, y: fromY },
|
|||
|
|
to: { x: toX, y: toY },
|
|||
|
|
card: cardName,
|
|||
|
|
captured: capturedPawnId
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const move = registry.register({
|
|||
|
|
schema: 'move <player> <card:string> <fromX:number> <fromY:number> <toX:number> <toY:number>',
|
|||
|
|
run: handleMove
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 交换卡牌:将使用的卡牌与备用卡牌交换
|
|||
|
|
*/
|
|||
|
|
async function handleSwapCard(game: OnitamaGame, player: PlayerType, usedCard: string) {
|
|||
|
|
await game.produceAsync(state => {
|
|||
|
|
const spareCard = state.spareCard;
|
|||
|
|
const usedCardData = state.cards[usedCard];
|
|||
|
|
const spareCardData = state.cards[spareCard];
|
|||
|
|
|
|||
|
|
// 从玩家手牌中移除使用的卡牌
|
|||
|
|
if (player === 'red') {
|
|||
|
|
state.redCards = state.redCards.filter(c => c !== usedCard);
|
|||
|
|
state.redCards.push(spareCard);
|
|||
|
|
} else {
|
|||
|
|
state.blackCards = state.blackCards.filter(c => c !== usedCard);
|
|||
|
|
state.blackCards.push(spareCard);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新卡牌区域
|
|||
|
|
usedCardData.regionId = 'spare';
|
|||
|
|
spareCardData.regionId = player;
|
|||
|
|
|
|||
|
|
// 更新备用卡牌
|
|||
|
|
state.spareCard = usedCard;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const swapCard = registry.register({
|
|||
|
|
schema: 'swap-card <player> <card:string>',
|
|||
|
|
run: handleSwapCard
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查占领胜利条件:玩家的师父棋子到达对手的初始位置
|
|||
|
|
*/
|
|||
|
|
async function handleCheckConquestWin(game: OnitamaGame): Promise<PlayerType | null> {
|
|||
|
|
const state = game.value;
|
|||
|
|
|
|||
|
|
// 红色师父到达 y=4(黑色初始位置)
|
|||
|
|
const redMaster = state.pawns['red-master'];
|
|||
|
|
if (redMaster && redMaster.regionId === 'board' && redMaster.position[1] === 4) {
|
|||
|
|
return 'red';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 黑色师父到达 y=0(红色初始位置)
|
|||
|
|
const blackMaster = state.pawns['black-master'];
|
|||
|
|
if (blackMaster && blackMaster.regionId === 'board' && blackMaster.position[1] === 0) {
|
|||
|
|
return 'black';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const checkConquestWin = registry.register({
|
|||
|
|
schema: 'check-conquest-win',
|
|||
|
|
run: handleCheckConquestWin
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查吃掉胜利条件:对手的师父棋子被吃掉(不在棋盘上)
|
|||
|
|
*/
|
|||
|
|
async function handleCheckCaptureWin(game: OnitamaGame): Promise<PlayerType | null> {
|
|||
|
|
const state = game.value;
|
|||
|
|
|
|||
|
|
// 红色师父不在棋盘上,黑色获胜
|
|||
|
|
const redMaster = state.pawns['red-master'];
|
|||
|
|
if (!redMaster || redMaster.regionId !== 'board') {
|
|||
|
|
return 'black';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 黑色师父不在棋盘上,红色获胜
|
|||
|
|
const blackMaster = state.pawns['black-master'];
|
|||
|
|
if (!blackMaster || blackMaster.regionId !== 'board') {
|
|||
|
|
return 'red';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const checkCaptureWin = registry.register({
|
|||
|
|
schema: 'check-capture-win',
|
|||
|
|
run: handleCheckCaptureWin
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 综合胜利检测
|
|||
|
|
*/
|
|||
|
|
async function handleCheckWin(game: OnitamaGame): Promise<PlayerType | null> {
|
|||
|
|
const conquestWinner = await handleCheckConquestWin(game);
|
|||
|
|
if (conquestWinner) {
|
|||
|
|
return conquestWinner;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const captureWinner = await handleCheckCaptureWin(game);
|
|||
|
|
if (captureWinner) {
|
|||
|
|
return captureWinner;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const checkWin = registry.register({
|
|||
|
|
schema: 'check-win',
|
|||
|
|
run: handleCheckWin
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取玩家可用的移动
|
|||
|
|
*/
|
|||
|
|
function getAvailableMoves(state: OnitamaState, player: PlayerType): Array<{card: string, fromX: number, fromY: number, toX: number, toY: number}> {
|
|||
|
|
const moves: Array<{card: string, fromX: number, fromY: number, toX: number, toY: number}> = [];
|
|||
|
|
|
|||
|
|
// 获取玩家的所有卡牌
|
|||
|
|
const cardNames = player === 'red' ? state.redCards : state.blackCards;
|
|||
|
|
|
|||
|
|
// 获取玩家的所有棋子
|
|||
|
|
const playerPawns = Object.values(state.pawns).filter(p => p.owner === player && p.regionId === 'board');
|
|||
|
|
|
|||
|
|
// 对于每张卡牌
|
|||
|
|
for (const cardName of cardNames) {
|
|||
|
|
const card = state.cards[cardName];
|
|||
|
|
|
|||
|
|
// 对于每个棋子
|
|||
|
|
for (const pawn of playerPawns) {
|
|||
|
|
const [fromX, fromY] = pawn.position;
|
|||
|
|
|
|||
|
|
// 对于卡牌的每个移动
|
|||
|
|
for (const move of card.moveCandidates) {
|
|||
|
|
const toX = fromX + move.dx;
|
|||
|
|
const toY = fromY + move.dy;
|
|||
|
|
|
|||
|
|
// 检查移动是否合法
|
|||
|
|
if (isInBounds(toX, toY)) {
|
|||
|
|
const targetPawn = getPawnAtPosition(state, toX, toY);
|
|||
|
|
// 目标位置为空或有敌方棋子
|
|||
|
|
if (!targetPawn || targetPawn.owner !== player) {
|
|||
|
|
moves.push({ card: cardName, fromX, fromY, toX, toY });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return moves;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理回合
|
|||
|
|
*/
|
|||
|
|
async function handleTurn(game: OnitamaGame, turnPlayer: PlayerType) {
|
|||
|
|
const state = game.value;
|
|||
|
|
const availableMoves = getAvailableMoves(state, turnPlayer);
|
|||
|
|
|
|||
|
|
let moveOutput;
|
|||
|
|
|
|||
|
|
if (availableMoves.length === 0) {
|
|||
|
|
// 没有可用移动,玩家必须交换一张卡牌
|
|||
|
|
const cardToSwap = await game.prompt(
|
|||
|
|
prompts.move,
|
|||
|
|
(player, card, _fromX, _fromY, _toX, _toY) => {
|
|||
|
|
if (player !== turnPlayer) {
|
|||
|
|
throw `Invalid player: ${player}. Expected ${turnPlayer}.`;
|
|||
|
|
}
|
|||
|
|
if (!playerHasCard(state, player, card)) {
|
|||
|
|
throw `Player ${player} does not have card ${card}.`;
|
|||
|
|
}
|
|||
|
|
return card;
|
|||
|
|
},
|
|||
|
|
turnPlayer
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
await swapCard(game, turnPlayer, cardToSwap);
|
|||
|
|
moveOutput = { swappedCard: cardToSwap, noMoves: true };
|
|||
|
|
} else {
|
|||
|
|
// 有可用移动,提示玩家选择
|
|||
|
|
moveOutput = await game.prompt(
|
|||
|
|
prompts.move,
|
|||
|
|
(player, card, fromX, fromY, toX, toY) => {
|
|||
|
|
if (player !== turnPlayer) {
|
|||
|
|
throw `Invalid player: ${player}. Expected ${turnPlayer}.`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const error = isValidMove(state, card, fromX, fromY, toX, toY, player);
|
|||
|
|
if (error) {
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { player, card, fromX, fromY, toX, toY };
|
|||
|
|
},
|
|||
|
|
turnPlayer
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
await move(game, moveOutput.player, moveOutput.card, moveOutput.fromX, moveOutput.fromY, moveOutput.toX, moveOutput.toY);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查胜利
|
|||
|
|
const winner = await checkWin(game);
|
|||
|
|
|
|||
|
|
await game.produceAsync(state => {
|
|||
|
|
state.winner = winner;
|
|||
|
|
if (!winner) {
|
|||
|
|
state.currentPlayer = state.currentPlayer === 'red' ? 'black' : 'red';
|
|||
|
|
state.turn++;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return { winner, move: moveOutput };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const turn = registry.register({
|
|||
|
|
schema: 'turn <player>',
|
|||
|
|
run: handleTurn
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 开始游戏主循环
|
|||
|
|
*/
|
|||
|
|
export async function start(game: OnitamaGame) {
|
|||
|
|
while (true) {
|
|||
|
|
const currentPlayer = game.value.currentPlayer;
|
|||
|
|
const turnOutput = await turn(game, currentPlayer);
|
|||
|
|
|
|||
|
|
if (turnOutput.winner) {
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return game.value;
|
|||
|
|
}
|