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 { import {
BOARD_SIZE, BOARD_SIZE,
BoopState, BoopState,
BoopPart,
PieceType, PieceType,
PlayerType, PlayerType,
WinnerType, WinnerType,
WIN_LENGTH, WIN_LENGTH,
MAX_PIECES_PER_PLAYER, BoopGame MAX_PIECES_PER_PLAYER,
BoopGame,
} from "./data"; } from "./data";
import {createGameCommandRegistry} from "@/core/game"; import {createGameCommandRegistry, Command, moveToRegion} from "boardgame-core";
import {moveToRegion} from "@/core/region";
import { import {
findPartAtPosition, findPartAtPosition,
findPartInRegion, findPartInRegion,
@ -34,7 +35,7 @@ async function place(game: BoopGame, row: number, col: number, player: PlayerTyp
const partId = part.id; const partId = part.id;
await game.produceAsync(state => { await game.produceAsync((state: BoopState) => {
// 将棋子从supply移动到棋盘 // 将棋子从supply移动到棋盘
const part = state.pieces[partId]; const part = state.pieces[partId];
moveToRegion(part, state.regions[player], state.regions.board, [row, col]); 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) { async function boop(game: BoopGame, row: number, col: number, type: PieceType) {
const booped: string[] = []; const booped: string[] = [];
await game.produceAsync(state => { await game.produceAsync((state: BoopState) => {
// 按照远离放置位置的方向推动 // 按照远离放置位置的方向推动
for (const [dr, dc] of getNeighborPositions()) { for (const [dr, dc] of getNeighborPositions()) {
const nr = row + dr; 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){ for(const partId of toUpgrade){
const part = state.pieces[partId]; const part = state.pieces[partId];
const [row, col] = part.position; const [row, col] = part.position;
@ -153,7 +154,7 @@ async function setup(game: BoopGame) {
const turnOutput = await turnCommand(game, currentPlayer); const turnOutput = await turnCommand(game, currentPlayer);
if (!turnOutput.success) throw new Error(turnOutput.error); if (!turnOutput.success) throw new Error(turnOutput.error);
await game.produceAsync(state => { await game.produceAsync((state: BoopState) => {
state.winner = turnOutput.result.winner; state.winner = turnOutput.result.winner;
if (!state.winner) { if (!state.winner) {
state.currentPlayer = state.currentPlayer === 'white' ? 'black' : 'white'; state.currentPlayer = state.currentPlayer === 'white' ? 'black' : 'white';
@ -169,15 +170,15 @@ registry.register('setup', setup);
async function checkFullBoard(game: BoopGame, turnPlayer: PlayerType){ async function checkFullBoard(game: BoopGame, turnPlayer: PlayerType){
// 检查8-piece规则: 如果玩家所有8个棋子都在棋盘上且没有获胜,强制升级一个小猫 // 检查8-piece规则: 如果玩家所有8个棋子都在棋盘上且没有获胜,强制升级一个小猫
const playerPieces = Object.values(game.value.pieces).filter( 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){ if(playerPieces.length < MAX_PIECES_PER_PLAYER || game.value.winner !== null){
return; return;
} }
const partId = await game.prompt( const partId = await game.prompt(
'choose <player> <row:number> <col:number>', 'play <player> <row:number> <col:number> [type:string]',
(command) => { (command: Command) => {
const [player, row, col] = command.params as [PlayerType, number, number]; const [player, row, col] = command.params as [PlayerType, number, number];
if (player !== turnPlayer) { if (player !== turnPlayer) {
throw `Invalid player: ${player}. Expected ${turnPlayer}.`; throw `Invalid player: ${player}. Expected ${turnPlayer}.`;
@ -185,17 +186,17 @@ async function checkFullBoard(game: BoopGame, turnPlayer: PlayerType){
if (!isInBounds(row, col)) { if (!isInBounds(row, col)) {
throw `Invalid position: (${row}, ${col}). Must be between 0 and ${BOARD_SIZE - 1}.`; throw `Invalid position: (${row}, ${col}). Must be between 0 and ${BOARD_SIZE - 1}.`;
} }
const part = findPartAtPosition(game, row, col); const part = findPartAtPosition(game, row, col);
if (!part || part.player !== turnPlayer) { if (!part || part.player !== turnPlayer) {
throw `No ${player} piece at (${row}, ${col}).`; throw `No ${player} piece at (${row}, ${col}).`;
} }
return part.id; return part.id;
} }
); );
await game.produceAsync(state => { await game.produceAsync((state: BoopState) => {
const part = state.pieces[partId]; const part = state.pieces[partId];
moveToRegion(part, state.regions.board, null); moveToRegion(part, state.regions.board, null);
const cat = findPartInRegion(state, '', 'cat'); const cat = findPartInRegion(state, '', 'cat');
@ -206,7 +207,7 @@ async function checkFullBoard(game: BoopGame, turnPlayer: PlayerType){
async function turn(game: BoopGame, turnPlayer: PlayerType) { async function turn(game: BoopGame, turnPlayer: PlayerType) {
const {row, col, type} = await game.prompt( const {row, col, type} = await game.prompt(
'play <player> <row:number> <col:number> [type:string]', 'play <player> <row:number> <col:number> [type:string]',
(command) => { (command: Command) => {
const [player, row, col, type] = command.params as [PlayerType, number, number, PieceType?]; const [player, row, col, type] = command.params as [PlayerType, number, number, PieceType?];
const pieceType = type === 'cat' ? 'cat' : 'kitten'; const pieceType = type === 'cat' ? 'cat' : 'kitten';
@ -239,4 +240,9 @@ async function turn(game: BoopGame, turnPlayer: PlayerType) {
await checkFullBoard(game, turnPlayer); await checkFullBoard(game, turnPlayer);
return { winner: null }; 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 parts from './parts.csv';
import {createRegion, moveToRegion, Region} from "@/core/region"; import {createRegion, moveToRegion, Region} from "boardgame-core";
import {createPartsFromTable} from "@/core/part-factory"; import {createPartsFromTable} from "boardgame-core";
import {Part} from "@/core/part"; import {Part} from "boardgame-core";
import {IGameContext} from "@/core/game"; import {IGameContext} from "boardgame-core";
export const BOARD_SIZE = 6; export const BOARD_SIZE = 6;
export const MAX_PIECES_PER_PLAYER = 8; export const MAX_PIECES_PER_PLAYER = 8;
@ -18,8 +18,8 @@ export type BoopPart = Part<BoopPartMeta>;
export function createInitialState() { export function createInitialState() {
const pieces = createPartsFromTable( const pieces = createPartsFromTable(
parts, parts,
(item, index) => `${item.player}-${item.type}-${index + 1}`, (item: {player: string, type: string}, index: number) => `${item.player}-${item.type}-${index + 1}`,
(item) => item.count (item: {count: number}) => item.count
) as Record<string, BoopPart>; ) as Record<string, BoopPart>;
// Initialize region childIds // Initialize region childIds

View File

@ -7,7 +7,7 @@
PlayerType, PlayerType,
RegionType, RegionType,
WIN_LENGTH WIN_LENGTH
} from "@/samples/boop/data"; } from "./data";
const DIRS = [ const DIRS = [
[0, 1], [0, 1],
@ -55,7 +55,7 @@ export function findPartInRegion(ctx: BoopGame | BoopState, regionId: keyof Boop
if(!regionId){ if(!regionId){
return Object.values(state.pieces).find(part => match(regionId, part, type, player)) || null; 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; return id ? state.pieces[id] || null : null;
} }
function match(regionId: RegionType, part: BoopPart, type: PieceType, player?: PlayerType){ function match(regionId: RegionType, part: BoopPart, type: PieceType, player?: PlayerType){

View File

@ -1,6 +1,5 @@
import Phaser from 'phaser'; import Phaser from 'phaser';
import type { BoopState, PlayerType } from '@/game'; import type { BoopState, PlayerType, BoopPart } from '@/game';
import type { ReadonlySignal } from '@preact/signals-core';
const BOARD_SIZE = 6; const BOARD_SIZE = 6;
const CELL_SIZE = 80; const CELL_SIZE = 80;
@ -70,18 +69,29 @@ export class BoardRenderer {
}).setOrigin(0.5); }).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 { updateTurnText(player: PlayerType, state: BoopState): void {
const current = player === 'white' ? state.players.white : state.players.black; const { kittensInSupply, catsInSupply } = this.countPieces(state, player);
const catsAvailable = current.catPool.remaining() + current.graduatedCount;
this.turnText.setText( this.turnText.setText(
`${player.toUpperCase()}'s turn | Kittens: ${current.kittenPool.remaining()} | Cats: ${catsAvailable}` `${player.toUpperCase()}'s turn | Kittens: ${kittensInSupply} | Cats: ${catsInSupply}`
); );
} }
setupInput( setupInput(
state: ReadonlySignal<BoopState>, getState: () => BoopState,
onCellClick: (row: number, col: number) => void onCellClick: (row: number, col: number) => void,
checkWinner: () => boolean
): void { ): void {
for (let row = 0; row < BOARD_SIZE; row++) { for (let row = 0; row < BOARD_SIZE; row++) {
for (let col = 0; col < BOARD_SIZE; col++) { 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(); const zone = this.scene.add.zone(x, y, CELL_SIZE, CELL_SIZE).setInteractive();
zone.on('pointerdown', () => { zone.on('pointerdown', () => {
const isOccupied = !!state.value.board.partMap[`${row},${col}`]; const state = getState();
if (!isOccupied && !state.value.winner) { const isOccupied = !!state.regions.board.partMap[`${row},${col}`];
if (!isOccupied && !checkWinner()) {
onCellClick(row, col); onCellClick(row, col);
} }
}); });

View File

@ -30,9 +30,11 @@ export class GameScene extends GameHostScene<BoopState> {
this.disposables.add(createPieceSpawner(this)); this.disposables.add(createPieceSpawner(this));
// 设置输入处理 // 设置输入处理
this.boardRenderer.setupInput(this.gameHost.state, (row, col) => { this.boardRenderer.setupInput(
this.handleCellClick(row, col); () => this.state,
}); (row, col) => this.handleCellClick(row, col),
() => !!this.state.winner
);
// 监听状态变化 // 监听状态变化
this.addEffect(() => { this.addEffect(() => {

View File

@ -1,5 +1,5 @@
import Phaser from 'phaser'; 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 { GameHostScene, spawnEffect, type Spawner } from 'boardgame-phaser';
import { BOARD_OFFSET, CELL_SIZE } from './BoardRenderer'; 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 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 baseColor = part.player === 'white' ? 0xffffff : 0x333333;
const strokeColor = part.player === 'white' ? 0x000000 : 0xffffff; const strokeColor = part.player === 'white' ? 0x000000 : 0xffffff;

View File

@ -1,5 +1,5 @@
import Phaser from 'phaser'; 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'; import { BOARD_OFFSET, CELL_SIZE, BOARD_SIZE } from './BoardRenderer';
export class PieceTypeSelector { export class PieceTypeSelector {
@ -58,23 +58,36 @@ export class PieceTypeSelector {
return this.selectedType; 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 { update(state: BoopState): void {
const currentPlayer = state.players[state.currentPlayer]; const currentPlayer = state.currentPlayer;
const kittenAvailable = currentPlayer.kittenPool.remaining() > 0; const { kittensInSupply, catsInSupply, catsOnBoard } = this.countPieces(state, currentPlayer);
const catsAvailable = currentPlayer.catPool.remaining() + currentPlayer.graduatedCount > 0;
const kittenAvailable = kittensInSupply > 0;
const catsAvailable = catsInSupply + catsOnBoard > 0;
this.updateButton( this.updateButton(
this.kittenButton, this.kittenButton,
kittenAvailable, kittenAvailable,
this.selectedType === 'kitten', this.selectedType === 'kitten',
`🐾 小猫 (${currentPlayer.kittenPool.remaining()})` `🐾 小猫 (${kittensInSupply})`
); );
this.updateButton( this.updateButton(
this.catButton, this.catButton,
catsAvailable, catsAvailable,
this.selectedType === 'cat', this.selectedType === 'cat',
`🐱 大猫 (${currentPlayer.catPool.remaining() + currentPlayer.graduatedCount})` `🐱 大猫 (${catsInSupply + catsOnBoard})`
); );
// 自动切换到可用类型 // 自动切换到可用类型

View File

@ -1,5 +1,5 @@
import Phaser from 'phaser'; 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'; import { BOARD_OFFSET, CELL_SIZE, BOARD_SIZE } from './BoardRenderer';
export class SupplyUI { export class SupplyUI {
@ -39,19 +39,30 @@ export class SupplyUI {
this.blackContainer.setDepth(100); this.blackContainer.setDepth(100);
} }
update(state: BoopState): void { private countPieces(state: BoopState, player: PlayerType) {
const white = state.players.white; const pieces = Object.values(state.pieces);
const black = state.players.black; 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; update(state: BoopState): void {
const blackCatsAvailable = black.catPool.remaining() + black.graduatedCount; 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( this.whiteText.setText(
`⚪ WHITE\n🐾 ${white.kittenPool.remaining()} | 🐱 ${whiteCatsAvailable}` `⚪ WHITE\n🐾 ${white.kittensInSupply} | 🐱 ${whiteCatsAvailable}`
); );
this.blackText.setText( this.blackText.setText(
`⚫ BLACK\n🐾 ${black.kittenPool.remaining()} | 🐱 ${blackCatsAvailable}` `⚫ BLACK\n🐾 ${black.kittensInSupply} | 🐱 ${blackCatsAvailable}`
); );
this.updateHighlight(state.currentPlayer); this.updateHighlight(state.currentPlayer);

View File

@ -1,5 +1,5 @@
import Phaser from 'phaser'; 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'; import { BOARD_OFFSET, CELL_SIZE, BOARD_SIZE } from './BoardRenderer';
export class WinnerOverlay { export class WinnerOverlay {