feat(engine): add message queue, triggers, and orchestrators
New @tts/engine package: the pure message layer that drives bgm games. MessageQueue snapshots and drains on tick; TriggerRegistry matches by type/id with runtime enable/disable; runOrchestrator suspends on wait until a matching message is processed. Engine ties them together behind the CommandHost contract that @tts/tabletop will implement.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Triggers — declarative reactive glue.
|
||||
*
|
||||
* A trigger matches a message by `type` and named params, and emits messages
|
||||
* in response. It is declared as data, keyed by `role+type+id` like other
|
||||
* defs, and collision-checked the same way. `match` binds named params from
|
||||
* the payload (like a route's candidates); `emit` uses the command-id-as-key
|
||||
* convention. Multiple triggers can match the same message — both fire.
|
||||
*/
|
||||
import type { Message } from './message.js';
|
||||
|
||||
export interface Trigger {
|
||||
/** The message type this trigger matches. */
|
||||
type: string;
|
||||
/** Optional identity, for runtime enable/disable and collision checks. */
|
||||
id?: string;
|
||||
/** Named params that must equal the corresponding fields in `msg.data`. */
|
||||
match?: Record<string, unknown>;
|
||||
/** Messages to emit when the trigger matches. */
|
||||
emit: Message[];
|
||||
}
|
||||
|
||||
/** A trigger matches when its `type` and every `match` param line up. */
|
||||
export function triggerMatches(t: Trigger, msg: Message): boolean {
|
||||
if (t.type !== msg.type) return false;
|
||||
if (!t.match) return true;
|
||||
const data = msg.data as Record<string, unknown> | undefined;
|
||||
if (!data) return false;
|
||||
return Object.entries(t.match).every(([k, v]) => data[k] === v);
|
||||
}
|
||||
|
||||
/**
|
||||
* A registry of triggers, keyed by `type#id`. `register` collision-checks the
|
||||
* key; `enable`/`disable` toggle a trigger at runtime (an orchestrator's
|
||||
* "no more placements this turn" control). `match` returns every enabled
|
||||
* trigger that matches a message.
|
||||
*/
|
||||
export class TriggerRegistry {
|
||||
private triggers = new Map<string, Trigger>();
|
||||
private enabled = new Set<string>();
|
||||
|
||||
register(t: Trigger): void {
|
||||
const key = triggerKey(t);
|
||||
if (this.triggers.has(key)) {
|
||||
throw new Error(`Duplicate trigger: ${key}`);
|
||||
}
|
||||
this.triggers.set(key, t);
|
||||
this.enabled.add(key);
|
||||
}
|
||||
|
||||
unregister(t: Trigger): void {
|
||||
const key = triggerKey(t);
|
||||
this.triggers.delete(key);
|
||||
this.enabled.delete(key);
|
||||
}
|
||||
|
||||
enable(type: string, id?: string): void {
|
||||
this.enabled.add(triggerKey({ type, id }));
|
||||
}
|
||||
|
||||
disable(type: string, id?: string): void {
|
||||
this.enabled.delete(triggerKey({ type, id }));
|
||||
}
|
||||
|
||||
/** Every enabled trigger matching `msg`. */
|
||||
match(msg: Message): Trigger[] {
|
||||
const out: Trigger[] = [];
|
||||
for (const [key, t] of this.triggers) {
|
||||
if (this.enabled.has(key) && triggerMatches(t, msg)) out.push(t);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
function triggerKey(t: Pick<Trigger, 'type' | 'id'>): string {
|
||||
return t.id ? `${t.type}#${t.id}` : t.type;
|
||||
}
|
||||
Reference in New Issue
Block a user