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