Files
zalbot/index.ts
Kaysser Kayyali 9dc6e8e1a3 Initial commit — Mardonar encounter engine with UX improvements
Includes full bot source (Phases 1–4), plus five new features:
- Epic 1: emoji reaction state machine (👀🎲) + burst queue cap at 2 with in-world drop notices
- Epic 2: per-encounter tone field in YAML injected into LLM system prompt
- Epic 3: player pronouns via modal registration + system prompt players block
- Epic 4: strengthened skill_check_emit tool contract + missed-skill-check diagnostic

Also includes UX design docs, epics, and story files under Docs/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 04:51:21 +00:00

226 lines
5.7 KiB
TypeScript

// src/types/index.ts
// Shared types used across all layers of the Mardonar Encounter Engine.
// Import from here only — do not duplicate type definitions elsewhere.
// ---------------------------------------------------------------------------
// Players
// ---------------------------------------------------------------------------
export interface Player {
discordId: string;
dndName: string;
}
// ---------------------------------------------------------------------------
// Encounter Spec
// ---------------------------------------------------------------------------
export interface NpcPersona {
/** Unique stable ID used as Neo4j node key. e.g. "miriam-vendor-mardonar" */
id: string;
name: string;
role: string;
/** Full persona description injected into the system prompt. */
persona: string;
/**
* If set, NPC memories are loaded from Neo4j at session start
* and written back on encounter_resolve.
*/
memoryKey?: string;
}
export interface EncounterGoal {
id: string;
label: string;
}
export interface EncounterGoals {
hidden: boolean;
primary: EncounterGoal[];
secondary: EncounterGoal[];
}
export interface EncounterSetting {
location: string;
mood: string;
ambientNpcs: string;
}
export interface EncounterSpec {
encounterId: string;
title: string;
setting: EncounterSetting;
openingNarrative: string;
npcs: NpcPersona[];
goals: EncounterGoals;
sportsmanshipRules: string[];
/** Skill check DCs and notes keyed by check name e.g. "chase_dc" → 13 */
skillChecks: Record<string, number | string>;
dmNotes?: string;
}
// ---------------------------------------------------------------------------
// Session State
// ---------------------------------------------------------------------------
export type SessionPhase = 'open' | 'active' | 'resolved';
export interface SessionState {
encounterId: string;
/** Discord thread snowflake ID — used as the primary session key in Redis. */
threadId: string;
guildId: string;
spec: EncounterSpec;
/** Map of discordId → Player for all players who have entered the session. */
players: Record<string, Player>;
history: ChatMessage[];
phase: SessionPhase;
/** Messages held while waiting for a player to register their DnD name. */
heldMessages: HeldMessage[];
/** Outcome goal ID set when the encounter resolves. */
outcome?: string;
outcomeSummary?: string;
createdAt: number;
updatedAt: number;
}
// ---------------------------------------------------------------------------
// Chat History
// ---------------------------------------------------------------------------
export interface ChatMessage {
role: 'system' | 'user' | 'assistant';
content: string;
/**
* Pinned messages are never removed during history trimming.
* Use for: opening narrative, goal block.
*/
pinned?: boolean;
timestamp: number;
}
export interface HeldMessage {
discordUserId: string;
content: string;
timestamp: number;
}
// ---------------------------------------------------------------------------
// LLM Harness
// ---------------------------------------------------------------------------
export interface ToolCallBlock {
tool: ToolName;
args: Record<string, unknown>;
}
export interface LLMResponse {
/** Narrative text to post to the Discord thread. */
narrative: string;
/** Parsed tool call block, if the LLM emitted one. */
toolCall?: ToolCallBlock;
/** Raw token count returned by Ollama (eval_count). */
rawTokensUsed?: number;
}
// ---------------------------------------------------------------------------
// Tools
// ---------------------------------------------------------------------------
export type ToolName =
| 'skill_check_emit'
| 'skill_check_resolve'
| 'event_log_append'
| 'npc_memory_read'
| 'npc_memory_write'
| 'encounter_resolve';
export interface SkillCheckEmitArgs {
player: string;
prompt: string;
dc: number;
}
export interface SkillCheckResolveArgs {
player: string;
skill: string;
roll: number;
modifier: number;
dc: number;
success: boolean;
}
export interface EventLogAppendArgs {
sessionId: string;
eventType: EventType;
description: string;
}
export interface NpcMemoryReadArgs {
npcId: string;
}
export interface NpcMemoryWriteArgs {
npcId: string;
memoryFact: string;
}
export interface EncounterResolveArgs {
sessionId: string;
outcomeId: string;
summary: string;
}
export type EventType =
| 'player_action'
| 'skill_check'
| 'npc_action'
| 'outcome'
| 'sportsmanship'
| 'session_start'
| 'player_joined';
// ---------------------------------------------------------------------------
// Neo4j
// ---------------------------------------------------------------------------
export interface NpcNode {
id: string;
name: string;
personaSummary: string;
memoryFacts: string[];
lastSeenEncounter: string | null;
}
export interface EncounterNode {
id: string;
title: string;
resolved: boolean;
outcomeId: string | null;
createdAt: string;
}
export interface EncounterEventNode {
timestamp: string;
type: EventType;
description: string;
}
// ---------------------------------------------------------------------------
// Context Budget
// (Exported as a const so all callers use the same values)
// ---------------------------------------------------------------------------
export const CONTEXT_BUDGET = {
/** Maximum system prompt size including all NPC personas. */
SYSTEM: 4_000,
/** Pinned messages (opening narrative + goal block). Never trimmed. */
PINNED: 2_000,
/** Sliding history window. */
HISTORY: 118_000,
/** Hard floor — stop trimming here even if still over budget. */
SAFETY: 3_500,
/** Total context window of gemma4-it:e2b. */
TOTAL: 128_000,
} as const;