diff --git a/src/core/game-host.ts b/src/core/game-host.ts index 3a64a27..625dce4 100644 --- a/src/core/game-host.ts +++ b/src/core/game-host.ts @@ -1,100 +1,40 @@ import { ReadonlySignal, Signal } from "@preact/signals-core"; -import { CommandSchema, CommandRegistry, PromptEvent } from "@/utils/command"; -import { - createGameCommandRegistry, - createGameContext, - IGameContext, - PromptDef, -} from "./game"; +import { createPromptContext } from "@/utils/command"; +import { createGameContext, IGameContext } from "./game"; +import { PromptContext } from "@/utils/command/command-prompt"; export type GameHostStatus = "created" | "running" | "disposed"; -export class GameHost< - TState extends Record, - TResult = unknown, - TModule extends GameModule = GameModule, -> { - readonly state: ReadonlySignal; +export class GameHost { + readonly state: ReadonlySignal>; readonly status: ReadonlySignal; - readonly activePromptSchema: ReadonlySignal; - readonly activePromptPlayer: ReadonlySignal; - readonly activePromptHint: ReadonlySignal; + readonly prompts: PromptContext; - private _context: IGameContext; - private _start: (ctx: IGameContext) => Promise; + private _context: IGameContext>; + private _start: ( + ctx: IGameContext>, + ) => Promise>; private _status: Signal; - private _activePromptSchema: Signal; - private _activePromptPlayer: Signal; - private _activePromptHint: Signal; - private _createInitialState: () => TState; + private _createInitialState: () => GameState; private _eventListeners: Map<"start" | "dispose", Set<() => void>>; private _isDisposed = false; constructor(public readonly gameModule: TModule) { - const { createInitialState, registry, start } = gameModule; + const { createInitialState, start } = gameModule as unknown as GameModule< + GameState, + GameResult + >; this._createInitialState = createInitialState; this._eventListeners = new Map(); + this.prompts = createPromptContext(); const initialState = createInitialState(); - this._context = createGameContext( - registry ?? createGameCommandRegistry(), - initialState, - ); + this._context = createGameContext(this.prompts, initialState); this._start = start; this.state = this._context._state; this._status = new Signal("created"); this.status = this._status; - - this._activePromptSchema = new Signal(null); - this.activePromptSchema = this._activePromptSchema; - - this._activePromptPlayer = new Signal(null); - this.activePromptPlayer = this._activePromptPlayer; - - this._activePromptHint = new Signal(null); - this.activePromptHint = this._activePromptHint; - - 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(def: PromptDef, ...args: TArgs) { - return this._context._commands._tryCommit({ - name: def.schema.name, - params: args, - options: {}, - flags: {}, - }); } /** @@ -113,12 +53,12 @@ export class GameHost< this._context._state.clearInterruptions(); } - start(seed?: number): Promise { + start(seed?: number): Promise> { if (this._isDisposed) { throw new Error("GameHost is disposed"); } - this._context._commands._cancel(); + this.prompts.reset(); const initialState = this._createInitialState(); this._context._state.value = initialState as any; @@ -139,7 +79,7 @@ export class GameHost< } this._isDisposed = true; - this._context._commands._cancel(); + this.prompts.reset(); this._status.value = "disposed"; // Emit dispose event BEFORE clearing listeners @@ -169,18 +109,21 @@ export class GameHost< } export type GameModule< - TState extends Record, + TState extends Record = Record, TResult = unknown, > = { - registry?: CommandRegistry>; createInitialState: () => TState; start: (ctx: IGameContext) => Promise; }; +export type GameState = ReturnType< + TModule["createInitialState"] +>; +export type GameResult = Awaited< + ReturnType +>; -export function createGameHost< - TState extends Record, - TResult = unknown, - TModule extends GameModule = GameModule, ->(gameModule: TModule): GameHost { +export function createGameHost( + gameModule: TModule, +): GameHost { return new GameHost(gameModule); } diff --git a/src/core/game.ts b/src/core/game.ts index 422d1f5..5c2569d 100644 --- a/src/core/game.ts +++ b/src/core/game.ts @@ -1,40 +1,29 @@ import { MutableSignal, mutableSignal } from "@/utils/mutable-signal"; -import { - Command, - CommandRegistry, - CommandResult, - CommandRunnerContextExport, - CommandSchema, - createCommandRegistry, - createCommandRunnerContext, - parseCommandSchema, -} from "@/utils/command"; +import { CommandSchema, parseCommandSchema, PromptDef } from "@/utils/command"; import { PromptValidator } from "@/utils/command/command-runner"; import { Mulberry32RNG, ReadonlyRNG, RNG } from "@/utils/rng"; +import { PromptContext } from "@/utils/command/command-prompt"; export interface IGameContext = {}> { get value(): TState; get rng(): ReadonlyRNG; produce(fn: (draft: TState) => undefined): void; produceAsync(fn: (draft: TState) => undefined): Promise; - run(input: string): Promise>; - runParsed(command: Command): Promise>; prompt: ( def: PromptDef, validator: PromptValidator, - currentPlayer?: string | null, + player?: string, ) => Promise; // test only _state: MutableSignal; - _commands: CommandRunnerContextExport>; _rng: RNG; } export type IGameContextExport = {}> = Omit, "_state" | "_commands" | "_rng">; export function createGameContext = {}>( - commandRegistry: CommandRegistry>, + promptContext: PromptContext, initialState?: TState | (() => TState), ): IGameContext { const stateValue = @@ -42,7 +31,7 @@ export function createGameContext = {}>( ? initialState() : (initialState ?? ({} as TState)); const state = mutableSignal(stateValue); - let commands: CommandRunnerContextExport> = null as any; + const { prompt } = promptContext; const context: IGameContext = { get value(): TState { @@ -57,38 +46,15 @@ export function createGameContext = {}>( produceAsync(fn: (draft: TState) => undefined) { return state.produceAsync(fn); }, - run(input: string) { - return commands.run(input); - }, - runParsed(command: Command) { - return commands.runParsed(command); - }, - prompt(def, validator, currentPlayer) { - return commands.prompt( - def.schema, - validator, - def.hintText, - currentPlayer, - ); - }, + prompt, _state: state, - _commands: commands, _rng: new Mulberry32RNG(), }; - context._commands = commands = createCommandRunnerContext( - commandRegistry, - context, - ); - return context; } -export type PromptDef = { - schema: CommandSchema; - hintText?: string; -}; export function createPromptDef( schema: CommandSchema | string, hintText?: string, @@ -96,9 +62,3 @@ export function createPromptDef( schema = typeof schema === "string" ? parseCommandSchema(schema) : schema; return { schema, hintText }; } - -export function createGameCommandRegistry< - TState extends Record = {}, ->() { - return createCommandRegistry>(); -} diff --git a/src/index.ts b/src/index.ts index 7c3a5b1..2d411d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ // Core types export type { IGameContext } from "./core/game"; -export { createGameCommandRegistry, createPromptDef } from "./core/game"; +export { createPromptDef } from "./core/game"; export type { GameHost, GameHostStatus, GameModule } from "./core/game-host"; export { createGameHost } from "./core/game-host"; diff --git a/src/utils/command/command-prompt.ts b/src/utils/command/command-prompt.ts index 1d0cf8b..9850514 100644 --- a/src/utils/command/command-prompt.ts +++ b/src/utils/command/command-prompt.ts @@ -4,7 +4,7 @@ import { CommandSchema } from "./types"; export interface PromptDef { schema: CommandSchema; - hint?: string; + hintText?: string; } export interface PromptCall { @@ -26,6 +26,7 @@ export type PromptTryResult = ok: true; }; +export type PromptContext = ReturnType; export function createPromptContext() { const map = new Map(); const handleCall = createMiddlewareChain(async (call: PromptCall) => { @@ -44,6 +45,7 @@ export function createPromptContext() { }); function tryCommit( + def: PromptDef, player: string, ...args: TArgs ): PromptTryResult { @@ -83,7 +85,14 @@ export function createPromptContext() { reject: reject!, promise, } as PromptCall; - return await handleCall.execute(call); + return (await handleCall.execute(call)) as TRes; + } + + function reset() { + for (const call of map.values()) { + call.reject("Prompt Reset"); + } + map.clear(); } return { @@ -91,5 +100,6 @@ export function createPromptContext() { tryCommit, cancel, handleCall, + reset, }; } diff --git a/src/utils/mutable-signal.ts b/src/utils/mutable-signal.ts index 0017541..d58332b 100644 --- a/src/utils/mutable-signal.ts +++ b/src/utils/mutable-signal.ts @@ -1,7 +1,14 @@ import { Signal, SignalOptions } from "@preact/signals-core"; import { create } from "mutative"; -export class MutableSignal extends Signal { +export interface MutableSignal extends Signal { + produce(fn: (draft: T) => undefined): void; + addInterruption(promise: Promise): void; + clearInterruptions(): void; + produceAsync(fn: (draft: T) => undefined): Promise; +} + +class MutableSignalImpl extends Signal { private _interruptions: Promise[] = []; public constructor(t?: T, options?: SignalOptions) { @@ -41,5 +48,5 @@ export function mutableSignal( initial?: T, options?: SignalOptions, ): MutableSignal { - return new MutableSignal(initial, options); + return new MutableSignalImpl(initial, options); }