refactor: api surface change
This commit is contained in:
parent
467a56bd84
commit
6e1c42015f
|
|
@ -1,23 +1,23 @@
|
||||||
import { ReadonlySignal, Signal } from '@preact/signals-core';
|
import { ReadonlySignal, Signal } from '@preact/signals-core';
|
||||||
import type { CommandSchema, CommandRegistry, PromptEvent } from '@/utils/command';
|
import type {CommandSchema, CommandRegistry, PromptEvent, CommandRunnerContextExport} from '@/utils/command';
|
||||||
import type { MutableSignal } from '@/utils/mutable-signal';
|
import type { MutableSignal } from '@/utils/mutable-signal';
|
||||||
import { createGameContext } from './game';
|
import {createGameContext, IGameContext} from './game';
|
||||||
|
|
||||||
export type GameHostStatus = 'created' | 'running' | 'disposed';
|
export type GameHostStatus = 'created' | 'running' | 'disposed';
|
||||||
|
|
||||||
export interface GameModule<TState extends Record<string, unknown>> {
|
export interface GameModule<TState extends Record<string, unknown>> {
|
||||||
registry: CommandRegistry<MutableSignal<TState>>;
|
registry: CommandRegistry<IGameContext<TState>>;
|
||||||
createInitialState: () => TState;
|
createInitialState: () => TState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GameHost<TState extends Record<string, unknown>> {
|
export class GameHost<TState extends Record<string, unknown>> {
|
||||||
readonly state: ReadonlySignal<TState>;
|
readonly context: IGameContext<TState>;
|
||||||
readonly commands: ReturnType<typeof createGameContext<TState>>['commands'];
|
|
||||||
readonly status: ReadonlySignal<GameHostStatus>;
|
readonly status: ReadonlySignal<GameHostStatus>;
|
||||||
readonly activePromptSchema: ReadonlySignal<CommandSchema | null>;
|
readonly activePromptSchema: ReadonlySignal<CommandSchema | null>;
|
||||||
readonly activePromptPlayer: ReadonlySignal<string | null>;
|
readonly activePromptPlayer: ReadonlySignal<string | null>;
|
||||||
|
|
||||||
private _state: MutableSignal<TState>;
|
private _state: MutableSignal<TState>;
|
||||||
|
private _commands: CommandRunnerContextExport<IGameContext<TState>>;
|
||||||
private _status: Signal<GameHostStatus>;
|
private _status: Signal<GameHostStatus>;
|
||||||
private _activePromptSchema: Signal<CommandSchema | null>;
|
private _activePromptSchema: Signal<CommandSchema | null>;
|
||||||
private _activePromptPlayer: Signal<string | null>;
|
private _activePromptPlayer: Signal<string | null>;
|
||||||
|
|
@ -26,17 +26,14 @@ export class GameHost<TState extends Record<string, unknown>> {
|
||||||
private _isDisposed = false;
|
private _isDisposed = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
registry: CommandRegistry<MutableSignal<TState>>,
|
registry: CommandRegistry<IGameContext<TState>>,
|
||||||
createInitialState: () => TState,
|
createInitialState: () => TState,
|
||||||
) {
|
) {
|
||||||
this._createInitialState = createInitialState;
|
this._createInitialState = createInitialState;
|
||||||
this._eventListeners = new Map();
|
this._eventListeners = new Map();
|
||||||
|
|
||||||
const initialState = createInitialState();
|
const initialState = createInitialState();
|
||||||
const context = createGameContext(registry, initialState);
|
this.context = createGameContext(registry, initialState);
|
||||||
|
|
||||||
this._state = context.state;
|
|
||||||
this.commands = context.commands;
|
|
||||||
|
|
||||||
this._status = new Signal<GameHostStatus>('created');
|
this._status = new Signal<GameHostStatus>('created');
|
||||||
this.status = this._status;
|
this.status = this._status;
|
||||||
|
|
@ -47,7 +44,8 @@ export class GameHost<TState extends Record<string, unknown>> {
|
||||||
this._activePromptPlayer = new Signal<string | null>(null);
|
this._activePromptPlayer = new Signal<string | null>(null);
|
||||||
this.activePromptPlayer = this._activePromptPlayer;
|
this.activePromptPlayer = this._activePromptPlayer;
|
||||||
|
|
||||||
this.state = this._state;
|
this._state = this.context._state;
|
||||||
|
this._commands = this.context._commands;
|
||||||
|
|
||||||
this._setupPromptTracking();
|
this._setupPromptTracking();
|
||||||
}
|
}
|
||||||
|
|
@ -55,13 +53,13 @@ export class GameHost<TState extends Record<string, unknown>> {
|
||||||
private _setupPromptTracking() {
|
private _setupPromptTracking() {
|
||||||
let currentPromptEvent: PromptEvent | null = null;
|
let currentPromptEvent: PromptEvent | null = null;
|
||||||
|
|
||||||
this.commands.on('prompt', (e) => {
|
this._commands.on('prompt', (e) => {
|
||||||
currentPromptEvent = e as PromptEvent;
|
currentPromptEvent = e as PromptEvent;
|
||||||
this._activePromptSchema.value = currentPromptEvent.schema;
|
this._activePromptSchema.value = currentPromptEvent.schema;
|
||||||
this._activePromptPlayer.value = currentPromptEvent.currentPlayer;
|
this._activePromptPlayer.value = currentPromptEvent.currentPlayer;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.commands.on('promptEnd', () => {
|
this._commands.on('promptEnd', () => {
|
||||||
currentPromptEvent = null;
|
currentPromptEvent = null;
|
||||||
this._activePromptSchema.value = null;
|
this._activePromptSchema.value = null;
|
||||||
this._activePromptPlayer.value = null;
|
this._activePromptPlayer.value = null;
|
||||||
|
|
@ -76,7 +74,7 @@ export class GameHost<TState extends Record<string, unknown>> {
|
||||||
if (this._isDisposed) {
|
if (this._isDisposed) {
|
||||||
return 'GameHost is disposed';
|
return 'GameHost is disposed';
|
||||||
}
|
}
|
||||||
return this.commands._tryCommit(input);
|
return this._commands._tryCommit(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -100,14 +98,14 @@ export class GameHost<TState extends Record<string, unknown>> {
|
||||||
throw new Error('GameHost is disposed');
|
throw new Error('GameHost is disposed');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.commands._cancel();
|
this._commands._cancel();
|
||||||
|
|
||||||
const initialState = this._createInitialState();
|
const initialState = this._createInitialState();
|
||||||
this._state.value = initialState as any;
|
this._state.value = initialState as any;
|
||||||
|
|
||||||
// Start the setup command but don't wait for it to complete
|
// Start the setup command but don't wait for it to complete
|
||||||
// The command will run in the background and prompt for input
|
// The command will run in the background and prompt for input
|
||||||
this.commands.run(setupCommand).catch(() => {
|
this._commands.run(setupCommand).catch(() => {
|
||||||
// Command may be cancelled or fail, which is expected
|
// Command may be cancelled or fail, which is expected
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -121,7 +119,7 @@ export class GameHost<TState extends Record<string, unknown>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
this._isDisposed = true;
|
this._isDisposed = true;
|
||||||
this.commands._cancel();
|
this._commands._cancel();
|
||||||
this._status.value = 'disposed';
|
this._status.value = 'disposed';
|
||||||
|
|
||||||
// Emit dispose event BEFORE clearing listeners
|
// Emit dispose event BEFORE clearing listeners
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import {MutableSignal, mutableSignal} from "@/utils/mutable-signal";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandRegistry, CommandResult,
|
CommandRegistry, CommandResult,
|
||||||
CommandRunnerContext,
|
CommandRunnerContext, CommandRunnerContextExport,
|
||||||
CommandSchema,
|
CommandSchema,
|
||||||
createCommandRegistry,
|
createCommandRegistry,
|
||||||
createCommandRunnerContext,
|
createCommandRunnerContext,
|
||||||
|
|
@ -22,7 +22,7 @@ export interface IGameContext<TState extends Record<string, unknown> = {} > {
|
||||||
|
|
||||||
// test only
|
// test only
|
||||||
_state: MutableSignal<TState>;
|
_state: MutableSignal<TState>;
|
||||||
_commands: CommandRunnerContext<IGameContext<TState>>;
|
_commands: CommandRunnerContextExport<IGameContext<TState>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createGameContext<TState extends Record<string, unknown> = {} >(
|
export function createGameContext<TState extends Record<string, unknown> = {} >(
|
||||||
|
|
@ -31,7 +31,7 @@ export function createGameContext<TState extends Record<string, unknown> = {} >(
|
||||||
): IGameContext<TState> {
|
): IGameContext<TState> {
|
||||||
const stateValue = typeof initialState === 'function' ? initialState() : initialState ?? {} as TState;
|
const stateValue = typeof initialState === 'function' ? initialState() : initialState ?? {} as TState;
|
||||||
const state = mutableSignal(stateValue);
|
const state = mutableSignal(stateValue);
|
||||||
let commands: CommandRunnerContext<IGameContext<TState>> = null as any;
|
let commands: CommandRunnerContextExport<IGameContext<TState>> = null as any;
|
||||||
|
|
||||||
const context: IGameContext<TState> = {
|
const context: IGameContext<TState> = {
|
||||||
get value(): TState {
|
get value(): TState {
|
||||||
|
|
@ -82,18 +82,30 @@ export function createGameCommandRegistry<TState extends Record<string, unknown>
|
||||||
return createCommandRegistry<IGameContext<TState>>();
|
return createCommandRegistry<IGameContext<TState>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerGameCommand<TState extends Record<string, unknown> = {}, TResult = unknown>(
|
type CmdFunc<TState extends Record<string, unknown> = {}> = (this: IGameContext<TState>, ...args: any[]) => Promise<unknown>;
|
||||||
|
|
||||||
|
export function registerGameCommand<TState extends Record<string, unknown> = {}, TFunc extends CmdFunc<TState> = CmdFunc<TState>>(
|
||||||
registry: CommandRegistry<IGameContext<TState>>,
|
registry: CommandRegistry<IGameContext<TState>>,
|
||||||
schema: CommandSchema | string,
|
schema: CommandSchema | string,
|
||||||
run: (this: IGameContext<TState>, ...args: any[]) => Promise<TResult>
|
run: TFunc
|
||||||
) {
|
) {
|
||||||
|
const parsedSchema = typeof schema === 'string' ? parseCommandSchema(schema) : schema;
|
||||||
registerCommand(registry, {
|
registerCommand(registry, {
|
||||||
schema: typeof schema === 'string' ? parseCommandSchema(schema) : schema,
|
schema: parsedSchema,
|
||||||
async run(this: CommandRunnerContext<IGameContext<TState>>, command: Command){
|
async run(this: CommandRunnerContext<IGameContext<TState>>, command: Command){
|
||||||
const params = command.params;
|
const params = command.params;
|
||||||
return await run.call(this.context, ...params);
|
return await run.call(this.context, ...params);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return function(game: IGameContext<TState>, ...args: Parameters<TFunc>){
|
||||||
|
return game.runParsed({
|
||||||
|
options: {},
|
||||||
|
params: args,
|
||||||
|
flags: {},
|
||||||
|
name: parsedSchema.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { GameHost, createGameHost } from './game-host';
|
export { GameHost, createGameHost } from './game-host';
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,36 @@ import { applyCommandSchema } from './command-validate';
|
||||||
import { parseCommandSchema } from './schema-parse';
|
import { parseCommandSchema } from './schema-parse';
|
||||||
import {AsyncQueue} from "@/utils/async-queue";
|
import {AsyncQueue} from "@/utils/async-queue";
|
||||||
|
|
||||||
export type CommandRegistry<TContext> = Map<string, CommandRunner<TContext, unknown>>;
|
type CanRunParsed = {
|
||||||
|
runParsed<T=unknown>(command: Command): Promise<CommandResult<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
type CmdFunc<TContext> = (this: TContext, ...args: any[]) => Promise<unknown>;
|
||||||
|
export class CommandRegistry<TContext> extends Map<string, CommandRunner<TContext>>{
|
||||||
|
register<TFunc extends CmdFunc<TContext> = CmdFunc<TContext>>(schema: CommandSchema | string, run: TFunc) {
|
||||||
|
const parsedSchema = typeof schema === 'string' ? parseCommandSchema(schema) : schema;
|
||||||
|
registerCommand(this, {
|
||||||
|
schema: parsedSchema,
|
||||||
|
async run(this: CommandRunnerContext<TContext>, command: Command){
|
||||||
|
const params = command.params;
|
||||||
|
return await run.call(this.context, ...params);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type TResult = TFunc extends (this: TContext, ...args: any[]) => Promise<infer X> ? X : null;
|
||||||
|
return function(ctx: TContext & CanRunParsed, ...args: Parameters<TFunc>){
|
||||||
|
return ctx.runParsed({
|
||||||
|
options: {},
|
||||||
|
params: args,
|
||||||
|
flags: {},
|
||||||
|
name: parsedSchema.name,
|
||||||
|
}) as Promise<CommandResult<TResult>>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function createCommandRegistry<TContext>(): CommandRegistry<TContext> {
|
export function createCommandRegistry<TContext>(): CommandRegistry<TContext> {
|
||||||
return new Map();
|
return new CommandRegistry();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerCommand<TContext, TResult>(
|
export function registerCommand<TContext, TResult>(
|
||||||
|
|
@ -137,7 +163,7 @@ export function createCommandRunnerContext<TContext>(
|
||||||
registry,
|
registry,
|
||||||
context,
|
context,
|
||||||
run: <T=unknown>(input: string) => runCommandWithContext(runnerCtx, input) as Promise<CommandResult<T>>,
|
run: <T=unknown>(input: string) => runCommandWithContext(runnerCtx, input) as Promise<CommandResult<T>>,
|
||||||
runParsed: (command: Command) => runCommandParsedWithContext(runnerCtx, command),
|
runParsed: <T=unknown>(command: Command) => runCommandParsedWithContext(runnerCtx, command) as Promise<CommandResult<T>>,
|
||||||
prompt,
|
prompt,
|
||||||
on,
|
on,
|
||||||
off,
|
off,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue