refactor: format Onitama commands and Tic-Tac-Toe imports

This commit is contained in:
hyper 2026-04-23 15:18:37 +08:00
parent b009189b9a
commit 1bd49c32e0
2 changed files with 338 additions and 312 deletions

View File

@ -1,379 +1,409 @@
import { import {
OnitamaGame, OnitamaGame,
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>();
/** /**
* *
*/ */
function isInBounds(x: number, y: number): boolean { function isInBounds(x: number, y: number): boolean {
return x >= 0 && x < 5 && y >= 0 && y < 5; return x >= 0 && x < 5 && y >= 0 && y < 5;
} }
/** /**
* *
*/ */
function getPawnAtPosition(state: OnitamaState, x: number, y: number) { function getPawnAtPosition(state: OnitamaState, x: number, y: number) {
const key = `${x},${y}`; const key = `${x},${y}`;
const pawnId = state.regions.board.partMap[key]; const pawnId = state.regions.board.partMap[key];
return pawnId ? state.pawns[pawnId] : null; return pawnId ? state.pawns[pawnId] : null;
} }
/** /**
* *
*/ */
function playerHasCard(state: OnitamaState, player: PlayerType, cardName: string): boolean { function playerHasCard(
const cardList = player === 'red' ? state.redCards : state.blackCards; state: OnitamaState,
return cardList.includes(cardName); player: PlayerType,
cardName: string,
): boolean {
const cardList = player === "red" ? state.redCards : state.blackCards;
return cardList.includes(cardName);
} }
/** /**
* *
* 180 * 180
*/ */
export function getCardMoveCandidates(state: OnitamaState, cardName: string, player: PlayerType) { export function getCardMoveCandidates(
const card = state.cards[cardName]; state: OnitamaState,
const candidates = card.moveCandidates; cardName: string,
player: PlayerType,
) {
const card = state.cards[cardName];
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;
} }
/** /**
* *
*/ */
export function isValidMove(state: OnitamaState, cardName: string, fromX: number, fromY: number, toX: number, toY: number, player: PlayerType): string | null { export function isValidMove(
// 检查玩家是否拥有该卡牌 state: OnitamaState,
if (!playerHasCard(state, player, cardName)) { cardName: string,
return `玩家 ${player} 不拥有卡牌 ${cardName}`; 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); const fromPawn = getPawnAtPosition(state, fromX, fromY);
if (!fromPawn) { if (!fromPawn) {
return `位置 (${fromX}, ${fromY}) 没有棋子`; return `位置 (${fromX}, ${fromY}) 没有棋子`;
} }
if (fromPawn.owner !== player) { if (fromPawn.owner !== player) {
return `位置 (${fromX}, ${fromY}) 的棋子不属于玩家 ${player}`; return `位置 (${fromX}, ${fromY}) 的棋子不属于玩家 ${player}`;
} }
// 检查卡牌是否存在 // 检查卡牌是否存在
const card = state.cards[cardName]; const card = state.cards[cardName];
if (!card) { if (!card) {
return `卡牌 ${cardName} 不存在`; return `卡牌 ${cardName} 不存在`;
} }
// 计算移动偏移量 // 计算移动偏移量
const dx = toX - fromX; const dx = toX - fromX;
const dy = toY - fromY; const dy = toY - fromY;
// 检查移动是否在卡牌的移动候选项中黑方需要旋转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})`;
} }
// 检查目标位置是否在棋盘内 // 检查目标位置是否在棋盘内
if (!isInBounds(toX, toY)) { if (!isInBounds(toX, toY)) {
return `目标位置 (${toX}, ${toY}) 超出棋盘范围`; return `目标位置 (${toX}, ${toY}) 超出棋盘范围`;
} }
// 检查目标位置是否有己方棋子 // 检查目标位置是否有己方棋子
const toPawn = getPawnAtPosition(state, toX, toY); const toPawn = getPawnAtPosition(state, toX, toY);
if (toPawn && toPawn.owner === player) { if (toPawn && toPawn.owner === player) {
return `目标位置 (${toX}, ${toY}) 已有己方棋子`; return `目标位置 (${toX}, ${toY}) 已有己方棋子`;
} }
return null; return null;
} }
/** /**
* *
*/ */
async function handleMove(game: OnitamaGame, player: PlayerType, cardName: string, fromX: number, fromY: number, toX: number, toY: number) { async function move(
const state = game.value; game: OnitamaGame,
player: PlayerType,
// 验证移动 cardName: string,
const error = isValidMove(state, cardName, fromX, fromY, toX, toY, player); fromX: number,
if (error) { fromY: number,
throw new Error(error); toX: number,
} toY: number,
) {
const capturedPawnId = getPawnAtPosition(state, toX, toY)?.id || null; const state = game.value;
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>', const error = isValidMove(state, cardName, fromX, fromY, toX, toY, player);
run: handleMove 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 swapCard(game, player, cardName);
return {
from: { x: fromX, y: fromY },
to: { x: toX, y: toY },
card: cardName,
captured: capturedPawnId,
};
}
/** /**
* 使 * 使
*/ */
async function handleSwapCard(game: OnitamaGame, player: PlayerType, usedCard: string) { async function swapCard(
await game.produceAsync(state => { game: OnitamaGame,
const spareCard = state.spareCard; player: PlayerType,
const usedCardData = state.cards[usedCard]; usedCard: string,
const spareCardData = state.cards[spareCard]; ) {
await game.produceAsync((state) => {
// 从玩家手牌中移除使用的卡牌 const spareCard = state.spareCard;
if (player === 'red') { const usedCardData = state.cards[usedCard];
state.redCards = state.redCards.filter(c => c !== usedCard); const spareCardData = state.cards[spareCard];
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>', if (player === "red") {
run: handleSwapCard 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;
});
}
/** /**
* *
* (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'];
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', const redMaster = state.pawns["red-master"];
run: handleCheckCaptureWin if (!redMaster || redMaster.regionId !== "board") {
}); return "black";
}
// 黑色师父不在棋盘上,红色获胜
const blackMaster = state.pawns["black-master"];
if (!blackMaster || blackMaster.regionId !== "board") {
return "red";
}
return null;
}
/** /**
* *
*/ */
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);
if (captureWinner) {
return captureWinner;
}
return null;
}
const checkWin = registry.register({ const captureWinner = await checkCaptureWin(game);
schema: 'check-win', if (captureWinner) {
run: handleCheckWin return captureWinner;
}); }
return null;
}
/** /**
* *
*/ */
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) {
// 获取旋转后的移动候选项黑方需要旋转180度 // 获取旋转后的移动候选项黑方需要旋转180度
const candidates = getCardMoveCandidates(state, cardName, player); const candidates = getCardMoveCandidates(state, cardName, player);
// 对于每个棋子 // 对于每个棋子
for (const pawn of playerPawns) { for (const pawn of playerPawns) {
const [fromX, fromY] = pawn.position; const [fromX, fromY] = pawn.position;
// 对于卡牌的每个移动 // 对于卡牌的每个移动
for (const move of candidates) { for (const move of candidates) {
const toX = fromX + move.dx; const toX = fromX + move.dx;
const toY = fromY + move.dy; const toY = fromY + move.dy;
// 检查移动是否合法 // 检查移动是否合法
if (isInBounds(toX, toY)) { if (isInBounds(toX, toY)) {
const targetPawn = getPawnAtPosition(state, toX, toY); const targetPawn = getPawnAtPosition(state, toX, toY);
// 目标位置为空或有敌方棋子 // 目标位置为空或有敌方棋子
if (!targetPawn || targetPawn.owner !== player) { if (!targetPawn || targetPawn.owner !== player) {
moves.push({ card: cardName, fromX, fromY, toX, toY }); moves.push({ card: cardName, fromX, fromY, toX, toY });
} }
}
}
} }
}
} }
}
return moves; return moves;
} }
/** /**
* *
*/ */
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);
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({ let moveOutput;
schema: 'turn <player>',
run: handleTurn 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 };
}
/** /**
* *
*/ */
export async function start(game: OnitamaGame) { export async function start(game: OnitamaGame) {
// Initialize cards with RNG at game start // Initialize cards with RNG at game start
initializeCards(game); initializeCards(game);
while (true) { while (true) {
const currentPlayer = game.value.currentPlayer; const currentPlayer = game.value.currentPlayer;
const turnOutput = await turn(game, currentPlayer); const turnOutput = await turn(game, currentPlayer);
if (turnOutput.winner) { if (turnOutput.winner) {
break; break;
}
} }
}
return game.value; return game.value;
} }

