boardgame-core/src/core/game-host.ts

162 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ReadonlySignal, Signal } from '@preact/signals-core';
import {
CommandSchema,
CommandRegistry,
PromptEvent,
} from '@/utils/command';
import {createGameCommandRegistry, createGameContext, IGameContext} from './game';
export type GameHostStatus = 'created' | 'running' | 'disposed';
export class GameHost<TState extends Record<string, unknown>, TResult=unknown> {
readonly state: ReadonlySignal<TState>;
readonly status: ReadonlySignal<GameHostStatus>;
readonly activePromptSchema: ReadonlySignal<CommandSchema | null>;
readonly activePromptPlayer: ReadonlySignal<string | null>;
private _context: IGameContext<TState>;
private _start: (ctx: IGameContext<TState>) => Promise<TResult>;
private _status: Signal<GameHostStatus>;
private _activePromptSchema: Signal<CommandSchema | null>;
private _activePromptPlayer: Signal<string | null>;
private _createInitialState: () => TState;
private _eventListeners: Map<'start' | 'dispose', Set<() => void>>;
private _isDisposed = false;
constructor(
registry: CommandRegistry<IGameContext<TState>>,
createInitialState: () => TState,
start: (ctx: IGameContext<TState>) => Promise<TResult>
) {
this._createInitialState = createInitialState;
this._eventListeners = new Map();
const initialState = createInitialState();
this._context = createGameContext(registry, initialState);
this._start = start;
this.state = this._context._state;
this._status = new Signal<GameHostStatus>('created');
this.status = this._status;
this._activePromptSchema = new Signal<CommandSchema | null>(null);
this.activePromptSchema = this._activePromptSchema;
this._activePromptPlayer = new Signal<string | null>(null);
this.activePromptPlayer = this._activePromptPlayer;
this._setupPromptTracking();
}
private _setupPromptTracking() {
let currentPromptEvent: PromptEvent | null = null;
this._context._commands.on('prompt', (e) => {
currentPromptEvent = e as PromptEvent;
this._activePromptSchema.value = currentPromptEvent.schema;
this._activePromptPlayer.value = currentPromptEvent.currentPlayer;
});
this._context._commands.on('promptEnd', () => {
currentPromptEvent = null;
this._activePromptSchema.value = null;
this._activePromptPlayer.value = null;
});
// Initial state
this._activePromptSchema.value = null;
this._activePromptPlayer.value = null;
}
onInput(input: string): string | null {
if (this._isDisposed) {
return 'GameHost is disposed';
}
return this._context._commands._tryCommit(input);
}
/**
* 为下一个 produceAsync 注册中断 Promise通常用于 UI 动画)。
* @see MutableSignal.addInterruption
*/
addInterruption(promise: Promise<void>): void {
this._context._state.addInterruption(promise);
}
/**
* 清除所有未完成的中断。
* @see MutableSignal.clearInterruptions
*/
clearInterruptions(): void {
this._context._state.clearInterruptions();
}
start(): Promise<TResult> {
if (this._isDisposed) {
throw new Error('GameHost is disposed');
}
this._context._commands._cancel();
const initialState = this._createInitialState();
this._context._state.value = initialState as any;
const promise = this._start(this._context);
this._status.value = 'running';
this._emitEvent('start');
return promise;
}
dispose(): void {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
this._context._commands._cancel();
this._status.value = 'disposed';
// Emit dispose event BEFORE clearing listeners
this._emitEvent('dispose');
this._eventListeners.clear();
}
on(event: 'start' | 'dispose', listener: () => void): () => void {
if (!this._eventListeners.has(event)) {
this._eventListeners.set(event, new Set());
}
this._eventListeners.get(event)!.add(listener);
return () => {
this._eventListeners.get(event)?.delete(listener);
};
}
private _emitEvent(event: 'start' | 'dispose') {
const listeners = this._eventListeners.get(event);
if (listeners) {
for (const listener of listeners) {
listener();
}
}
}
}
export type GameModule<TState extends Record<string, unknown>, TResult=unknown> = {
registry?: CommandRegistry<IGameContext<TState>>;
createInitialState: () => TState;
start: (ctx: IGameContext<TState>) => Promise<TResult>;
}
export function createGameHost<TState extends Record<string, unknown>>(
gameModule: GameModule<TState>
): GameHost<TState> {
return new GameHost(
gameModule.registry || createGameCommandRegistry(),
gameModule.createInitialState,
gameModule.start
);
}