Extract PromptContext from GameHost into separate module
The prompt system is now managed by a dedicated PromptContext instead of being embedded in GameHost. This simplifies the API by removing individual prompt signals and consolidating prompt logic into a reusable context that can be shared between components.
This commit is contained in:
parent
59b99d2042
commit
b009189b9a
|
|
@ -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<string, unknown>,
|
||||
TResult = unknown,
|
||||
TModule extends GameModule<TState, TResult> = GameModule<TState, TResult>,
|
||||
> {
|
||||
readonly state: ReadonlySignal<TState>;
|
||||
export class GameHost<TModule extends GameModule> {
|
||||
readonly state: ReadonlySignal<GameState<TModule>>;
|
||||
readonly status: ReadonlySignal<GameHostStatus>;
|
||||
readonly activePromptSchema: ReadonlySignal<CommandSchema | null>;
|
||||
readonly activePromptPlayer: ReadonlySignal<string | null>;
|
||||
readonly activePromptHint: ReadonlySignal<string | null>;
|
||||
readonly prompts: PromptContext;
|
||||
|
||||
private _context: IGameContext<TState>;
|
||||
private _start: (ctx: IGameContext<TState>) => Promise<TResult>;
|
||||
private _context: IGameContext<GameState<TModule>>;
|
||||
private _start: (
|
||||
ctx: IGameContext<GameState<TModule>>,
|
||||
) => Promise<GameResult<TModule>>;
|
||||
private _status: Signal<GameHostStatus>;
|
||||
private _activePromptSchema: Signal<CommandSchema | null>;
|
||||
private _activePromptPlayer: Signal<string | null>;
|
||||
private _activePromptHint: Signal<string | null>;
|
||||
private _createInitialState: () => TState;
|
||||
private _createInitialState: () => GameState<TModule>;
|
||||
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<TModule>,
|
||||
GameResult<TModule>
|
||||
>;
|
||||
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<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._activePromptHint = new Signal<string | null>(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<TArgs extends any[]>(def: PromptDef<TArgs>, ...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<TResult> {
|
||||
start(seed?: number): Promise<GameResult<TModule>> {
|
||||
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<string, unknown>,
|
||||
TState extends Record<string, unknown> = Record<string, unknown>,
|
||||
TResult = unknown,
|
||||
> = {
|
||||
registry?: CommandRegistry<IGameContext<TState>>;
|
||||
createInitialState: () => TState;
|
||||
start: (ctx: IGameContext<TState>) => Promise<TResult>;
|
||||
};
|
||||
export type GameState<TModule extends GameModule> = ReturnType<
|
||||
TModule["createInitialState"]
|
||||
>;
|
||||
export type GameResult<TModule extends GameModule> = Awaited<
|
||||
ReturnType<TModule["start"]>
|
||||
>;
|
||||
|
||||
export function createGameHost<
|
||||
TState extends Record<string, unknown>,
|
||||
TResult = unknown,
|
||||
TModule extends GameModule<TState, TResult> = GameModule<TState, TResult>,
|
||||
>(gameModule: TModule): GameHost<TState, TResult> {
|
||||
export function createGameHost<TModule extends GameModule>(
|
||||
gameModule: TModule,
|
||||
): GameHost<TModule> {
|
||||
return new GameHost(gameModule);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<TState extends Record<string, unknown> = {}> {
|
||||
get value(): TState;
|
||||
get rng(): ReadonlyRNG;
|
||||
produce(fn: (draft: TState) => undefined): void;
|
||||
produceAsync(fn: (draft: TState) => undefined): Promise<void>;
|
||||
run<T>(input: string): Promise<CommandResult<T>>;
|
||||
runParsed<T>(command: Command): Promise<CommandResult<T>>;
|
||||
prompt: <TResult, TArgs extends any[] = any[]>(
|
||||
def: PromptDef<TArgs>,
|
||||
validator: PromptValidator<TResult, TArgs>,
|
||||
currentPlayer?: string | null,
|
||||
player?: string,
|
||||
) => Promise<TResult>;
|
||||
|
||||
// test only
|
||||
_state: MutableSignal<TState>;
|
||||
_commands: CommandRunnerContextExport<IGameContext<TState>>;
|
||||
_rng: RNG;
|
||||
}
|
||||
export type IGameContextExport<TState extends Record<string, unknown> = {}> =
|
||||
Omit<IGameContext<TState>, "_state" | "_commands" | "_rng">;
|
||||
|
||||
export function createGameContext<TState extends Record<string, unknown> = {}>(
|
||||
commandRegistry: CommandRegistry<IGameContext<TState>>,
|
||||
promptContext: PromptContext,
|
||||
initialState?: TState | (() => TState),
|
||||
): IGameContext<TState> {
|
||||
const stateValue =
|
||||
|
|
@ -42,7 +31,7 @@ export function createGameContext<TState extends Record<string, unknown> = {}>(
|
|||
? initialState()
|
||||
: (initialState ?? ({} as TState));
|
||||
const state = mutableSignal(stateValue);
|
||||
let commands: CommandRunnerContextExport<IGameContext<TState>> = null as any;
|
||||
const { prompt } = promptContext;
|
||||
|
||||
const context: IGameContext<TState> = {
|
||||
get value(): TState {
|
||||
|
|
@ -57,38 +46,15 @@ export function createGameContext<TState extends Record<string, unknown> = {}>(
|
|||
produceAsync(fn: (draft: TState) => undefined) {
|
||||
return state.produceAsync(fn);
|
||||
},
|
||||
run<T>(input: string) {
|
||||
return commands.run<T>(input);
|
||||
},
|
||||
runParsed<T>(command: Command) {
|
||||
return commands.runParsed<T>(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<TArgs extends any[] = any[]> = {
|
||||
schema: CommandSchema;
|
||||
hintText?: string;
|
||||
};
|
||||
export function createPromptDef<TArgs extends any[] = any[]>(
|
||||
schema: CommandSchema | string,
|
||||
hintText?: string,
|
||||
|
|
@ -96,9 +62,3 @@ export function createPromptDef<TArgs extends any[] = any[]>(
|
|||
schema = typeof schema === "string" ? parseCommandSchema(schema) : schema;
|
||||
return { schema, hintText };
|
||||
}
|
||||
|
||||
export function createGameCommandRegistry<
|
||||
TState extends Record<string, unknown> = {},
|
||||
>() {
|
||||
return createCommandRegistry<IGameContext<TState>>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { CommandSchema } from "./types";
|
|||
|
||||
export interface PromptDef<TArgs extends any[]> {
|
||||
schema: CommandSchema;
|
||||
hint?: string;
|
||||
hintText?: string;
|
||||
}
|
||||
|
||||
export interface PromptCall<TArgs extends any[] = unknown[], TRes = unknown> {
|
||||
|
|
@ -26,6 +26,7 @@ export type PromptTryResult =
|
|||
ok: true;
|
||||
};
|
||||
|
||||
export type PromptContext = ReturnType<typeof createPromptContext>;
|
||||
export function createPromptContext() {
|
||||
const map = new Map<string, PromptCall>();
|
||||
const handleCall = createMiddlewareChain(async (call: PromptCall) => {
|
||||
|
|
@ -44,6 +45,7 @@ export function createPromptContext() {
|
|||
});
|
||||
|
||||
function tryCommit<TArgs extends any[]>(
|
||||
def: PromptDef<TArgs>,
|
||||
player: string,
|
||||
...args: TArgs
|
||||
): PromptTryResult {
|
||||
|
|
@ -83,7 +85,14 @@ export function createPromptContext() {
|
|||
reject: reject!,
|
||||
promise,
|
||||
} as PromptCall<unknown[], unknown>;
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import { Signal, SignalOptions } from "@preact/signals-core";
|
||||
import { create } from "mutative";
|
||||
|
||||
export class MutableSignal<T> extends Signal<T> {
|
||||
export interface MutableSignal<T> extends Signal<T> {
|
||||
produce(fn: (draft: T) => undefined): void;
|
||||
addInterruption(promise: Promise<void>): void;
|
||||
clearInterruptions(): void;
|
||||
produceAsync(fn: (draft: T) => undefined): Promise<void>;
|
||||
}
|
||||
|
||||
class MutableSignalImpl<T> extends Signal<T> {
|
||||
private _interruptions: Promise<void>[] = [];
|
||||
|
||||
public constructor(t?: T, options?: SignalOptions<T>) {
|
||||
|
|
@ -41,5 +48,5 @@ export function mutableSignal<T>(
|
|||
initial?: T,
|
||||
options?: SignalOptions<T>,
|
||||
): MutableSignal<T> {
|
||||
return new MutableSignal<T>(initial, options);
|
||||
return new MutableSignalImpl<T>(initial, options);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue