import {Command, CommandSchema, parseCommand, parseCommandSchema, applyCommandSchema} from "../utils/command"; export type RuleState = 'running' | 'yielded' | 'waiting' | 'invoking' | 'done'; export type InvokeYield = { type: 'invoke'; rule: string; command: Command; }; export type RuleYield = string | CommandSchema | InvokeYield; export type RuleContext = { type: string; schema?: CommandSchema; generator: Generator>; parent?: RuleContext; children: RuleContext[]; state: RuleState; resolution?: T; } export type RuleDef = { schema: CommandSchema; create: (this: GameContextLike, cmd: Command) => Generator>; }; export type RuleRegistry = Map>; export function createRule( schemaStr: string, fn: (this: GameContextLike, cmd: Command) => Generator> ): RuleDef { return { schema: parseCommandSchema(schemaStr, ''), create: fn as RuleDef['create'], }; } function isInvokeYield(value: RuleYield): value is InvokeYield { return typeof value === 'object' && value !== null && 'type' in value && (value as InvokeYield).type === 'invoke'; } function parseYieldedSchema(value: string | CommandSchema): CommandSchema { if (typeof value === 'string') { return parseCommandSchema(value, ''); } return value; } function parseCommandWithSchema(command: Command, schema: CommandSchema): Command { return applyCommandSchema(command, schema).command; } function pushContextToGame(game: GameContextLike, ctx: RuleContext) { game.contexts.value = [...game.contexts.value, { value: ctx } as any]; game.addRuleContext(ctx); } function discardChildren(game: GameContextLike, parent: RuleContext) { for (const child of parent.children) { game.removeRuleContext(child); const ctxIdx = game.contexts.value.findIndex((c: any) => c.value === child); if (ctxIdx !== -1) { const arr = [...game.contexts.value]; arr.splice(ctxIdx, 1); game.contexts.value = arr; } } parent.children = []; parent.state = 'yielded'; } function validateYieldedSchema(command: Command, schema: CommandSchema): boolean { const requiredParams = schema.params.filter(p => p.required); const variadicParam = schema.params.find(p => p.variadic); if (command.params.length < requiredParams.length) { return false; } if (!variadicParam && command.params.length > schema.params.length) { return false; } const requiredOptions = schema.options.filter(o => o.required); for (const opt of requiredOptions) { const hasOption = opt.name in command.options || (opt.short && opt.short in command.options); if (!hasOption) { return false; } } return true; } function invokeChildRule( game: GameContextLike, ruleName: string, command: Command, parent: RuleContext ): RuleContext { const ruleDef = game.rules.get(ruleName)!; const ctx: RuleContext = { type: ruleDef.schema.name, schema: undefined, generator: ruleDef.create.call(game, command), parent, children: [], state: 'running', resolution: undefined, }; parent.children.push(ctx); pushContextToGame(game, ctx); return stepGenerator(game, ctx); } function resumeInvokingParent( game: GameContextLike, childCtx: RuleContext ): RuleContext | undefined { const parent = childCtx.parent; if (!parent || parent.state !== 'invoking') return undefined; parent.children = parent.children.filter(c => c !== childCtx); const result = parent.generator.next(childCtx); if (result.done) { (parent as RuleContext).resolution = result.value; (parent as RuleContext).state = 'done'; const resumed = resumeInvokingParent(game, parent); return resumed ?? parent; } else if (isInvokeYield(result.value)) { (parent as RuleContext).state = 'invoking'; const childCtx2 = invokeChildRule(game, result.value.rule, result.value.command, parent); return childCtx2; } else { (parent as RuleContext).schema = parseYieldedSchema(result.value); (parent as RuleContext).state = 'yielded'; } return parent; } function stepGenerator( game: GameContextLike, ctx: RuleContext ): RuleContext { const result = ctx.generator.next(); if (result.done) { ctx.resolution = result.value; ctx.state = 'done'; const resumed = resumeInvokingParent(game, ctx as RuleContext); if (resumed) return resumed as RuleContext; } else if (isInvokeYield(result.value)) { const childRuleDef = game.rules.get(result.value.rule); if (childRuleDef) { ctx.state = 'invoking'; const childCtx = invokeChildRule(game, result.value.rule, result.value.command, ctx as RuleContext); return childCtx as RuleContext; } else { ctx.schema = parseYieldedSchema(''); ctx.state = 'yielded'; } } else { ctx.schema = parseYieldedSchema(result.value); ctx.state = 'yielded'; } return ctx; } function invokeRule( game: GameContextLike, command: Command, ruleDef: RuleDef, parent?: RuleContext ): RuleContext { const ctx: RuleContext = { type: ruleDef.schema.name, schema: undefined, generator: ruleDef.create.call(game, command), parent, children: [], state: 'running', resolution: undefined, }; if (parent) { discardChildren(game, parent); parent.children.push(ctx as RuleContext); parent.state = 'waiting'; } pushContextToGame(game, ctx as RuleContext); return stepGenerator(game, ctx); } export function dispatchCommand(game: GameContextLike, input: string): RuleContext | undefined { const command = parseCommand(input); if (game.rules.has(command.name)) { const ruleDef = game.rules.get(command.name)!; const typedCommand = parseCommandWithSchema(command, ruleDef.schema); const parent = findYieldedParent(game); return invokeRule(game, typedCommand, ruleDef, parent); } for (let i = game.ruleContexts.length - 1; i >= 0; i--) { const ctx = game.ruleContexts[i]; if (ctx.state === 'yielded' && ctx.schema) { if (validateYieldedSchema(command, ctx.schema)) { const typedCommand = parseCommandWithSchema(command, ctx.schema); const result = ctx.generator.next(typedCommand); if (result.done) { ctx.resolution = result.value; ctx.state = 'done'; const resumed = resumeInvokingParent(game, ctx); return resumed ?? ctx; } else if (isInvokeYield(result.value)) { ctx.state = 'invoking'; const childCtx = invokeChildRule(game, result.value.rule, result.value.command, ctx); return childCtx; } else { ctx.schema = parseYieldedSchema(result.value); ctx.state = 'yielded'; } return ctx; } } } return undefined; } function findYieldedParent(game: GameContextLike): RuleContext | undefined { for (let i = game.ruleContexts.length - 1; i >= 0; i--) { const ctx = game.ruleContexts[i]; if (ctx.state === 'yielded') { return ctx; } } return undefined; } export type GameContextLike = { rules: RuleRegistry; ruleContexts: RuleContext[]; contexts: { value: any[] }; addRuleContext: (ctx: RuleContext) => void; removeRuleContext: (ctx: RuleContext) => void; parts: { collection: { value: Record }; add: (...entities: any[]) => void; remove: (...ids: string[]) => void; get: (id: string) => any; }; regions: { collection: { value: Record }; add: (...entities: any[]) => void; remove: (...ids: string[]) => void; get: (id: string) => any; }; pushContext: (context: any) => any; popContext: () => void; latestContext: (type: string) => any | undefined; };