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}`; },