From 4fa06098bad4e8583e709e9bbab87435be5b87cd Mon Sep 17 00:00:00 2001 From: hypercross Date: Sat, 4 Apr 2026 01:11:49 +0800 Subject: [PATCH] refactor: use GameHost for simplification --- packages/sample-game/src/main.tsx | 93 +++++++++----------- packages/sample-game/src/scenes/GameScene.ts | 44 ++++----- 2 files changed, 65 insertions(+), 72 deletions(-) diff --git a/packages/sample-game/src/main.tsx b/packages/sample-game/src/main.tsx index 0549ac9..7b6d3b5 100644 --- a/packages/sample-game/src/main.tsx +++ b/packages/sample-game/src/main.tsx @@ -1,21 +1,22 @@ import { h, render } from 'preact'; -import { signal, computed } from '@preact/signals-core'; +import { signal } from '@preact/signals-core'; import { useEffect, useState, useCallback } from 'preact/hooks'; import Phaser from 'phaser'; -import { createGameContext } from 'boardgame-core'; -import { GameUI, PromptDialog, CommandLog, createPromptHandler } from 'boardgame-phaser'; +import { createGameHost } from 'boardgame-core'; +import { GameUI, PromptDialog, CommandLog } from 'boardgame-phaser'; import { createInitialState, registry, type TicTacToeState } from './game/tic-tac-toe'; import { GameScene } from './scenes/GameScene'; import './style.css'; -const gameContext = createGameContext(registry, createInitialState); +// 创建 GameHost 实例,自动管理状态和 prompt +const gameHost = createGameHost( + { registry, createInitialState }, + 'setup', + { autoStart: false } +); const commandLog = signal>([]); -// Single PromptHandler — the only consumer of promptQueue -let promptHandler: ReturnType | null = null; -const promptSignal = signal(null); - // 记录命令日志的辅助函数 function logCommand(input: string, result: { success: boolean; result?: unknown; error?: string }) { commandLog.value = [ @@ -33,6 +34,7 @@ function App() { const [game, setGame] = useState(null); const [scene, setScene] = useState(null); const [gameState, setGameState] = useState(null); + const [promptSchema, setPromptSchema] = useState(null); useEffect(() => { const phaserConfig: Phaser.Types.Core.GameConfig = { @@ -45,81 +47,70 @@ function App() { }; const phaserGame = new Phaser.Game(phaserConfig); - // 通过 init 传递 gameContext + // 通过 init 传递 gameHost const gameScene = new GameScene(); - phaserGame.scene.add('GameScene', gameScene, true, { gameContext }); + phaserGame.scene.add('GameScene', gameScene, true, { gameHost }); setGame(phaserGame); setScene(gameScene); setPhaserReady(true); return () => { + gameHost.dispose(); phaserGame.destroy(true); }; }, []); useEffect(() => { if (phaserReady && scene) { - // Initialize the single PromptHandler - promptHandler = createPromptHandler({ - commands: gameContext.commands, - onPrompt: (prompt) => { - promptSignal.value = prompt; - // Also update the scene's prompt reference - scene.promptSignal.current = prompt; - }, - onCancel: () => { - promptSignal.value = null; - scene.promptSignal.current = null; - }, - }); - promptHandler.start(); - - // Wire the scene's submit function to this PromptHandler - scene.setSubmitPrompt((cmd: string) => { - const error = promptHandler!.submit(cmd); - if (error === null) { - logCommand(cmd, { success: true }); - promptSignal.value = null; - scene.promptSignal.current = null; - } else { - logCommand(cmd, { success: false, error }); - } - return error; + // 监听 prompt 状态变化 + const disposePromptSchema = gameHost.activePromptSchema.subscribe((schema) => { + setPromptSchema(schema); + scene.promptSchema.current = schema; }); // 监听状态变化 - const dispose = gameContext.state.subscribe(() => { - setGameState({ ...gameContext.state.value }); + const disposeState = gameHost.state.subscribe(() => { + setGameState(gameHost.state.value as TicTacToeState); }); // 运行游戏设置 - gameContext.commands.run('setup').then(result => { - logCommand('setup', result); + gameHost.setup('setup').then(() => { + logCommand('setup', { success: true }); + }).catch(err => { + logCommand('setup', { success: false, error: err.message }); }); return () => { - dispose(); - promptHandler?.destroy(); - promptHandler = null; + disposePromptSchema(); + disposeState(); }; } }, [phaserReady, scene]); const handlePromptSubmit = useCallback((input: string) => { - if (promptHandler) { - promptHandler.submit(input); + const error = gameHost.onInput(input); + if (error === null) { + logCommand(input, { success: true }); + setPromptSchema(null); + if (scene) { + scene.promptSchema.current = null; + } + } else { + logCommand(input, { success: false, error }); } }, []); const handlePromptCancel = useCallback(() => { - if (promptHandler) { - promptHandler.cancel('User cancelled'); + gameHost.commands._cancel('User cancelled'); + setPromptSchema(null); + if (scene) { + scene.promptSchema.current = null; } }, []); const handleReset = useCallback(() => { - gameContext.commands.run('reset').then(result => { + gameHost.commands.run('reset').then(result => { logCommand('reset', result); }); }, []); @@ -128,7 +119,7 @@ function App() {
- + {/* 游戏状态显示 */} {gameState && !gameState.winner && (
@@ -137,7 +128,7 @@ function App() {
)} - + {gameState?.winner && (
@@ -147,7 +138,7 @@ function App() { )} null, cancel: () => {} } : null} onSubmit={handlePromptSubmit} onCancel={handlePromptCancel} /> diff --git a/packages/sample-game/src/scenes/GameScene.ts b/packages/sample-game/src/scenes/GameScene.ts index e6a0e48..26dba5b 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, PlayerType } from '@/game/tic-tac-toe'; +import type { GameHost, CommandSchema, IGameContext, MutableSignal } from 'boardgame-core'; import { ReactiveScene, bindRegion, createInputMapper, InputMapper } from 'boardgame-phaser'; const CELL_SIZE = 120; @@ -11,13 +12,31 @@ export class GameScene extends ReactiveScene { private gridGraphics!: Phaser.GameObjects.Graphics; private inputMapper!: InputMapper; private turnText!: Phaser.GameObjects.Text; - /** Receives the active prompt from the single PromptHandler in main.tsx */ - promptSignal: { current: any } = { current: null }; + /** Receives the active prompt schema from main.tsx */ + promptSchema: { current: CommandSchema | null } = { current: null }; + /** GameHost instance passed from main.tsx */ + private gameHost!: GameHost; constructor() { super('GameScene'); } + init(data: { gameHost: GameHost } | { gameContext: IGameContext }): void { + if ('gameHost' in data) { + this.gameHost = data.gameHost; + // Create a compatible gameContext from GameHost + this.gameContext = { + state: this.gameHost.state as MutableSignal, + commands: this.gameHost.commands, + } as IGameContext; + this.state = this.gameContext.state; + this.commands = this.gameContext.commands; + } else { + // Fallback for direct gameContext passing + super.init(data); + } + } + protected onStateReady(_state: TicTacToeState): void { } @@ -84,10 +103,8 @@ export class GameScene extends ReactiveScene { private setupInput(): void { this.inputMapper = createInputMapper(this, { onSubmit: (cmd: string) => { - // Delegate to the single PromptHandler via the shared commands reference. - // The actual PromptHandler instance lives in main.tsx and is set up once. - // We call through a callback that main.tsx provides via the scene's public interface. - return this.submitToPrompt(cmd); + // Directly submit to GameHost + return this.gameHost.onInput(cmd); } }); @@ -106,21 +123,6 @@ export class GameScene extends ReactiveScene { ); } - /** - * Called by main.tsx to wire up the single PromptHandler's submit function. - */ - private _submitToPrompt: ((cmd: string) => string | null) | null = null; - - setSubmitPrompt(fn: (cmd: string) => string | null): void { - this._submitToPrompt = fn; - } - - private submitToPrompt(cmd: string): string | null { - return this._submitToPrompt - ? this._submitToPrompt(cmd) - : null; // no handler wired yet, accept silently - } - private drawGrid(): void { const g = this.gridGraphics; g.lineStyle(3, 0x6b7280);