refactor: use GameHost for simplification

This commit is contained in:
hypercross 2026-04-04 01:11:49 +08:00
parent 32509d7812
commit 4fa06098ba
2 changed files with 65 additions and 72 deletions

View File

@ -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<TicTacToeState>(registry, createInitialState);
// 创建 GameHost 实例,自动管理状态和 prompt
const gameHost = createGameHost(
{ registry, createInitialState },
'setup',
{ autoStart: false }
);
const commandLog = signal<Array<{ input: string; result: string; timestamp: number }>>([]);
// Single PromptHandler — the only consumer of promptQueue
let promptHandler: ReturnType<typeof createPromptHandler> | null = null;
const promptSignal = signal<import('boardgame-core').PromptEvent | null>(null);
// 记录命令日志的辅助函数
function logCommand(input: string, result: { success: boolean; result?: unknown; error?: string }) {
commandLog.value = [
@ -33,6 +34,7 @@ function App() {
const [game, setGame] = useState<Phaser.Game | null>(null);
const [scene, setScene] = useState<GameScene | null>(null);
const [gameState, setGameState] = useState<TicTacToeState | null>(null);
const [promptSchema, setPromptSchema] = useState<any>(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() {
<div className="flex flex-col h-screen">
<div className="flex-1 relative">
<div id="phaser-container" className="w-full h-full" />
{/* 游戏状态显示 */}
{gameState && !gameState.winner && (
<div className="absolute top-4 left-1/2 transform -translate-x-1/2 bg-white px-4 py-2 rounded-lg shadow-lg z-10">
@ -137,7 +128,7 @@ function App() {
</span>
</div>
)}
{gameState?.winner && (
<div className="absolute top-4 left-1/2 transform -translate-x-1/2 bg-white px-4 py-2 rounded-lg shadow-lg z-10">
<span className="text-lg font-semibold text-yellow-600">
@ -147,7 +138,7 @@ function App() {
)}
<PromptDialog
prompt={promptSignal.value}
prompt={promptSchema ? { schema: promptSchema, tryCommit: () => null, cancel: () => {} } : null}
onSubmit={handlePromptSubmit}
onCancel={handlePromptCancel}
/>

View File

@ -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<TicTacToeState> {
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<TicTacToeState>;
constructor() {
super('GameScene');
}
init(data: { gameHost: GameHost<TicTacToeState> } | { gameContext: IGameContext<TicTacToeState> }): void {
if ('gameHost' in data) {
this.gameHost = data.gameHost;
// Create a compatible gameContext from GameHost
this.gameContext = {
state: this.gameHost.state as MutableSignal<TicTacToeState>,
commands: this.gameHost.commands,
} as IGameContext<TicTacToeState>;
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<TicTacToeState> {
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<TicTacToeState> {
);
}
/**
* 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);