diff --git a/src/samples/onitama/commands.ts b/src/samples/onitama/commands.ts index 917fb44..78aaa6e 100644 --- a/src/samples/onitama/commands.ts +++ b/src/samples/onitama/commands.ts @@ -1,379 +1,409 @@ import { - OnitamaGame, - OnitamaState, - PlayerType, - prompts, - initializeCards + OnitamaGame, + OnitamaState, + PlayerType, + prompts, + initializeCards, } from "./types"; -import {createGameCommandRegistry} from "@/core/game"; -import {moveToRegion} from "@/core/region"; - -export const registry = createGameCommandRegistry(); +import { moveToRegion } from "@/core/region"; /** * 检查位置是否在棋盘范围内 */ 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) { - const key = `${x},${y}`; - const pawnId = state.regions.board.partMap[key]; - return pawnId ? state.pawns[pawnId] : null; + 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 playerHasCard( + state: OnitamaState, + player: PlayerType, + cardName: string, +): boolean { + const cardList = player === "red" ? state.redCards : state.blackCards; + return cardList.includes(cardName); } /** * 获取卡牌的移动候选项,根据玩家视角进行旋转 * 黑方需要将卡牌旋转180度 */ -export function getCardMoveCandidates(state: OnitamaState, cardName: string, player: PlayerType) { - const card = state.cards[cardName]; - const candidates = card.moveCandidates; +export function getCardMoveCandidates( + state: OnitamaState, + cardName: string, + player: PlayerType, +) { + const card = state.cards[cardName]; + const candidates = card.moveCandidates; - // 黑方需要将卡牌旋转180度 - if (player === 'black') { - return candidates.map(m => ({ dx: -m.dx, dy: -m.dy })); - } + // 黑方需要将卡牌旋转180度 + if (player === "black") { + 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 { - // 检查玩家是否拥有该卡牌 - if (!playerHasCard(state, player, cardName)) { - return `玩家 ${player} 不拥有卡牌 ${cardName}`; - } +export 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 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 card = state.cards[cardName]; + if (!card) { + return `卡牌 ${cardName} 不存在`; + } - // 计算移动偏移量 - const dx = toX - fromX; - const dy = toY - fromY; + // 计算移动偏移量 + const dx = toX - fromX; + const dy = toY - fromY; - // 检查移动是否在卡牌的移动候选项中(黑方需要旋转180度) - const candidates = getCardMoveCandidates(state, cardName, player); - const isValid = candidates.some(m => m.dx === dx && m.dy === dy); - if (!isValid) { - return `卡牌 ${cardName} 不支持移动 (${dx}, ${dy})`; - } + // 检查移动是否在卡牌的移动候选项中(黑方需要旋转180度) + const candidates = getCardMoveCandidates(state, cardName, player); + const isValid = candidates.some((m) => m.dx === dx && m.dy === dy); + if (!isValid) { + return `卡牌 ${cardName} 不支持移动 (${dx}, ${dy})`; + } - // 检查目标位置是否在棋盘内 - if (!isInBounds(toX, toY)) { - return `目标位置 (${toX}, ${toY}) 超出棋盘范围`; - } + // 检查目标位置是否在棋盘内 + if (!isInBounds(toX, toY)) { + return `目标位置 (${toX}, ${toY}) 超出棋盘范围`; + } - // 检查目标位置是否有己方棋子 - const toPawn = getPawnAtPosition(state, toX, toY); - if (toPawn && toPawn.owner === player) { - return `目标位置 (${toX}, ${toY}) 已有己方棋子`; - } + // 检查目标位置是否有己方棋子 + const toPawn = getPawnAtPosition(state, toX, toY); + if (toPawn && toPawn.owner === player) { + return `目标位置 (${toX}, ${toY}) 已有己方棋子`; + } - return null; + 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 - }; -} +async function move( + game: OnitamaGame, + player: PlayerType, + cardName: string, + fromX: number, + fromY: number, + toX: number, + toY: number, +) { + const state = game.value; -const move = registry.register({ - schema: 'move ', - run: handleMove -}); + // 验证移动 + 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 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) { - 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; - }); -} +async function swapCard( + game: OnitamaGame, + player: PlayerType, + usedCard: string, +) { + await game.produceAsync((state) => { + const spareCard = state.spareCard; + const usedCardData = state.cards[usedCard]; + const spareCardData = state.cards[spareCard]; -const swapCard = registry.register({ - schema: 'swap-card ', - run: handleSwapCard -}); + // 从玩家手牌中移除使用的卡牌 + 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; + }); +} /** * 检查占领胜利条件:玩家的师父棋子到达对手的初始位置 * 红色师父需要到达 (2, 4) - 黑色师父的初始位置 * 黑色师父需要到达 (2, 0) - 红色师父的初始位置 */ -async function handleCheckConquestWin(game: OnitamaGame): Promise { - const state = game.value; +async function checkConquestWin(game: OnitamaGame): Promise { + const state = game.value; - // 红色师父到达 (2, 4)(黑色师父的初始位置) - const redMaster = state.pawns['red-master']; - if (redMaster && redMaster.regionId === 'board' && redMaster.position[0] === 2 && redMaster.position[1] === 4) { - return 'red'; - } + // 红色师父到达 (2, 4)(黑色师父的初始位置) + const redMaster = state.pawns["red-master"]; + if ( + redMaster && + redMaster.regionId === "board" && + redMaster.position[0] === 2 && + redMaster.position[1] === 4 + ) { + return "red"; + } - // 黑色师父到达 (2, 0)(红色师父的初始位置) - const blackMaster = state.pawns['black-master']; - if (blackMaster && blackMaster.regionId === 'board' && blackMaster.position[0] === 2 && blackMaster.position[1] === 0) { - return 'black'; - } + // 黑色师父到达 (2, 0)(红色师父的初始位置) + const blackMaster = state.pawns["black-master"]; + if ( + 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 { - 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; -} +async function checkCaptureWin(game: OnitamaGame): Promise { + const state = game.value; -const checkCaptureWin = registry.register({ - schema: 'check-capture-win', - run: handleCheckCaptureWin -}); + // 红色师父不在棋盘上,黑色获胜 + 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; +} /** * 综合胜利检测 */ -async function handleCheckWin(game: OnitamaGame): Promise { - const conquestWinner = await handleCheckConquestWin(game); - if (conquestWinner) { - return conquestWinner; - } - - const captureWinner = await handleCheckCaptureWin(game); - if (captureWinner) { - return captureWinner; - } - - return null; -} +async function checkWin(game: OnitamaGame): Promise { + const conquestWinner = await checkConquestWin(game); + if (conquestWinner) { + return conquestWinner; + } -const checkWin = registry.register({ - schema: 'check-win', - run: handleCheckWin -}); + const captureWinner = await checkCaptureWin(game); + if (captureWinner) { + return captureWinner; + } + + return null; +} /** * 获取玩家可用的移动 */ -export 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}> = []; +export 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 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) { - // 获取旋转后的移动候选项(黑方需要旋转180度) - const candidates = getCardMoveCandidates(state, cardName, player); + // 对于每张卡牌 + for (const cardName of cardNames) { + // 获取旋转后的移动候选项(黑方需要旋转180度) + const candidates = getCardMoveCandidates(state, cardName, player); - // 对于每个棋子 - for (const pawn of playerPawns) { - const [fromX, fromY] = pawn.position; + // 对于每个棋子 + for (const pawn of playerPawns) { + const [fromX, fromY] = pawn.position; - // 对于卡牌的每个移动 - for (const move of candidates) { - const toX = fromX + move.dx; - const toY = fromY + move.dy; + // 对于卡牌的每个移动 + for (const move of candidates) { + 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 }); - } - } - } + // 检查移动是否合法 + 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; + 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 }; -} +async function turn(game: OnitamaGame, turnPlayer: PlayerType) { + const state = game.value; + const availableMoves = getAvailableMoves(state, turnPlayer); -const turn = registry.register({ - schema: 'turn ', - run: handleTurn -}); + 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 }; +} /** * 开始游戏主循环 */ export async function start(game: OnitamaGame) { - // Initialize cards with RNG at game start - initializeCards(game); + // Initialize cards with RNG at game start + initializeCards(game); - while (true) { - const currentPlayer = game.value.currentPlayer; - const turnOutput = await turn(game, currentPlayer); + while (true) { + const currentPlayer = game.value.currentPlayer; + const turnOutput = await turn(game, currentPlayer); - if (turnOutput.winner) { - break; - } + if (turnOutput.winner) { + break; } + } - return game.value; + return game.value; } diff --git a/src/samples/tic-tac-toe.ts b/src/samples/tic-tac-toe.ts index 74e78b4..72e91b2 100644 --- a/src/samples/tic-tac-toe.ts +++ b/src/samples/tic-tac-toe.ts @@ -1,10 +1,6 @@ import { Part } from "@/core/part"; import { createRegion } from "@/core/region"; -import { - createGameCommandRegistry, - createPromptDef, - IGameContext, -} from "@/core/game"; +import { createPromptDef, IGameContext } from "@/core/game"; const BOARD_SIZE = 3; const MAX_TURNS = BOARD_SIZE * BOARD_SIZE; @@ -69,7 +65,6 @@ export function createInitialState() { } export type TicTacToeState = ReturnType; export type TicTacToeGame = IGameContext; -export const registry = createGameCommandRegistry(); export const prompts = { play: createPromptDef<[PlayerType, number, number]>( "play ", @@ -95,34 +90,35 @@ export async function start(game: TicTacToeGame) { return game.value; } -const turn = registry.register({ - schema: "turn ", - async run(game: TicTacToeGame, turnPlayer: PlayerType, turnNumber: number) { - const { player, row, col } = await game.prompt( - prompts.play, - (player, row, col) => { - if (player !== turnPlayer) { - throw `Invalid player: ${player}. Expected ${turnPlayer}.`; - } else if (!isValidMove(row, col)) { - throw `Invalid position: (${row}, ${col}). Must be between 0 and ${BOARD_SIZE - 1}.`; - } else if (isCellOccupied(game, row, col)) { - throw `Cell (${row}, ${col}) is already occupied.`; - } else { - return { player, row, col }; - } - }, - game.value.currentPlayer, - ); +async function turn( + game: TicTacToeGame, + turnPlayer: PlayerType, + turnNumber: number, +) { + const { player, row, col } = await game.prompt( + prompts.play, + (player, row, col) => { + if (player !== turnPlayer) { + throw `Invalid player: ${player}. Expected ${turnPlayer}.`; + } else if (!isValidMove(row, col)) { + throw `Invalid position: (${row}, ${col}). Must be between 0 and ${BOARD_SIZE - 1}.`; + } else if (isCellOccupied(game, row, col)) { + throw `Cell (${row}, ${col}) is already occupied.`; + } else { + return { player, row, col }; + } + }, + game.value.currentPlayer, + ); - placePiece(game, row, col, turnPlayer); + placePiece(game, row, col, turnPlayer); - const winner = checkWinner(game); - if (winner) return { winner }; - if (turnNumber >= MAX_TURNS) return { winner: "draw" as WinnerType }; + const winner = checkWinner(game); + if (winner) return { winner }; + if (turnNumber >= MAX_TURNS) return { winner: "draw" as WinnerType }; - return { winner: null }; - }, -}); + return { winner: null }; +} function isValidMove(row: number, col: number): boolean { return (