refactor: reformat code and fix type signatures in GameHost

Reformat `src/core/game-host.ts` to use double quotes and consistent
spacing. Update `createGameHost` to correctly propagate the `TResult`
generic from `GameModule`.
This commit is contained in:
hypercross 2026-04-19 15:44:59 +08:00
parent 8142fbfa60
commit 1e1d04777f
1 changed files with 165 additions and 157 deletions

View File

@ -1,180 +1,188 @@
import { ReadonlySignal, Signal } from '@preact/signals-core';
import { ReadonlySignal, Signal } from "@preact/signals-core";
import { CommandSchema, CommandRegistry, PromptEvent } from "@/utils/command";
import {
CommandSchema,
CommandRegistry,
PromptEvent,
} from '@/utils/command';
import {createGameCommandRegistry, createGameContext, IGameContext, PromptDef} from './game';
createGameCommandRegistry,
createGameContext,
IGameContext,
PromptDef,
} from "./game";
export type GameHostStatus = 'created' | 'running' | 'disposed';
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>;
readonly activePromptHint: ReadonlySignal<string | null>;
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>;
readonly activePromptHint: 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 _activePromptHint: Signal<string | null>;
private _createInitialState: () => TState;
private _eventListeners: Map<'start' | 'dispose', Set<() => void>>;
private _isDisposed = false;
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 _activePromptHint: 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();
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;
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._status = new Signal<GameHostStatus>("created");
this.status = this._status;
this._activePromptSchema = new Signal<CommandSchema | null>(null);
this.activePromptSchema = this._activePromptSchema;
this._activePromptSchema = new Signal<CommandSchema | null>(null);
this.activePromptSchema = this._activePromptSchema;
this._activePromptPlayer = new Signal<string | null>(null);
this.activePromptPlayer = this._activePromptPlayer;
this._activePromptPlayer = new Signal<string | null>(null);
this.activePromptPlayer = this._activePromptPlayer;
this._activePromptHint = new Signal<string | null>(null);
this.activePromptHint = this._activePromptHint;
this._activePromptHint = new Signal<string | null>(null);
this.activePromptHint = this._activePromptHint;
this._setupPromptTracking();
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._activePromptHint.value = currentPromptEvent.hintText || null;
});
this._context._commands.on("promptEnd", () => {
currentPromptEvent = null;
this._activePromptSchema.value = null;
this._activePromptPlayer.value = null;
this._activePromptHint.value = null;
});
// Initial state
this._activePromptSchema.value = null;
this._activePromptPlayer.value = null;
this._activePromptHint.value = null;
}
tryInput(input: string): string | null {
if (this._isDisposed) {
return "GameHost is disposed";
}
return this._context._commands._tryCommit(input);
}
tryAnswerPrompt<TArgs extends any[]>(def: PromptDef<TArgs>, ...args: TArgs) {
return this._context._commands._tryCommit({
name: def.schema.name,
params: args,
options: {},
flags: {},
});
}
/**
* 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(seed?: number): Promise<TResult> {
if (this._isDisposed) {
throw new Error("GameHost is disposed");
}
private _setupPromptTracking() {
let currentPromptEvent: PromptEvent | null = null;
this._context._commands._cancel();
this._context._commands.on('prompt', (e) => {
currentPromptEvent = e as PromptEvent;
this._activePromptSchema.value = currentPromptEvent.schema;
this._activePromptPlayer.value = currentPromptEvent.currentPlayer;
this._activePromptHint.value = currentPromptEvent.hintText || null;
});
const initialState = this._createInitialState();
this._context._state.value = initialState as any;
this._context._commands.on('promptEnd', () => {
currentPromptEvent = null;
this._activePromptSchema.value = null;
this._activePromptPlayer.value = null;
this._activePromptHint.value = null;
});
seed = seed || Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
this._context._rng.setSeed(seed);
const promise = this._start(this._context);
// Initial state
this._activePromptSchema.value = null;
this._activePromptPlayer.value = null;
this._activePromptHint.value = null;
this._status.value = "running";
this._emitEvent("start");
return promise;
}
dispose(): void {
if (this._isDisposed) {
return;
}
tryInput(input: string): string | null {
if (this._isDisposed) {
return 'GameHost is disposed';
}
return this._context._commands._tryCommit(input);
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);
tryAnswerPrompt<TArgs extends any[]>(def: PromptDef<TArgs>, ...args: TArgs){
return this._context._commands._tryCommit({
name: def.schema.name,
params: args,
options: {},
flags: {}
});
}
/**
* 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(seed?: number): 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;
seed = seed || Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
this._context._rng.setSeed(seed);
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();
}
}
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 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
);
export function createGameHost<
TState extends Record<string, unknown>,
TResult = unknown,
>(gameModule: GameModule<TState, TResult>): GameHost<TState, TResult> {
return new GameHost(
gameModule.registry || createGameCommandRegistry(),
gameModule.createInitialState,
gameModule.start,
);
}