diff --git a/packages/boop-game/src/game/commands.ts b/packages/boop-game/src/game/commands.ts index 1d7d117..744a8d6 100644 --- a/packages/boop-game/src/game/commands.ts +++ b/packages/boop-game/src/game/commands.ts @@ -1,14 +1,15 @@ import { BOARD_SIZE, BoopState, + BoopPart, PieceType, PlayerType, WinnerType, WIN_LENGTH, - MAX_PIECES_PER_PLAYER, BoopGame + MAX_PIECES_PER_PLAYER, + BoopGame, } from "./data"; -import {createGameCommandRegistry} from "@/core/game"; -import {moveToRegion} from "@/core/region"; +import {createGameCommandRegistry, Command, moveToRegion} from "boardgame-core"; import { findPartAtPosition, findPartInRegion, @@ -34,7 +35,7 @@ async function place(game: BoopGame, row: number, col: number, player: PlayerTyp const partId = part.id; - await game.produceAsync(state => { + await game.produceAsync((state: BoopState) => { // 将棋子从supply移动到棋盘 const part = state.pieces[partId]; moveToRegion(part, state.regions[player], state.regions.board, [row, col]); @@ -50,7 +51,7 @@ const placeCommand = registry.register( 'place { + await game.produceAsync((state: BoopState) => { // 按照远离放置位置的方向推动 for (const [dr, dc] of getNeighborPositions()) { const nr = row + dr; @@ -133,7 +134,7 @@ async function checkGraduates(game: BoopGame){ } } - await game.produceAsync(state => { + await game.produceAsync((state: BoopState) => { for(const partId of toUpgrade){ const part = state.pieces[partId]; const [row, col] = part.position; @@ -153,7 +154,7 @@ async function setup(game: BoopGame) { const turnOutput = await turnCommand(game, currentPlayer); if (!turnOutput.success) throw new Error(turnOutput.error); - await game.produceAsync(state => { + await game.produceAsync((state: BoopState) => { state.winner = turnOutput.result.winner; if (!state.winner) { state.currentPlayer = state.currentPlayer === 'white' ? 'black' : 'white'; @@ -169,15 +170,15 @@ registry.register('setup', setup); 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' + (p: BoopPart) => p.player === turnPlayer && p.regionId === 'board' ); if(playerPieces.length < MAX_PIECES_PER_PLAYER || game.value.winner !== null){ return; } const partId = await game.prompt( - 'choose ', - (command) => { + 'play [type:string]', + (command: Command) => { const [player, row, col] = command.params as [PlayerType, number, number]; if (player !== turnPlayer) { throw `Invalid player: ${player}. Expected ${turnPlayer}.`; @@ -185,17 +186,17 @@ async function checkFullBoard(game: BoopGame, turnPlayer: PlayerType){ 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 => { + await game.produceAsync((state: BoopState) => { const part = state.pieces[partId]; moveToRegion(part, state.regions.board, null); const cat = findPartInRegion(state, '', 'cat'); @@ -206,7 +207,7 @@ async function checkFullBoard(game: BoopGame, turnPlayer: PlayerType){ async function turn(game: BoopGame, turnPlayer: PlayerType) { const {row, col, type} = await game.prompt( 'play [type:string]', - (command) => { + (command: Command) => { const [player, row, col, type] = command.params as [PlayerType, number, number, PieceType?]; const pieceType = type === 'cat' ? 'cat' : 'kitten'; @@ -239,4 +240,9 @@ async function turn(game: BoopGame, turnPlayer: PlayerType) { await checkFullBoard(game, turnPlayer); return { winner: null }; } -const turnCommand = registry.register('turn ', turn); \ No newline at end of file +const turnCommand = registry.register('turn ', turn); +export const commands = { + play: (player: PlayerType, row: number, col: number, type: PieceType) => { + return `play ${player} ${row} ${col} ${type}`; + } +}; \ No newline at end of file diff --git a/packages/boop-game/src/game/data.ts b/packages/boop-game/src/game/data.ts index fcb33ef..a957e28 100644 --- a/packages/boop-game/src/game/data.ts +++ b/packages/boop-game/src/game/data.ts @@ -1,8 +1,8 @@ import parts from './parts.csv'; -import {createRegion, moveToRegion, Region} from "@/core/region"; -import {createPartsFromTable} from "@/core/part-factory"; -import {Part} from "@/core/part"; -import {IGameContext} from "@/core/game"; +import {createRegion, moveToRegion, Region} from "boardgame-core"; +import {createPartsFromTable} from "boardgame-core"; +import {Part} from "boardgame-core"; +import {IGameContext} from "boardgame-core"; export const BOARD_SIZE = 6; export const MAX_PIECES_PER_PLAYER = 8; @@ -18,8 +18,8 @@ export type BoopPart = Part; export function createInitialState() { const pieces = createPartsFromTable( parts, - (item, index) => `${item.player}-${item.type}-${index + 1}`, - (item) => item.count + (item: {player: string, type: string}, index: number) => `${item.player}-${item.type}-${index + 1}`, + (item: {count: number}) => item.count ) as Record; // Initialize region childIds diff --git a/packages/boop-game/src/game/utils.ts b/packages/boop-game/src/game/utils.ts index e101f42..570ec8c 100644 --- a/packages/boop-game/src/game/utils.ts +++ b/packages/boop-game/src/game/utils.ts @@ -7,7 +7,7 @@ PlayerType, RegionType, WIN_LENGTH -} from "@/samples/boop/data"; +} from "./data"; const DIRS = [ [0, 1], @@ -55,7 +55,7 @@ export function findPartInRegion(ctx: BoopGame | BoopState, regionId: keyof Boop 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)); + const id = state.regions[regionId].childIds.find((id: string) => match(regionId, state.pieces[id], type, player)); return id ? state.pieces[id] || null : null; } function match(regionId: RegionType, part: BoopPart, type: PieceType, player?: PlayerType){ diff --git a/packages/boop-game/src/scenes/BoardRenderer.ts b/packages/boop-game/src/scenes/BoardRenderer.ts index d452fa4..e0a34e7 100644 --- a/packages/boop-game/src/scenes/BoardRenderer.ts +++ b/packages/boop-game/src/scenes/BoardRenderer.ts @@ -1,6 +1,5 @@ import Phaser from 'phaser'; -import type { BoopState, PlayerType } from '@/game'; -import type { ReadonlySignal } from '@preact/signals-core'; +import type { BoopState, PlayerType, BoopPart } from '@/game'; const BOARD_SIZE = 6; const CELL_SIZE = 80; @@ -70,18 +69,29 @@ export class BoardRenderer { }).setOrigin(0.5); } + private countPieces(state: BoopState, player: PlayerType) { + const pieces = Object.values(state.pieces); + const playerPieces = pieces.filter((p: BoopPart) => p.player === player); + + const kittensInSupply = playerPieces.filter((p: BoopPart) => p.type === 'kitten' && p.regionId === player).length; + const catsInSupply = playerPieces.filter((p: BoopPart) => p.type === 'cat' && p.regionId === player).length; + const piecesOnBoard = playerPieces.filter((p: BoopPart) => p.regionId === 'board').length; + + return { kittensInSupply, catsInSupply, piecesOnBoard }; + } + updateTurnText(player: PlayerType, state: BoopState): void { - const current = player === 'white' ? state.players.white : state.players.black; - const catsAvailable = current.catPool.remaining() + current.graduatedCount; + const { kittensInSupply, catsInSupply } = this.countPieces(state, player); this.turnText.setText( - `${player.toUpperCase()}'s turn | Kittens: ${current.kittenPool.remaining()} | Cats: ${catsAvailable}` + `${player.toUpperCase()}'s turn | Kittens: ${kittensInSupply} | Cats: ${catsInSupply}` ); } setupInput( - state: ReadonlySignal, - onCellClick: (row: number, col: number) => void + getState: () => BoopState, + onCellClick: (row: number, col: number) => void, + checkWinner: () => boolean ): void { for (let row = 0; row < BOARD_SIZE; row++) { for (let col = 0; col < BOARD_SIZE; col++) { @@ -91,8 +101,9 @@ export class BoardRenderer { const zone = this.scene.add.zone(x, y, CELL_SIZE, CELL_SIZE).setInteractive(); zone.on('pointerdown', () => { - const isOccupied = !!state.value.board.partMap[`${row},${col}`]; - if (!isOccupied && !state.value.winner) { + const state = getState(); + const isOccupied = !!state.regions.board.partMap[`${row},${col}`]; + if (!isOccupied && !checkWinner()) { onCellClick(row, col); } }); diff --git a/packages/boop-game/src/scenes/GameScene.ts b/packages/boop-game/src/scenes/GameScene.ts index 13d5fd6..944b0fd 100644 --- a/packages/boop-game/src/scenes/GameScene.ts +++ b/packages/boop-game/src/scenes/GameScene.ts @@ -30,9 +30,11 @@ export class GameScene extends GameHostScene { this.disposables.add(createPieceSpawner(this)); // 设置输入处理 - this.boardRenderer.setupInput(this.gameHost.state, (row, col) => { - this.handleCellClick(row, col); - }); + this.boardRenderer.setupInput( + () => this.state, + (row, col) => this.handleCellClick(row, col), + () => !!this.state.winner + ); // 监听状态变化 this.addEffect(() => { diff --git a/packages/boop-game/src/scenes/PieceSpawner.ts b/packages/boop-game/src/scenes/PieceSpawner.ts index a81880e..fcffa60 100644 --- a/packages/boop-game/src/scenes/PieceSpawner.ts +++ b/packages/boop-game/src/scenes/PieceSpawner.ts @@ -1,5 +1,5 @@ import Phaser from 'phaser'; -import type { BoopState, BoopPart } from '@/game/boop'; +import type { BoopState, BoopPart } from '@/game'; import { GameHostScene, spawnEffect, type Spawner } from 'boardgame-phaser'; import { BOARD_OFFSET, CELL_SIZE } from './BoardRenderer'; @@ -37,7 +37,7 @@ class BoopPartSpawner implements Spawner const container = this.scene.add.container(x, y); - const isCat = part.pieceType === 'cat'; + const isCat = part.type === 'cat'; const baseColor = part.player === 'white' ? 0xffffff : 0x333333; const strokeColor = part.player === 'white' ? 0x000000 : 0xffffff; diff --git a/packages/boop-game/src/scenes/PieceTypeSelector.ts b/packages/boop-game/src/scenes/PieceTypeSelector.ts index 2798fa9..4ccfc26 100644 --- a/packages/boop-game/src/scenes/PieceTypeSelector.ts +++ b/packages/boop-game/src/scenes/PieceTypeSelector.ts @@ -1,5 +1,5 @@ import Phaser from 'phaser'; -import type { BoopState, PlayerType, PieceType } from '@/game/boop'; +import type { BoopState, PlayerType, PieceType, BoopPart } from '@/game'; import { BOARD_OFFSET, CELL_SIZE, BOARD_SIZE } from './BoardRenderer'; export class PieceTypeSelector { @@ -58,23 +58,36 @@ export class PieceTypeSelector { return this.selectedType; } + private countPieces(state: BoopState, player: PlayerType) { + const pieces = Object.values(state.pieces); + const playerPieces = pieces.filter((p: BoopPart) => p.player === player); + + const kittensInSupply = playerPieces.filter((p: BoopPart) => p.type === 'kitten' && p.regionId === player).length; + const catsInSupply = playerPieces.filter((p: BoopPart) => p.type === 'cat' && p.regionId === player).length; + const catsOnBoard = playerPieces.filter((p: BoopPart) => p.type === 'cat' && p.regionId === 'board').length; + + return { kittensInSupply, catsInSupply, catsOnBoard }; + } + update(state: BoopState): void { - const currentPlayer = state.players[state.currentPlayer]; - const kittenAvailable = currentPlayer.kittenPool.remaining() > 0; - const catsAvailable = currentPlayer.catPool.remaining() + currentPlayer.graduatedCount > 0; + const currentPlayer = state.currentPlayer; + const { kittensInSupply, catsInSupply, catsOnBoard } = this.countPieces(state, currentPlayer); + + const kittenAvailable = kittensInSupply > 0; + const catsAvailable = catsInSupply + catsOnBoard > 0; this.updateButton( this.kittenButton, kittenAvailable, this.selectedType === 'kitten', - `🐾 小猫 (${currentPlayer.kittenPool.remaining()})` + `🐾 小猫 (${kittensInSupply})` ); this.updateButton( this.catButton, catsAvailable, this.selectedType === 'cat', - `🐱 大猫 (${currentPlayer.catPool.remaining() + currentPlayer.graduatedCount})` + `🐱 大猫 (${catsInSupply + catsOnBoard})` ); // 自动切换到可用类型 diff --git a/packages/boop-game/src/scenes/SupplyUI.ts b/packages/boop-game/src/scenes/SupplyUI.ts index e91027a..e26c7b4 100644 --- a/packages/boop-game/src/scenes/SupplyUI.ts +++ b/packages/boop-game/src/scenes/SupplyUI.ts @@ -1,5 +1,5 @@ import Phaser from 'phaser'; -import type { BoopState, PlayerType } from '@/game/boop'; +import type { BoopState, PlayerType, BoopPart } from '@/game'; import { BOARD_OFFSET, CELL_SIZE, BOARD_SIZE } from './BoardRenderer'; export class SupplyUI { @@ -39,19 +39,30 @@ export class SupplyUI { this.blackContainer.setDepth(100); } - update(state: BoopState): void { - const white = state.players.white; - const black = state.players.black; + private countPieces(state: BoopState, player: PlayerType) { + const pieces = Object.values(state.pieces); + const playerPieces = pieces.filter((p: BoopPart) => p.player === player); + + const kittensInSupply = playerPieces.filter((p: BoopPart) => p.type === 'kitten' && p.regionId === player).length; + const catsInSupply = playerPieces.filter((p: BoopPart) => p.type === 'cat' && p.regionId === player).length; + const catsOnBoard = playerPieces.filter((p: BoopPart) => p.type === 'cat' && p.regionId === 'board').length; + + return { kittensInSupply, catsInSupply, catsOnBoard }; + } - const whiteCatsAvailable = white.catPool.remaining() + white.graduatedCount; - const blackCatsAvailable = black.catPool.remaining() + black.graduatedCount; + update(state: BoopState): void { + const white = this.countPieces(state, 'white'); + const black = this.countPieces(state, 'black'); + + const whiteCatsAvailable = white.catsInSupply + white.catsOnBoard; + const blackCatsAvailable = black.catsInSupply + black.catsOnBoard; this.whiteText.setText( - `⚪ WHITE\n🐾 ${white.kittenPool.remaining()} | 🐱 ${whiteCatsAvailable}` + `⚪ WHITE\n🐾 ${white.kittensInSupply} | 🐱 ${whiteCatsAvailable}` ); this.blackText.setText( - `⚫ BLACK\n🐾 ${black.kittenPool.remaining()} | 🐱 ${blackCatsAvailable}` + `⚫ BLACK\n🐾 ${black.kittensInSupply} | 🐱 ${blackCatsAvailable}` ); this.updateHighlight(state.currentPlayer); diff --git a/packages/boop-game/src/scenes/WinnerOverlay.ts b/packages/boop-game/src/scenes/WinnerOverlay.ts index 428354e..4ecbbe1 100644 --- a/packages/boop-game/src/scenes/WinnerOverlay.ts +++ b/packages/boop-game/src/scenes/WinnerOverlay.ts @@ -1,5 +1,5 @@ import Phaser from 'phaser'; -import type { BoopState, WinnerType } from '@/game/boop'; +import type { BoopState, WinnerType } from '@/game'; import { BOARD_OFFSET, CELL_SIZE, BOARD_SIZE } from './BoardRenderer'; export class WinnerOverlay {