From 91c993b223f05cb4a6e8dd6e72c68e4efbca4529 Mon Sep 17 00:00:00 2001 From: hypercross Date: Tue, 7 Apr 2026 15:43:17 +0800 Subject: [PATCH] refactor: clean up boop implementation structure --- src/samples/boop/commands.ts | 259 ------------------ src/samples/boop/commands/boop.ts | 46 ++++ src/samples/boop/commands/full-board.ts | 45 +++ src/samples/boop/commands/graduate.ts | 40 +++ src/samples/boop/commands/index.ts | 101 +++++++ src/samples/boop/commands/place.ts | 27 ++ src/samples/boop/commands/turn.ts | 49 ++++ src/samples/boop/commands/win.ts | 27 ++ src/samples/boop/constants.ts | 3 + src/samples/boop/index.ts | 51 +++- src/samples/boop/prompts.ts | 9 + src/samples/boop/{data.ts => state.ts} | 33 +-- src/samples/boop/types-extensions.ts | 4 + src/samples/boop/types.ts | 9 + src/samples/boop/{utils.ts => utils/board.ts} | 46 ++-- src/samples/boop/utils/index.ts | 11 + src/samples/boop/utils/pieces.ts | 40 +++ tests/samples/boop-utils.test.ts | 2 +- 18 files changed, 485 insertions(+), 317 deletions(-) delete mode 100644 src/samples/boop/commands.ts create mode 100644 src/samples/boop/commands/boop.ts create mode 100644 src/samples/boop/commands/full-board.ts create mode 100644 src/samples/boop/commands/graduate.ts create mode 100644 src/samples/boop/commands/index.ts create mode 100644 src/samples/boop/commands/place.ts create mode 100644 src/samples/boop/commands/turn.ts create mode 100644 src/samples/boop/commands/win.ts create mode 100644 src/samples/boop/constants.ts create mode 100644 src/samples/boop/prompts.ts rename src/samples/boop/{data.ts => state.ts} (59%) create mode 100644 src/samples/boop/types-extensions.ts create mode 100644 src/samples/boop/types.ts rename src/samples/boop/{utils.ts => utils/board.ts} (52%) create mode 100644 src/samples/boop/utils/index.ts create mode 100644 src/samples/boop/utils/pieces.ts diff --git a/src/samples/boop/commands.ts b/src/samples/boop/commands.ts deleted file mode 100644 index c062e2c..0000000 --- a/src/samples/boop/commands.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { - BOARD_SIZE, - BoopState, - PieceType, - PlayerType, - WinnerType, - WIN_LENGTH, - MAX_PIECES_PER_PLAYER, - BoopGame, - prompts -} 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(); - -/** - * 放置棋子到棋盘 - */ -async function handlePlace(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 place = registry.register({ - schema: 'place ', - run: handlePlace -}); - -/** - * 执行boop - 推动周围棋子 - */ -async function handleBoop(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 boop = registry.register({ - schema: 'boop ', - run: handleBoop -}); - -/** - * 检查是否有玩家获胜(三个猫连线) - */ -async function handleCheckWin(game: BoopGame): Promise { - 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 checkWin = registry.register({ - schema: 'check-win', - run: handleCheckWin -}); - -/** - * 检查并执行小猫升级(三个小猫连线变成猫) - */ -async function handleCheckGraduates(game: BoopGame){ - const toUpgrade = new Set(); - 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 checkGraduates = registry.register({ - schema: 'check-graduates', - run: handleCheckGraduates -}); - -export async function start(game: BoopGame) { - while (true) { - const currentPlayer = game.value.currentPlayer; - const turnOutput = await turn(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; -} - -async function handleCheckFullBoard(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( - prompts.choose, - (player, row, col) => { - 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]); - }); -} -const checkFullBoard = registry.register({ - schema: 'check-full-board ', - run: handleCheckFullBoard -}); - -async function handleTurn(game: BoopGame, turnPlayer: PlayerType) { - const {row, col, type} = await game.prompt( - prompts.play, - (player, row, col, type) => { - 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 place(game, row, col, turnPlayer, pieceType); - await boop(game, row, col, pieceType); - const winner = await checkWin(game); - if(winner) return { winner: winner }; - - await checkGraduates(game); - await checkFullBoard(game, turnPlayer); - return { winner: null }; -} -const turn = registry.register({ - schema: 'turn ', - run: handleTurn -}); \ No newline at end of file diff --git a/src/samples/boop/commands/boop.ts b/src/samples/boop/commands/boop.ts new file mode 100644 index 0000000..e93b32b --- /dev/null +++ b/src/samples/boop/commands/boop.ts @@ -0,0 +1,46 @@ +import {BoopGame} from "@/samples/boop/types-extensions"; +import {PieceType} from "@/samples/boop/types"; +import {findPartAtPosition, getNeighborPositions, isCellOccupied, isInBounds} from "@/samples/boop/utils"; +import {moveToRegion} from "@/core/region"; + +/** + * 执行boop - 推动周围棋子 + */ +export 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 }; +} diff --git a/src/samples/boop/commands/full-board.ts b/src/samples/boop/commands/full-board.ts new file mode 100644 index 0000000..03d31b9 --- /dev/null +++ b/src/samples/boop/commands/full-board.ts @@ -0,0 +1,45 @@ +import {BoopGame} from "@/samples/boop/types-extensions"; +import {PlayerType} from "@/samples/boop/types"; +import {findPartAtPosition, findPartInRegion, isInBounds} from "@/samples/boop/utils"; +import {MAX_PIECES_PER_PLAYER} from "@/samples/boop/constants"; +import {prompts} from "@/samples/boop/prompts"; +import {moveToRegion} from "@/core/region"; + +/** + * 检查并执行 8-piece 规则 + */ +export 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( + prompts.choose, + (player, row, col) => { + if (player !== turnPlayer) { + throw `Invalid player: ${player}. Expected ${turnPlayer}.`; + } + if (!isInBounds(row, col)) { + throw `Invalid position: (${row}, ${col}). Must be between 0 and 5.`; + } + + 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]); + }); +} diff --git a/src/samples/boop/commands/graduate.ts b/src/samples/boop/commands/graduate.ts new file mode 100644 index 0000000..5cd6911 --- /dev/null +++ b/src/samples/boop/commands/graduate.ts @@ -0,0 +1,40 @@ +import {BoopGame} from "@/samples/boop/types-extensions"; +import {PlayerType} from "@/samples/boop/types"; +import {findPartAtPosition, findPartInRegion, getLineCandidates} from "@/samples/boop/utils"; +import {WIN_LENGTH} from "@/samples/boop/constants"; +import {moveToRegion} from "@/core/region"; + +/** + * 检查并执行小猫升级(三个小猫连线变成猫) + */ +export async function checkGraduates(game: BoopGame){ + const toUpgrade = new Set(); + 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]); + } + }); +} diff --git a/src/samples/boop/commands/index.ts b/src/samples/boop/commands/index.ts new file mode 100644 index 0000000..47df61c --- /dev/null +++ b/src/samples/boop/commands/index.ts @@ -0,0 +1,101 @@ +import {BoopGame} from "@/samples/boop/types-extensions"; +import {PlayerType} from "@/samples/boop/types"; +import {turn} from "@/samples/boop/commands/turn"; +import {createGameCommandRegistry} from "@/core/game"; + +export const registry = createGameCommandRegistry(); + +/** + * 放置棋子到棋盘 + */ +const placeCmd = registry.register({ + schema: 'place ', + run: async (game, row, col, player, type) => { + const {place} = await import('./place'); + return place(game, row, col, player, type); + } +}); + +/** + * 执行boop - 推动周围棋子 + */ +const boopCmd = registry.register({ + schema: 'boop ', + run: async (game, row, col, type) => { + const {boop} = await import('./boop'); + return boop(game, row, col, type); + } +}); + +/** + * 检查是否有玩家获胜(三个猫连线) + */ +const checkWinCmd = registry.register({ + schema: 'check-win', + run: async (game) => { + const {checkWin} = await import('./win'); + return checkWin(game); + } +}); + +/** + * 检查并执行小猫升级(三个小猫连线变成猫) + */ +const checkGraduatesCmd = registry.register({ + schema: 'check-graduates', + run: async (game) => { + const {checkGraduates} = await import('./graduate'); + return checkGraduates(game); + } +}); + +/** + * 检查并执行 8-piece 规则 + */ +const checkFullBoardCmd = registry.register({ + schema: 'check-full-board ', + run: async (game, player) => { + const {checkFullBoard} = await import('./full-board'); + return checkFullBoard(game, player); + } +}); + +/** + * 处理一个回合 + */ +const turnCmd = registry.register({ + schema: 'turn ', + run: async (game, player) => { + const {turn} = await import('./turn'); + return turn(game, player); + } +}); + +/** + * 启动游戏主循环 + */ +export async function start(game: BoopGame) { + while (true) { + const currentPlayer = game.value.currentPlayer; + const turnOutput = await turn(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; +} + +export { + placeCmd as place, + boopCmd as boop, + checkWinCmd as checkWin, + checkGraduatesCmd as checkGraduates, + checkFullBoardCmd as checkFullBoard, + turnCmd as turn +}; diff --git a/src/samples/boop/commands/place.ts b/src/samples/boop/commands/place.ts new file mode 100644 index 0000000..3058ec5 --- /dev/null +++ b/src/samples/boop/commands/place.ts @@ -0,0 +1,27 @@ +import {BoopGame} from "@/samples/boop/types-extensions"; +import {PieceType, PlayerType} from "@/samples/boop/types"; +import {findPartInRegion} from "@/samples/boop/utils"; +import {moveToRegion} from "@/core/region"; + +/** + * 放置棋子到棋盘 + */ +export 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 }; +} diff --git a/src/samples/boop/commands/turn.ts b/src/samples/boop/commands/turn.ts new file mode 100644 index 0000000..a5a4bee --- /dev/null +++ b/src/samples/boop/commands/turn.ts @@ -0,0 +1,49 @@ +import {BoopGame} from "@/samples/boop/types-extensions"; +import {PieceType, PlayerType, WinnerType} from "@/samples/boop/types"; +import {prompts} from "@/samples/boop/prompts"; +import {findPartInRegion, isCellOccupied, isInBounds} from "@/samples/boop/utils"; +import {place} from "@/samples/boop/commands/place"; +import {boop} from "@/samples/boop/commands/boop"; +import {checkWin} from "@/samples/boop/commands/win"; +import {checkGraduates} from "@/samples/boop/commands/graduate"; +import {checkFullBoard} from "@/samples/boop/commands/full-board"; +import {BOARD_SIZE} from "@/samples/boop/constants"; + +/** + * 处理一个回合 + */ +export async function turn(game: BoopGame, turnPlayer: PlayerType) { + const {row, col, type} = await game.prompt( + prompts.play, + (player, row, col, type) => { + 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 place(game, row, col, turnPlayer, pieceType); + await boop(game, row, col, pieceType); + const winner = await checkWin(game); + if(winner) return { winner: winner }; + + await checkGraduates(game); + await checkFullBoard(game, turnPlayer); + return { winner: null }; +} diff --git a/src/samples/boop/commands/win.ts b/src/samples/boop/commands/win.ts new file mode 100644 index 0000000..e27196b --- /dev/null +++ b/src/samples/boop/commands/win.ts @@ -0,0 +1,27 @@ +import {BoopGame} from "@/samples/boop/types-extensions"; +import {PlayerType, WinnerType} from "@/samples/boop/types"; +import {findPartAtPosition, getLineCandidates} from "@/samples/boop/utils"; +import {WIN_LENGTH} from "@/samples/boop/constants"; + +/** + * 检查是否有玩家获胜(三个猫连线) + */ +export async function checkWin(game: BoopGame): Promise { + 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; +} diff --git a/src/samples/boop/constants.ts b/src/samples/boop/constants.ts new file mode 100644 index 0000000..7e83cc6 --- /dev/null +++ b/src/samples/boop/constants.ts @@ -0,0 +1,3 @@ +export const BOARD_SIZE = 6; +export const MAX_PIECES_PER_PLAYER = 8; +export const WIN_LENGTH = 3; diff --git a/src/samples/boop/index.ts b/src/samples/boop/index.ts index 5800496..fca48ab 100644 --- a/src/samples/boop/index.ts +++ b/src/samples/boop/index.ts @@ -1,2 +1,49 @@ -export * from './data'; -export * from './commands'; \ No newline at end of file +// Types +export type { + PlayerType, + PieceType, + WinnerType, + RegionType, + BoopPartMeta, + BoopPart +} from './types'; + +export type {BoopGame} from './types-extensions'; + +// Constants +export { + BOARD_SIZE, + MAX_PIECES_PER_PLAYER, + WIN_LENGTH +} from './constants'; + +// State +export { + createInitialState, + type BoopState +} from './state'; + +// Prompts +export {prompts} from './prompts'; + +// Commands +export { + registry, + start, + place as placeCmd, + boop as boopCmd, + checkWin as checkWinCmd, + checkGraduates as checkGraduatesCmd, + checkFullBoard as checkFullBoardCmd, + turn as turnCmd +} from './commands'; + +// Utils +export { + getLineCandidates, + isInBounds, + isCellOccupied, + getNeighborPositions, + findPartInRegion, + findPartAtPosition +} from './utils'; diff --git a/src/samples/boop/prompts.ts b/src/samples/boop/prompts.ts new file mode 100644 index 0000000..25b5c8d --- /dev/null +++ b/src/samples/boop/prompts.ts @@ -0,0 +1,9 @@ +import {createPromptDef} from "@/core/game"; +import {PieceType, PlayerType} from "@/samples/boop/types"; + +export const prompts = { + play: createPromptDef<[PlayerType, number, number, PieceType?]>( + 'play [type:string]'), + choose: createPromptDef<[PlayerType, number, number]>( + 'choose ') +} diff --git a/src/samples/boop/data.ts b/src/samples/boop/state.ts similarity index 59% rename from src/samples/boop/data.ts rename to src/samples/boop/state.ts index bff32e7..530a9f4 100644 --- a/src/samples/boop/data.ts +++ b/src/samples/boop/state.ts @@ -1,25 +1,9 @@ -import parts from './parts.csv'; +import parts from './parts.csv'; import {createRegion, moveToRegion, Region} from "@/core/region"; import {createPartsFromTable} from "@/core/part-factory"; -import {Part} from "@/core/part"; -import {createPromptDef, IGameContext} from "@/core/game"; - -export const BOARD_SIZE = 6; -export const MAX_PIECES_PER_PLAYER = 8; -export const WIN_LENGTH = 3; - -export type PlayerType = 'white' | 'black'; -export type PieceType = 'kitten' | 'cat'; -export type WinnerType = PlayerType | 'draw' | null; -export type RegionType = 'white' | 'black' | 'board' | ''; -export type BoopPartMeta = { player: PlayerType; type: PieceType }; -export type BoopPart = Part; -export const prompts = { - play: createPromptDef<[PlayerType, number, number, PieceType?]>( - 'play [type:string]'), - choose: createPromptDef<[PlayerType, number, number]>( - 'choose ') -} +import {BoopPart} from "@/samples/boop/types"; +import {BOARD_SIZE} from "@/samples/boop/constants"; +import {PlayerType, PieceType, RegionType} from "@/samples/boop/types"; export function createInitialState() { const pieces = createPartsFromTable( @@ -27,7 +11,7 @@ export function createInitialState() { (item, index) => `${item.player}-${item.type}-${index + 1}`, (item) => item.count ) as Record; - + // Initialize region childIds const whiteRegion = createRegion('white', []); const blackRegion = createRegion('black', []); @@ -35,7 +19,7 @@ export function createInitialState() { { name: 'x', min: 0, max: BOARD_SIZE - 1 }, { name: 'y', min: 0, max: BOARD_SIZE - 1 }, ]); - + // Populate region childIds based on piece regionId for (const part of Object.values(pieces)) { if(part.type !== 'kitten') continue; @@ -45,7 +29,7 @@ export function createInitialState() { moveToRegion(part, null, blackRegion); } } - + return { regions: { white: whiteRegion, @@ -54,8 +38,7 @@ export function createInitialState() { } as Record, pieces, currentPlayer: 'white' as PlayerType, - winner: null as WinnerType, + winner: null as PlayerType | 'draw' | null, }; } export type BoopState = ReturnType; -export type BoopGame = IGameContext; \ No newline at end of file diff --git a/src/samples/boop/types-extensions.ts b/src/samples/boop/types-extensions.ts new file mode 100644 index 0000000..2f66545 --- /dev/null +++ b/src/samples/boop/types-extensions.ts @@ -0,0 +1,4 @@ +import {IGameContext} from "@/core/game"; +import {BoopState} from "@/samples/boop/state"; + +export type BoopGame = IGameContext; diff --git a/src/samples/boop/types.ts b/src/samples/boop/types.ts new file mode 100644 index 0000000..ed34539 --- /dev/null +++ b/src/samples/boop/types.ts @@ -0,0 +1,9 @@ +import {Part} from "@/core/part"; + +export type PlayerType = 'white' | 'black'; +export type PieceType = 'kitten' | 'cat'; +export type WinnerType = PlayerType | 'draw' | null; +export type RegionType = 'white' | 'black' | 'board' | ''; + +export type BoopPartMeta = { player: PlayerType; type: PieceType }; +export type BoopPart = Part; diff --git a/src/samples/boop/utils.ts b/src/samples/boop/utils/board.ts similarity index 52% rename from src/samples/boop/utils.ts rename to src/samples/boop/utils/board.ts index e101f42..7d703d4 100644 --- a/src/samples/boop/utils.ts +++ b/src/samples/boop/utils/board.ts @@ -1,13 +1,6 @@ -import { - BOARD_SIZE, - BoopGame, - BoopPart, - BoopState, - PieceType, - PlayerType, - RegionType, - WIN_LENGTH -} from "@/samples/boop/data"; +import {BOARD_SIZE, WIN_LENGTH} from "@/samples/boop/constants"; +import {BoopState} from "@/samples/boop/state"; +import {BoopGame} from "@/samples/boop/types-extensions"; const DIRS = [ [0, 1], @@ -17,6 +10,10 @@ const DIRS = [ ] type PT = [number, number]; type Line = PT[]; + +/** + * 生成所有可能的连线候选(用于检查胜利或升级) + */ export function* getLineCandidates(){ for(const [dx, dy] of DIRS){ for(let x = 0; x < BOARD_SIZE; x ++) @@ -38,11 +35,18 @@ export function isInBounds(x: number, y: number): boolean { return x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE; } +/** + * 检查单元格是否被占用 + */ export function isCellOccupied(game: BoopGame | BoopState, x: number, y: number): boolean { + const state = getState(game); const id = `${x},${y}`; - return getState(game).regions.board.partMap[id] !== undefined; + return state.regions.board.partMap[id] !== undefined; } +/** + * 获取邻居位置生成器 + */ export function* getNeighborPositions(x: number = 0, y: number = 0){ for(let dx = -1; dx <= 1; dx ++) for(let dy = -1; dy <= 1; dy ++) @@ -50,27 +54,9 @@ export function* getNeighborPositions(x: number = 0, y: number = 0){ yield [x + dx, y + dy] as PT; } -export function findPartInRegion(ctx: BoopGame | BoopState, regionId: keyof BoopGame['value']['regions'], type: PieceType, player?: PlayerType): BoopPart | null { - const state = getState(ctx); - if(!regionId){ - return Object.values(state.pieces).find(part => match(regionId, part, type, player)) || null; - } - const id = state.regions[regionId].childIds.find(id => match(regionId, state.pieces[id], type, player)); - return id ? state.pieces[id] || null : null; -} -function match(regionId: RegionType, part: BoopPart, type: PieceType, player?: PlayerType){ - return regionId === part.regionId && part.type === type && (!player || part.player === player); -} - -export function findPartAtPosition(ctx: BoopGame | BoopState, row: number, col: number): BoopPart | null { - const state = getState(ctx); - const id = state.regions.board.partMap[`${row},${col}`]; - return id ? state.pieces[id] || null : null; -} - function getState(ctx: BoopGame | BoopState): BoopState { if('value' in ctx){ return ctx.value; } return ctx; -} \ No newline at end of file +} diff --git a/src/samples/boop/utils/index.ts b/src/samples/boop/utils/index.ts new file mode 100644 index 0000000..23b33f3 --- /dev/null +++ b/src/samples/boop/utils/index.ts @@ -0,0 +1,11 @@ +export { + getLineCandidates, + isInBounds, + isCellOccupied, + getNeighborPositions +} from './board'; + +export { + findPartInRegion, + findPartAtPosition +} from './pieces'; diff --git a/src/samples/boop/utils/pieces.ts b/src/samples/boop/utils/pieces.ts new file mode 100644 index 0000000..ef46e11 --- /dev/null +++ b/src/samples/boop/utils/pieces.ts @@ -0,0 +1,40 @@ +import {BoopPart, PieceType, PlayerType, RegionType} from "@/samples/boop/types"; +import {BoopState} from "@/samples/boop/state"; +import {BoopGame} from "@/samples/boop/types-extensions"; + +/** + * 在区域中查找棋子 + */ +export function findPartInRegion( + ctx: BoopGame | BoopState, + regionId: keyof BoopGame['value']['regions'], + type: PieceType, + player?: PlayerType +): BoopPart | null { + const state = getState(ctx); + if(!regionId){ + return Object.values(state.pieces).find(part => match(regionId, part, type, player)) || null; + } + const id = state.regions[regionId].childIds.find(id => match(regionId, state.pieces[id], type, player)); + return id ? state.pieces[id] || null : null; +} + +/** + * 在指定位置查找棋子 + */ +export function findPartAtPosition(ctx: BoopGame | BoopState, row: number, col: number): BoopPart | null { + const state = getState(ctx); + const id = state.regions.board.partMap[`${row},${col}`]; + return id ? state.pieces[id] || null : null; +} + +function match(regionId: RegionType, part: BoopPart, type: PieceType, player?: PlayerType){ + return regionId === part.regionId && part.type === type && (!player || part.player === player); +} + +function getState(ctx: BoopGame | BoopState): BoopState { + if('value' in ctx){ + return ctx.value; + } + return ctx; +} diff --git a/tests/samples/boop-utils.test.ts b/tests/samples/boop-utils.test.ts index 7561131..d5f76b6 100644 --- a/tests/samples/boop-utils.test.ts +++ b/tests/samples/boop-utils.test.ts @@ -7,7 +7,7 @@ import { findPartInRegion, findPartAtPosition } from '@/samples/boop/utils'; -import { createInitialState, BOARD_SIZE, WIN_LENGTH } from '@/samples/boop/data'; +import { createInitialState, BOARD_SIZE, WIN_LENGTH } from '@/samples/boop'; import { createGameContext } from '@/core/game'; import { registry } from '@/samples/boop';