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

167 lines
5.2 KiB
TypeScript
Raw Normal View History

2026-04-04 14:05:23 +08:00
import { ReadonlySignal, Signal } from '@preact/signals-core';
import type {
CommandSchema,
CommandRegistry,
PromptEvent,
CommandRunnerContextExport,
} from '@/utils/command';
2026-04-04 00:54:19 +08:00
import type { MutableSignal } from '@/utils/mutable-signal';
2026-04-06 10:39:10 +08:00
import {createGameCommandRegistry, createGameContext, IGameContext} from './game';
2026-04-04 00:54:19 +08:00
export type GameHostStatus = 'created' | 'running' | 'disposed';
2026-04-06 10:39:10 +08:00
export class GameHost<TState extends Record<string, unknown>, TResult=unknown> {
2026-04-04 20:50:17 +08:00
readonly context: IGameContext<TState>;
2026-04-04 00:54:19 +08:00
readonly status: ReadonlySignal<GameHostStatus>;
readonly activePromptSchema: ReadonlySignal<CommandSchema | null>;
2026-04-04 10:30:00 +08:00
readonly activePromptPlayer: ReadonlySignal<string | null>;
2026-04-04 00:54:19 +08:00
private _state: MutableSignal<TState>;
2026-04-04 20:50:17 +08:00
private _commands: CommandRunnerContextExport<IGameContext<TState>>;
2026-04-06 10:39:10 +08:00
private _start: (ctx: IGameContext<TState>) => Promise<TResult>;
2026-04-04 00:54:19 +08:00
private _status: Signal<GameHostStatus>;
private _activePromptSchema: Signal<CommandSchema | null>;
2026-04-04 10:30:00 +08:00
private _activePromptPlayer: Signal<string | null>;
2026-04-04 00:54:19 +08:00
private _createInitialState: () => TState;
2026-04-06 09:46:59 +08:00
private _eventListeners: Map<'start' | 'dispose', Set<() => void>>;
2026-04-04 00:54:19 +08:00
private _isDisposed = false;
constructor(
2026-04-04 20:50:17 +08:00
registry: CommandRegistry<IGameContext<TState>>,
2026-04-04 00:54:19 +08:00
createInitialState: () => TState,
2026-04-06 10:39:10 +08:00
start: (ctx: IGameContext<TState>) => Promise<TResult>
2026-04-04 00:54:19 +08:00
) {
this._createInitialState = createInitialState;
this._eventListeners = new Map();
const initialState = createInitialState();
2026-04-04 20:50:17 +08:00
this.context = createGameContext(registry, initialState);
2026-04-06 10:39:10 +08:00
this._start = start;
2026-04-04 00:54:19 +08:00
this._status = new Signal<GameHostStatus>('created');
this.status = this._status;
this._activePromptSchema = new Signal<CommandSchema | null>(null);
this.activePromptSchema = this._activePromptSchema;
2026-04-04 10:30:00 +08:00
this._activePromptPlayer = new Signal<string | null>(null);
this.activePromptPlayer = this._activePromptPlayer;
2026-04-04 20:50:17 +08:00
this._state = this.context._state;
this._commands = this.context._commands;
2026-04-04 00:54:19 +08:00
this._setupPromptTracking();
}
private _setupPromptTracking() {
2026-04-04 10:30:00 +08:00
let currentPromptEvent: PromptEvent | null = null;
2026-04-04 00:54:19 +08:00
2026-04-04 20:50:17 +08:00
this._commands.on('prompt', (e) => {
2026-04-04 10:30:00 +08:00
currentPromptEvent = e as PromptEvent;
this._activePromptSchema.value = currentPromptEvent.schema;
this._activePromptPlayer.value = currentPromptEvent.currentPlayer;
2026-04-04 00:59:40 +08:00
});
2026-04-04 00:54:19 +08:00
2026-04-04 20:50:17 +08:00
this._commands.on('promptEnd', () => {
2026-04-04 10:30:00 +08:00
currentPromptEvent = null;
this._activePromptSchema.value = null;
this._activePromptPlayer.value = null;
2026-04-04 00:54:19 +08:00
});
2026-04-04 10:30:00 +08:00
// Initial state
this._activePromptSchema.value = null;
this._activePromptPlayer.value = null;
2026-04-04 00:54:19 +08:00
}
onInput(input: string): string | null {
if (this._isDisposed) {
return 'GameHost is disposed';
}
2026-04-04 20:50:17 +08:00
return this._commands._tryCommit(input);
2026-04-04 00:54:19 +08:00
}
/**
* produceAsync Promise UI
* @see MutableSignal.addInterruption
*/
addInterruption(promise: Promise<void>): void {
this._state.addInterruption(promise);
}
/**
*
* @see MutableSignal.clearInterruptions
*/
clearInterruptions(): void {
this._state.clearInterruptions();
}
2026-04-06 10:39:10 +08:00
start(): Promise<TResult> {
2026-04-04 00:54:19 +08:00
if (this._isDisposed) {
throw new Error('GameHost is disposed');
}
2026-04-04 20:50:17 +08:00
this._commands._cancel();
2026-04-04 00:54:19 +08:00
const initialState = this._createInitialState();
this._state.value = initialState as any;
2026-04-06 10:39:10 +08:00
const promise = this._start(this.context);
2026-04-04 00:54:19 +08:00
this._status.value = 'running';
2026-04-06 09:46:59 +08:00
this._emitEvent('start');
return promise;
2026-04-04 00:54:19 +08:00
}
dispose(): void {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
2026-04-04 20:50:17 +08:00
this._commands._cancel();
2026-04-04 00:54:19 +08:00
this._status.value = 'disposed';
// Emit dispose event BEFORE clearing listeners
this._emitEvent('dispose');
this._eventListeners.clear();
}
2026-04-06 09:46:59 +08:00
on(event: 'start' | 'dispose', listener: () => void): () => void {
2026-04-04 00:54:19 +08:00
if (!this._eventListeners.has(event)) {
this._eventListeners.set(event, new Set());
}
this._eventListeners.get(event)!.add(listener);
return () => {
this._eventListeners.get(event)?.delete(listener);
};
}
2026-04-06 09:46:59 +08:00
private _emitEvent(event: 'start' | 'dispose') {
2026-04-04 00:54:19 +08:00
const listeners = this._eventListeners.get(event);
if (listeners) {
for (const listener of listeners) {
listener();
}
}
}
}
2026-04-06 10:39:10 +08:00
export type GameModule<TState extends Record<string, unknown>, TResult=unknown> = {
registry?: CommandRegistry<IGameContext<TState>>;
2026-04-05 10:22:27 +08:00
createInitialState: () => TState;
2026-04-06 10:39:10 +08:00
start: (ctx: IGameContext<TState>) => Promise<TResult>;
2026-04-05 10:22:27 +08:00
}
2026-04-04 00:54:19 +08:00
export function createGameHost<TState extends Record<string, unknown>>(
2026-04-05 10:22:27 +08:00
gameModule: GameModule<TState>
2026-04-04 00:54:19 +08:00
): GameHost<TState> {
return new GameHost(
2026-04-06 10:39:10 +08:00
gameModule.registry || createGameCommandRegistry(),
2026-04-05 10:22:27 +08:00
gameModule.createInitialState,
2026-04-06 10:39:10 +08:00
gameModule.start
2026-04-04 00:54:19 +08:00
);
}