fix: api change for bop

This commit is contained in:
hypercross 2026-04-04 23:25:43 +08:00
parent 61857b8256
commit de4e83e4ea
9 changed files with 95 additions and 52 deletions

View File

@ -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 <row:number> <col:number> <player
async function boop(game: BoopGame, row: number, col: number, type: PieceType) {
const booped: string[] = [];
await game.produceAsync(state => {
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 <player> <row:number> <col:number>',
(command) => {
'play <player> <row:number> <col:number> [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 <player> <row:number> <col:number> [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 <player>', turn);
const turnCommand = registry.register('turn <player>', turn);
export const commands = {
play: (player: PlayerType, row: number, col: number, type: PieceType) => {
return `play ${player} ${row} ${col} ${type}`;
}
};

View File

@ -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<BoopPartMeta>;
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<string, BoopPart>;
// Initialize region childIds

View File

@ -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){

View File

@ -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<BoopState>,
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);
}
});

View File

@ -30,9 +30,11 @@ export class GameScene extends GameHostScene<BoopState> {
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(() => {

View File

@ -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<BoopPart, Phaser.GameObjects.Container>
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;

View File

@ -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})`
);
// 自动切换到可用类型

View File

@ -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);

View File

@ -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 {