View File

@ -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,34 +90,35 @@ 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,
const { player, row, col } = await game.prompt( turnNumber: number,
prompts.play, ) {
(player, row, col) => { const { player, row, col } = await game.prompt(
if (player !== turnPlayer) { prompts.play,
throw `Invalid player: ${player}. Expected ${turnPlayer}.`; (player, row, col) => {
} else if (!isValidMove(row, col)) { if (player !== turnPlayer) {
throw `Invalid position: (${row}, ${col}). Must be between 0 and ${BOARD_SIZE - 1}.`; throw `Invalid player: ${player}. Expected ${turnPlayer}.`;
} else if (isCellOccupied(game, row, col)) { } else if (!isValidMove(row, col)) {
throw `Cell (${row}, ${col}) is already occupied.`; throw `Invalid position: (${row}, ${col}). Must be between 0 and ${BOARD_SIZE - 1}.`;
} else { } else if (isCellOccupied(game, row, col)) {
return { player, row, col }; throw `Cell (${row}, ${col}) is already occupied.`;
} } else {
}, return { player, row, col };
game.value.currentPlayer, }
); },
game.value.currentPlayer,
);
placePiece(game, row, col, turnPlayer); placePiece(game, row, col, turnPlayer);
const winner = checkWinner(game); const winner = checkWinner(game);
if (winner) return { winner }; if (winner) return { winner };
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 (