refactor: format Onitama commands and Tic-Tac-Toe imports
This commit is contained in:
parent
b009189b9a
commit
1bd49c32e0
|
|
@ -3,12 +3,9 @@ import {
|
||||||
OnitamaState,
|
OnitamaState,
|
||||||
PlayerType,
|
PlayerType,
|
||||||
prompts,
|
prompts,
|
||||||
initializeCards
|
initializeCards,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import {createGameCommandRegistry} from "@/core/game";
|
import { moveToRegion } from "@/core/region";
|
||||||
import {moveToRegion} from "@/core/region";
|
|
||||||
|
|
||||||
export const registry = createGameCommandRegistry<OnitamaState>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查位置是否在棋盘范围内
|
* 检查位置是否在棋盘范围内
|
||||||
|
|
@ -29,8 +26,12 @@ function getPawnAtPosition(state: OnitamaState, x: number, y: number) {
|
||||||
/**
|
/**
|
||||||
* 检查玩家是否拥有某张卡牌
|
* 检查玩家是否拥有某张卡牌
|
||||||
*/
|
*/
|
||||||
function playerHasCard(state: OnitamaState, player: PlayerType, cardName: string): boolean {
|
function playerHasCard(
|
||||||
const cardList = player === 'red' ? state.redCards : state.blackCards;
|
state: OnitamaState,
|
||||||
|
player: PlayerType,
|
||||||
|
cardName: string,
|
||||||
|
): boolean {
|
||||||
|
const cardList = player === "red" ? state.redCards : state.blackCards;
|
||||||
return cardList.includes(cardName);
|
return cardList.includes(cardName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,13 +39,17 @@ function playerHasCard(state: OnitamaState, player: PlayerType, cardName: string
|
||||||
* 获取卡牌的移动候选项,根据玩家视角进行旋转
|
* 获取卡牌的移动候选项,根据玩家视角进行旋转
|
||||||
* 黑方需要将卡牌旋转180度
|
* 黑方需要将卡牌旋转180度
|
||||||
*/
|
*/
|
||||||
export function getCardMoveCandidates(state: OnitamaState, cardName: string, player: PlayerType) {
|
export function getCardMoveCandidates(
|
||||||
|
state: OnitamaState,
|
||||||
|
cardName: string,
|
||||||
|
player: PlayerType,
|
||||||
|
) {
|
||||||
const card = state.cards[cardName];
|
const card = state.cards[cardName];
|
||||||
const candidates = card.moveCandidates;
|
const candidates = card.moveCandidates;
|
||||||
|
|
||||||
// 黑方需要将卡牌旋转180度
|
// 黑方需要将卡牌旋转180度
|
||||||
if (player === 'black') {
|
if (player === "black") {
|
||||||
return candidates.map(m => ({ dx: -m.dx, dy: -m.dy }));
|
return candidates.map((m) => ({ dx: -m.dx, dy: -m.dy }));
|
||||||
}
|
}
|
||||||
|
|
||||||
return candidates;
|
return candidates;
|
||||||
|
|
@ -53,7 +58,15 @@ export function getCardMoveCandidates(state: OnitamaState, cardName: string, pla
|
||||||
/**
|
/**
|
||||||
* 检查移动是否合法
|
* 检查移动是否合法
|
||||||
*/
|
*/
|
||||||
export function isValidMove(state: OnitamaState, cardName: string, fromX: number, fromY: number, toX: number, toY: number, player: PlayerType): string | null {
|
export function isValidMove(
|
||||||
|
state: OnitamaState,
|
||||||
|
cardName: string,
|
||||||
|
fromX: number,
|
||||||
|
fromY: number,
|
||||||
|
toX: number,
|
||||||
|
toY: number,
|
||||||
|
player: PlayerType,
|
||||||
|
): string | null {
|
||||||
// 检查玩家是否拥有该卡牌
|
// 检查玩家是否拥有该卡牌
|
||||||
if (!playerHasCard(state, player, cardName)) {
|
if (!playerHasCard(state, player, cardName)) {
|
||||||
return `玩家 ${player} 不拥有卡牌 ${cardName}`;
|
return `玩家 ${player} 不拥有卡牌 ${cardName}`;
|
||||||
|
|
@ -80,7 +93,7 @@ export function isValidMove(state: OnitamaState, cardName: string, fromX: number
|
||||||
|
|
||||||
// 检查移动是否在卡牌的移动候选项中(黑方需要旋转180度)
|
// 检查移动是否在卡牌的移动候选项中(黑方需要旋转180度)
|
||||||
const candidates = getCardMoveCandidates(state, cardName, player);
|
const candidates = getCardMoveCandidates(state, cardName, player);
|
||||||
const isValid = candidates.some(m => m.dx === dx && m.dy === dy);
|
const isValid = candidates.some((m) => m.dx === dx && m.dy === dy);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
return `卡牌 ${cardName} 不支持移动 (${dx}, ${dy})`;
|
return `卡牌 ${cardName} 不支持移动 (${dx}, ${dy})`;
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +115,15 @@ export function isValidMove(state: OnitamaState, cardName: string, fromX: number
|
||||||
/**
|
/**
|
||||||
* 执行移动
|
* 执行移动
|
||||||
*/
|
*/
|
||||||
async function handleMove(game: OnitamaGame, player: PlayerType, cardName: string, fromX: number, fromY: number, toX: number, toY: number) {
|
async function move(
|
||||||
|
game: OnitamaGame,
|
||||||
|
player: PlayerType,
|
||||||
|
cardName: string,
|
||||||
|
fromX: number,
|
||||||
|
fromY: number,
|
||||||
|
toX: number,
|
||||||
|
toY: number,
|
||||||
|
) {
|
||||||
const state = game.value;
|
const state = game.value;
|
||||||
|
|
||||||
// 验证移动
|
// 验证移动
|
||||||
|
|
@ -113,7 +134,7 @@ async function handleMove(game: OnitamaGame, player: PlayerType, cardName: strin
|
||||||
|
|
||||||
const capturedPawnId = getPawnAtPosition(state, toX, toY)?.id || null;
|
const capturedPawnId = getPawnAtPosition(state, toX, toY)?.id || null;
|
||||||
|
|
||||||
await game.produceAsync(state => {
|
await game.produceAsync((state) => {
|
||||||
const pawn = state.pawns[getPawnAtPosition(state, fromX, fromY)!.id];
|
const pawn = state.pawns[getPawnAtPosition(state, fromX, fromY)!.id];
|
||||||
|
|
||||||
// 如果目标位置有敌方棋子,将其移除(吃掉)
|
// 如果目标位置有敌方棋子,将其移除(吃掉)
|
||||||
|
|
@ -127,41 +148,40 @@ async function handleMove(game: OnitamaGame, player: PlayerType, cardName: strin
|
||||||
});
|
});
|
||||||
|
|
||||||
// 交换卡牌
|
// 交换卡牌
|
||||||
await handleSwapCard(game, player, cardName);
|
await swapCard(game, player, cardName);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
from: { x: fromX, y: fromY },
|
from: { x: fromX, y: fromY },
|
||||||
to: { x: toX, y: toY },
|
to: { x: toX, y: toY },
|
||||||
card: cardName,
|
card: cardName,
|
||||||
captured: capturedPawnId
|
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) {
|
async function swapCard(
|
||||||
await game.produceAsync(state => {
|
game: OnitamaGame,
|
||||||
|
player: PlayerType,
|
||||||
|
usedCard: string,
|
||||||
|
) {
|
||||||
|
await game.produceAsync((state) => {
|
||||||
const spareCard = state.spareCard;
|
const spareCard = state.spareCard;
|
||||||
const usedCardData = state.cards[usedCard];
|
const usedCardData = state.cards[usedCard];
|
||||||
const spareCardData = state.cards[spareCard];
|
const spareCardData = state.cards[spareCard];
|
||||||
|
|
||||||
// 从玩家手牌中移除使用的卡牌
|
// 从玩家手牌中移除使用的卡牌
|
||||||
if (player === 'red') {
|
if (player === "red") {
|
||||||
state.redCards = state.redCards.filter(c => c !== usedCard);
|
state.redCards = state.redCards.filter((c) => c !== usedCard);
|
||||||
state.redCards.push(spareCard);
|
state.redCards.push(spareCard);
|
||||||
} else {
|
} else {
|
||||||
state.blackCards = state.blackCards.filter(c => c !== usedCard);
|
state.blackCards = state.blackCards.filter((c) => c !== usedCard);
|
||||||
state.blackCards.push(spareCard);
|
state.blackCards.push(spareCard);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新卡牌区域
|
// 更新卡牌区域
|
||||||
usedCardData.regionId = 'spare';
|
usedCardData.regionId = "spare";
|
||||||
spareCardData.regionId = player;
|
spareCardData.regionId = player;
|
||||||
|
|
||||||
// 更新备用卡牌
|
// 更新备用卡牌
|
||||||
|
|
@ -169,75 +189,70 @@ async function handleSwapCard(game: OnitamaGame, player: PlayerType, usedCard: s
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const swapCard = registry.register({
|
|
||||||
schema: 'swap-card <player> <card:string>',
|
|
||||||
run: handleSwapCard
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查占领胜利条件:玩家的师父棋子到达对手的初始位置
|
* 检查占领胜利条件:玩家的师父棋子到达对手的初始位置
|
||||||
* 红色师父需要到达 (2, 4) - 黑色师父的初始位置
|
* 红色师父需要到达 (2, 4) - 黑色师父的初始位置
|
||||||
* 黑色师父需要到达 (2, 0) - 红色师父的初始位置
|
* 黑色师父需要到达 (2, 0) - 红色师父的初始位置
|
||||||
*/
|
*/
|
||||||
async function handleCheckConquestWin(game: OnitamaGame): Promise<PlayerType | null> {
|
async function checkConquestWin(game: OnitamaGame): Promise<PlayerType | null> {
|
||||||
const state = game.value;
|
const state = game.value;
|
||||||
|
|
||||||
// 红色师父到达 (2, 4)(黑色师父的初始位置)
|
// 红色师父到达 (2, 4)(黑色师父的初始位置)
|
||||||
const redMaster = state.pawns['red-master'];
|
const redMaster = state.pawns["red-master"];
|
||||||
if (redMaster && redMaster.regionId === 'board' && redMaster.position[0] === 2 && redMaster.position[1] === 4) {
|
if (
|
||||||
return 'red';
|
redMaster &&
|
||||||
|
redMaster.regionId === "board" &&
|
||||||
|
redMaster.position[0] === 2 &&
|
||||||
|
redMaster.position[1] === 4
|
||||||
|
) {
|
||||||
|
return "red";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 黑色师父到达 (2, 0)(红色师父的初始位置)
|
// 黑色师父到达 (2, 0)(红色师父的初始位置)
|
||||||
const blackMaster = state.pawns['black-master'];
|
const blackMaster = state.pawns["black-master"];
|
||||||
if (blackMaster && blackMaster.regionId === 'board' && blackMaster.position[0] === 2 && blackMaster.position[1] === 0) {
|
if (
|
||||||
return 'black';
|
blackMaster &&
|
||||||
|
blackMaster.regionId === "board" &&
|
||||||
|
blackMaster.position[0] === 2 &&
|
||||||
|
blackMaster.position[1] === 0
|
||||||
|
) {
|
||||||
|
return "black";
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkConquestWin = registry.register({
|
|
||||||
schema: 'check-conquest-win',
|
|
||||||
run: handleCheckConquestWin
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查吃掉胜利条件:对手的师父棋子被吃掉(不在棋盘上)
|
* 检查吃掉胜利条件:对手的师父棋子被吃掉(不在棋盘上)
|
||||||
*/
|
*/
|
||||||
async function handleCheckCaptureWin(game: OnitamaGame): Promise<PlayerType | null> {
|
async function checkCaptureWin(game: OnitamaGame): Promise<PlayerType | null> {
|
||||||
const state = game.value;
|
const state = game.value;
|
||||||
|
|
||||||
// 红色师父不在棋盘上,黑色获胜
|
// 红色师父不在棋盘上,黑色获胜
|
||||||
const redMaster = state.pawns['red-master'];
|
const redMaster = state.pawns["red-master"];
|
||||||
if (!redMaster || redMaster.regionId !== 'board') {
|
if (!redMaster || redMaster.regionId !== "board") {
|
||||||
return 'black';
|
return "black";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 黑色师父不在棋盘上,红色获胜
|
// 黑色师父不在棋盘上,红色获胜
|
||||||
const blackMaster = state.pawns['black-master'];
|
const blackMaster = state.pawns["black-master"];
|
||||||
if (!blackMaster || blackMaster.regionId !== 'board') {
|
if (!blackMaster || blackMaster.regionId !== "board") {
|
||||||
return 'red';
|
return "red";
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkCaptureWin = registry.register({
|
|
||||||
schema: 'check-capture-win',
|
|
||||||
run: handleCheckCaptureWin
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 综合胜利检测
|
* 综合胜利检测
|
||||||
*/
|
*/
|
||||||
async function handleCheckWin(game: OnitamaGame): Promise<PlayerType | null> {
|
async function checkWin(game: OnitamaGame): Promise<PlayerType | null> {
|
||||||
const conquestWinner = await handleCheckConquestWin(game);
|
const conquestWinner = await checkConquestWin(game);
|
||||||
if (conquestWinner) {
|
if (conquestWinner) {
|
||||||
return conquestWinner;
|
return conquestWinner;
|
||||||
}
|
}
|
||||||
|
|
||||||
const captureWinner = await handleCheckCaptureWin(game);
|
const captureWinner = await checkCaptureWin(game);
|
||||||
if (captureWinner) {
|
if (captureWinner) {
|
||||||
return captureWinner;
|
return captureWinner;
|
||||||
}
|
}
|
||||||
|
|
@ -245,22 +260,34 @@ async function handleCheckWin(game: OnitamaGame): Promise<PlayerType | null> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkWin = registry.register({
|
|
||||||
schema: 'check-win',
|
|
||||||
run: handleCheckWin
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取玩家可用的移动
|
* 获取玩家可用的移动
|
||||||
*/
|
*/
|
||||||
export function getAvailableMoves(state: OnitamaState, player: PlayerType): Array<{card: string, fromX: number, fromY: number, toX: number, toY: number}> {
|
export function getAvailableMoves(
|
||||||
const moves: Array<{card: string, fromX: number, fromY: number, toX: number, toY: number}> = [];
|
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 cardNames = player === "red" ? state.redCards : state.blackCards;
|
||||||
|
|
||||||
// 获取玩家的所有棋子
|
// 获取玩家的所有棋子
|
||||||
const playerPawns = Object.values(state.pawns).filter(p => p.owner === player && p.regionId === 'board');
|
const playerPawns = Object.values(state.pawns).filter(
|
||||||
|
(p) => p.owner === player && p.regionId === "board",
|
||||||
|
);
|
||||||
|
|
||||||
// 对于每张卡牌
|
// 对于每张卡牌
|
||||||
for (const cardName of cardNames) {
|
for (const cardName of cardNames) {
|
||||||
|
|
@ -294,7 +321,7 @@ export function getAvailableMoves(state: OnitamaState, player: PlayerType): Arra
|
||||||
/**
|
/**
|
||||||
* 处理回合
|
* 处理回合
|
||||||
*/
|
*/
|
||||||
async function handleTurn(game: OnitamaGame, turnPlayer: PlayerType) {
|
async function turn(game: OnitamaGame, turnPlayer: PlayerType) {
|
||||||
const state = game.value;
|
const state = game.value;
|
||||||
const availableMoves = getAvailableMoves(state, turnPlayer);
|
const availableMoves = getAvailableMoves(state, turnPlayer);
|
||||||
|
|
||||||
|
|
@ -313,7 +340,7 @@ async function handleTurn(game: OnitamaGame, turnPlayer: PlayerType) {
|
||||||
}
|
}
|
||||||
return card;
|
return card;
|
||||||
},
|
},
|
||||||
turnPlayer
|
turnPlayer,
|
||||||
);
|
);
|
||||||
|
|
||||||
await swapCard(game, turnPlayer, cardToSwap);
|
await swapCard(game, turnPlayer, cardToSwap);
|
||||||
|
|
@ -334,19 +361,27 @@ async function handleTurn(game: OnitamaGame, turnPlayer: PlayerType) {
|
||||||
|
|
||||||
return { player, card, fromX, fromY, toX, toY };
|
return { player, card, fromX, fromY, toX, toY };
|
||||||
},
|
},
|
||||||
turnPlayer
|
turnPlayer,
|
||||||
);
|
);
|
||||||
|
|
||||||
await move(game, moveOutput.player, moveOutput.card, moveOutput.fromX, moveOutput.fromY, moveOutput.toX, moveOutput.toY);
|
await move(
|
||||||
|
game,
|
||||||
|
moveOutput.player,
|
||||||
|
moveOutput.card,
|
||||||
|
moveOutput.fromX,
|
||||||
|
moveOutput.fromY,
|
||||||
|
moveOutput.toX,
|
||||||
|
moveOutput.toY,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查胜利
|
// 检查胜利
|
||||||
const winner = await checkWin(game);
|
const winner = await checkWin(game);
|
||||||
|
|
||||||
await game.produceAsync(state => {
|
await game.produceAsync((state) => {
|
||||||
state.winner = winner;
|
state.winner = winner;
|
||||||
if (!winner) {
|
if (!winner) {
|
||||||
state.currentPlayer = state.currentPlayer === 'red' ? 'black' : 'red';
|
state.currentPlayer = state.currentPlayer === "red" ? "black" : "red";
|
||||||
state.turn++;
|
state.turn++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -354,11 +389,6 @@ async function handleTurn(game: OnitamaGame, turnPlayer: PlayerType) {
|
||||||
return { winner, move: moveOutput };
|
return { winner, move: moveOutput };
|
||||||
}
|
}
|
||||||
|
|
||||||
const turn = registry.register({
|
|
||||||
schema: 'turn <player>',
|
|
||||||
run: handleTurn
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 开始游戏主循环
|
* 开始游戏主循环
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
import { Part } from "@/core/part";
|
import { Part } from "@/core/part";
|
||||||
import { createRegion } from "@/core/region";
|
import { createRegion } from "@/core/region";
|
||||||
import {
|
import { createPromptDef, IGameContext } from "@/core/game";
|
||||||
createGameCommandRegistry,
|
|
||||||
createPromptDef,
|
|
||||||
IGameContext,
|
|
||||||
} from "@/core/game";
|
|
||||||
|
|
||||||
const BOARD_SIZE = 3;
|
const BOARD_SIZE = 3;
|
||||||
const MAX_TURNS = BOARD_SIZE * BOARD_SIZE;
|
const MAX_TURNS = BOARD_SIZE * BOARD_SIZE;
|
||||||
|
|
@ -69,7 +65,6 @@ export function createInitialState() {
|
||||||
}
|
}
|
||||||
export type TicTacToeState = ReturnType<typeof createInitialState>;
|
export type TicTacToeState = ReturnType<typeof createInitialState>;
|
||||||
export type TicTacToeGame = IGameContext<TicTacToeState>;
|
export type TicTacToeGame = IGameContext<TicTacToeState>;
|
||||||
export const registry = createGameCommandRegistry<TicTacToeState>();
|
|
||||||
export const prompts = {
|
export const prompts = {
|
||||||
play: createPromptDef<[PlayerType, number, number]>(
|
play: createPromptDef<[PlayerType, number, number]>(
|
||||||
"play <player> <row:number> <col:number>",
|
"play <player> <row:number> <col:number>",
|
||||||
|
|
@ -95,9 +90,11 @@ export async function start(game: TicTacToeGame) {
|
||||||
return game.value;
|
return game.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const turn = registry.register({
|
async function turn(
|
||||||
schema: "turn <player> <turnNumber:number>",
|
game: TicTacToeGame,
|
||||||
async run(game: TicTacToeGame, turnPlayer: PlayerType, turnNumber: number) {
|
turnPlayer: PlayerType,
|
||||||
|
turnNumber: number,
|
||||||
|
) {
|
||||||
const { player, row, col } = await game.prompt(
|
const { player, row, col } = await game.prompt(
|
||||||
prompts.play,
|
prompts.play,
|
||||||
(player, row, col) => {
|
(player, row, col) => {
|
||||||
|
|
@ -121,8 +118,7 @@ const turn = registry.register({
|
||||||
if (turnNumber >= MAX_TURNS) return { winner: "draw" as WinnerType };
|
if (turnNumber >= MAX_TURNS) return { winner: "draw" as WinnerType };
|
||||||
|
|
||||||
return { winner: null };
|
return { winner: null };
|
||||||
},
|
}
|
||||||
});
|
|
||||||
|
|
||||||
function isValidMove(row: number, col: number): boolean {
|
function isValidMove(row: number, col: number): boolean {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue