diff --git a/packages/sample-game/index.html b/packages/sample-game/index.html
index 163e2e2..ed50467 100644
--- a/packages/sample-game/index.html
+++ b/packages/sample-game/index.html
@@ -7,7 +7,6 @@
diff --git a/packages/sample-game/src/game/tic-tac-toe.ts b/packages/sample-game/src/game/tic-tac-toe.ts
index 8bfa206..82c8b71 100644
--- a/packages/sample-game/src/game/tic-tac-toe.ts
+++ b/packages/sample-game/src/game/tic-tac-toe.ts
@@ -3,6 +3,7 @@ import {
type Part,
createRegion,
type MutableSignal,
+ isCellOccupied as isCellOccupiedUtil,
} from 'boardgame-core';
const BOARD_SIZE = 3;
@@ -21,7 +22,7 @@ const WINNING_LINES: number[][][] = [
export type PlayerType = 'X' | 'O';
export type WinnerType = PlayerType | 'draw' | null;
-export type TicTacToePart = Part & { player: PlayerType };
+export type TicTacToePart = Part<{ player: PlayerType }>;
export function createInitialState() {
return {
@@ -29,7 +30,7 @@ export function createInitialState() {
{ name: 'x', min: 0, max: BOARD_SIZE - 1 },
{ name: 'y', min: 0, max: BOARD_SIZE - 1 },
]),
- parts: {} as Record,
+ parts: [] as TicTacToePart[],
currentPlayer: 'X' as PlayerType,
winner: null as WinnerType,
turn: 0,
@@ -93,8 +94,7 @@ registration.add('turn ', async function (cmd) {
});
export function isCellOccupied(host: MutableSignal, row: number, col: number): boolean {
- const board = host.value.board;
- return board.partMap[`${row},${col}`] !== undefined;
+ return isCellOccupiedUtil(host.value.parts, 'board', [row, col]);
}
export function hasWinningLine(positions: number[][]): boolean {
@@ -106,7 +106,7 @@ export function hasWinningLine(positions: number[][]): boolean {
}
export function checkWinner(host: MutableSignal): WinnerType {
- const parts = Object.values(host.value.parts);
+ const parts = host.value.parts;
const xPositions = parts.filter((p: TicTacToePart) => p.player === 'X').map((p: TicTacToePart) => p.position);
const oPositions = parts.filter((p: TicTacToePart) => p.player === 'O').map((p: TicTacToePart) => p.position);
@@ -119,8 +119,7 @@ export function checkWinner(host: MutableSignal): WinnerType {
}
export function placePiece(host: MutableSignal, row: number, col: number, player: PlayerType) {
- const board = host.value.board;
- const moveNumber = Object.keys(host.value.parts).length + 1;
+ const moveNumber = host.value.parts.length + 1;
const piece: TicTacToePart = {
id: `piece-${player}-${moveNumber}`,
regionId: 'board',
@@ -128,8 +127,8 @@ export function placePiece(host: MutableSignal, row: number, col
player,
};
host.produce(state => {
- state.parts[piece.id] = piece;
- board.childIds.push(piece.id);
- board.partMap[`${row},${col}`] = piece.id;
+ state.parts.push(piece);
+ state.board.childIds.push(piece.id);
+ state.board.partMap[`${row},${col}`] = piece.id;
});
}
diff --git a/packages/sample-game/src/main.tsx b/packages/sample-game/src/main.tsx
index 1299de5..f54c551 100644
--- a/packages/sample-game/src/main.tsx
+++ b/packages/sample-game/src/main.tsx
@@ -1,5 +1,6 @@
import { h, render } from 'preact';
import { signal } from '@preact/signals-core';
+import { useEffect, useState } from 'preact/hooks';
import Phaser from 'phaser';
import { createGameContext } from 'boardgame-core';
import { GameUI, PromptDialog, CommandLog } from 'boardgame-phaser';
@@ -12,10 +13,12 @@ const gameContext = createGameContext(registry, createInitialSta
const promptSignal = signal>>(null);
const commandLog = signal>([]);
+// 监听 prompt 事件
gameContext.commands.on('prompt', (event) => {
promptSignal.value = event;
});
+// 包装 run 方法以记录命令日志
const originalRun = gameContext.commands.run.bind(gameContext.commands);
(gameContext.commands as any).run = async (input: string) => {
const result = await originalRun(input);
@@ -35,42 +38,63 @@ const sceneData: GameSceneData = {
commands: gameContext.commands,
};
-const phaserConfig: Phaser.Types.Core.GameConfig = {
- type: Phaser.AUTO,
- width: 560,
- height: 560,
- parent: 'phaser-container',
- backgroundColor: '#f9fafb',
- scene: [],
-};
+function App() {
+ const [phaserReady, setPhaserReady] = useState(false);
+ const [game, setGame] = useState(null);
-const game = new Phaser.Game(phaserConfig);
+ useEffect(() => {
+ const phaserConfig: Phaser.Types.Core.GameConfig = {
+ type: Phaser.AUTO,
+ width: 560,
+ height: 560,
+ parent: 'phaser-container',
+ backgroundColor: '#f9fafb',
+ scene: [],
+ };
-game.scene.add('GameScene', GameScene, true, sceneData);
+ const phaserGame = new Phaser.Game(phaserConfig);
+ phaserGame.scene.add('GameScene', GameScene, true, sceneData);
+
+ setGame(phaserGame);
+ setPhaserReady(true);
+
+ return () => {
+ phaserGame.destroy(true);
+ };
+ }, []);
+
+ useEffect(() => {
+ if (phaserReady) {
+ gameContext.commands.run('setup');
+ }
+ }, [phaserReady]);
+
+ return (
+
+
+
+
{
+ gameContext.commands._tryCommit(input);
+ promptSignal.value = null;
+ }}
+ onCancel={() => {
+ gameContext.commands._cancel('cancelled');
+ promptSignal.value = null;
+ }}
+ />
+
+
+
+
+
+ );
+}
const ui = new GameUI({
container: document.getElementById('ui-root')!,
- root: h('div', { className: 'flex flex-col h-screen' },
- h('div', { className: 'flex-1 relative' },
- h('div', { id: 'phaser-container', className: 'w-full h-full' }),
- h(PromptDialog, {
- prompt: promptSignal.value,
- onSubmit: (input: string) => {
- gameContext.commands._tryCommit(input);
- promptSignal.value = null;
- },
- onCancel: () => {
- gameContext.commands._cancel('cancelled');
- promptSignal.value = null;
- },
- }),
- ),
- h('div', { className: 'p-4 bg-gray-100 border-t' },
- h(CommandLog, { entries: commandLog }),
- ),
- ),
+ root: ,
});
ui.mount();
-
-gameContext.commands.run('setup');
diff --git a/packages/sample-game/src/scenes/GameScene.ts b/packages/sample-game/src/scenes/GameScene.ts
index fbbce83..f11b8df 100644
--- a/packages/sample-game/src/scenes/GameScene.ts
+++ b/packages/sample-game/src/scenes/GameScene.ts
@@ -1,5 +1,6 @@
import Phaser from 'phaser';
-import type { TicTacToeState, TicTacToePart } from '@/game/tic-tac-toe';
+import type { TicTacToeState, TicTacToePart, PlayerType } from '@/game/tic-tac-toe';
+import { isCellOccupied } from 'boardgame-core';
import { ReactiveScene, bindRegion, createInputMapper, createPromptHandler } from 'boardgame-phaser';
import type { PromptEvent, MutableSignal, IGameContext } from 'boardgame-core';
@@ -33,6 +34,7 @@ export class GameScene extends ReactiveScene {
}
create(): void {
+ super.create();
this.boardContainer = this.add.container(0, 0);
this.gridGraphics = this.add.graphics();
this.drawGrid();
@@ -49,18 +51,18 @@ export class GameScene extends ReactiveScene {
this.updateTurnText(currentPlayer);
});
- this.setupBindings();
this.setupInput();
}
protected setupBindings(): void {
- bindRegion(
+ bindRegion(
+ this.state,
+ (state) => state.parts,
this.state.value.board,
- this.state.value.parts,
{
cellSize: { x: CELL_SIZE, y: CELL_SIZE },
offset: BOARD_OFFSET,
- factory: (part: TicTacToePart, pos: Phaser.Math.Vector2) => {
+ factory: (part, pos: Phaser.Math.Vector2) => {
const text = this.add.text(pos.x + CELL_SIZE / 2, pos.y + CELL_SIZE / 2, part.player, {
fontSize: '64px',
fontFamily: 'Arial',
@@ -85,8 +87,7 @@ export class GameScene extends ReactiveScene {
if (this.state.value.winner) return null;
const currentPlayer = this.state.value.currentPlayer;
- const board = this.state.value.board;
- if (board.partMap[`${row},${col}`]) return null;
+ if (isCellOccupied(this.state.value.parts, 'board', [row, col])) return null;
return `play ${currentPlayer} ${row} ${col}`;
},