FU-12 — velvet-auction.yaml now uses the group-encounter tools: - minPlayers: 3 (lobby-gated party heist, matches PRD UJ-1) - passiveReveals: Insight/15 (notices Karr's tell — Feature B) - group_stealth skillChecks entry (group Stealth, successRule: majority, durationSeconds: 60) + skill_check_group_emit and character_status added to the tools list. - specsToolsConsistency: emptied the NOT_YET_REFERENCED allowlist (skill_check_group_emit + character_status are now referenced); all 8 registered tools are reachable from specs. Validated: specLoader + specsToolsConsistency + full unit suite (527) pass. FU-9 — docs/release-playtest-checklist.md: the 7-step manual pre-release multi-player playtest checklist checked into the repo as a release gate (was buried only in the arch doc). Includes pass criteria (no orphaned thread / lost roll / raw-JSON leak) + the NFR-3/NFR-4 latency checklist. docs/project-overview.md drift fix: pino -> src/lib/logger.ts (custom plaintext, ADR-002); primary LLM -> minimax-m3 via LiteLLM (LITELLM_MODEL); test count 22 -> 58; lib/ description; relabel dynamic goal registration as delivered. Co-Authored-By: Claude <noreply@anthropic.com>
6.9 KiB
Mardonar Encounter Engine — Project Overview
Discord-native, LLM-driven D&D encounter engine. Generated 2026-06-19 from a deep scan.
What it is
A Discord bot that runs structured D&D encounters. Each Discord thread is an encounter session. The bot loads a YAML spec, narrates the scene via an LLM (minimax-m3 through LiteLLM, with Ollama as fallback), voices NPCs with stable personas, runs skill checks via Discord embeds, and persists NPC memory + encounter history into a graph database through GraphMCP (JSON-RPC over HTTP). Optional Foundry VTT integration pulls live character stats and awards XP via an external relay.
Who it serves
Discord community members playing D&D 5e in the Land of Mardonar. The DM runs /encounter start <spec> to begin; players post their actions in the resulting thread. NPC personas are loaded from specs and grounded in long-term graph memory so that recurring NPCs remember prior interactions across encounters.
Tech stack at a glance
| Layer | Technology |
|---|---|
| Runtime | Node.js 22 (ESM, TypeScript 5.8 strict) |
| Discord | discord.js v14 |
| LLM (primary) | LiteLLM proxy — minimax-m3 (env: LITELLM_BASE_URL, LITELLM_MODEL) |
| LLM (fallback) | Ollama (env: OLLAMA_BASE_URL) — gemma4-it:e2b, 128k context |
| Session cache | Redis (ioredis), 12h TTL |
| Graph DB | Neo4j (via GraphMCP JSON-RPC, not direct) |
| Lore / NPC memory | GraphMCP HTTP JSON-RPC server |
| Foundry VTT | External relay (optional, requires API key) |
| Validation | Zod (env + encounter spec) |
| Logging | src/lib/logger.ts (custom plaintext — pino removed) |
| Testing | Vitest 3 (unit + integration) |
| Build | tsc → multi-stage Node 22 alpine Dockerfile |
Architecture type
Layered backend with a plugin-style tool registry.
Discord ──▶ src/bot/ (commands, embeds, handlers)
│
▼
src/harness/ (promptBuilder, contextAssembler,
llmClient, toolParser, toolDispatcher,
tools/* plugin registry)
│
┌────────────┼────────────┐
▼ ▼ ▼
Redis GraphMCP VTT relay
(session (JSON-RPC: (Foundry
state) NPC memory, live stats,
lore, log) XP grants)
Repository structure
Single-part monolith. All source under src/. The bot is one Node.js process that talks to external services over the network.
src/
├── bot/ # Discord I/O (commands, embeds, event handlers)
├── harness/ # LLM orchestration + 6 tool plugins
├── session/ # Redis-backed registries + session state
├── graphmcp/ # JSON-RPC client + Redis stream ingest
├── vtt/ # Foundry VTT relay client + spin-up
├── db/ # ioredis singleton
├── spec/ # YAML encounter loader + Zod schema
├── persona/ # persona.yaml loader
├── config.ts # Zod env validation
├── lib/ # logger (custom plaintext), historyTrim, skillCheckMessages
├── scripts/ # deploy-commands (slash command registration)
└── types/ # shared interfaces + CONTEXT_BUDGET
Plus specs/ (8 encounter YAML files), tests/ (58 test files), data/ (runtime tally + summaries), and Docs/ (pre-existing project documentation, partially out of date).
Documentation
- Architecture — full system design
- Source Tree Analysis — annotated directory tree
- Component Inventory — reusable components
- Development Guide — setup, run, test, troubleshoot
- Deployment Guide — production deploy + ops
- API Contracts — Discord commands + GraphMCP JSON-RPC
- Data Models — session state, encounter spec, Neo4j graph
Key features in the current codebase
- Per-encounter tool filtering. Each spec declares which tool plugins are active.
- Dynamic goal registration (delivered) —
tools/goalRegister.tslets the LLM add new goals mid-encounter. - Three-pattern tool parser — handles fenced
tool_call, baretool_callheader, and fuzzy bare JSON, so even smaller models can drive tools. - Self-spinning VTT relay — when the relay is down, the bot handshakes via RSA-OAEP and launches a headless Foundry session on demand.
- Burst cap with drop notices — if too many messages arrive before the last LLM response, the bot drops the excess and posts a tone-aware notice.
- Reaction lifecycle (👀) — visible "I'm working on it" feedback through queued → processing → complete states.
- NPC memory injection at session start from GraphMCP, filtered by score threshold and capped at top-3 chunks above the threshold.
- In-world voice for player-facing strings — no utility/jargon (see
feedback-in-world-voice).
Known drift and open issues
Resolved in the 2026-06-19 /loop improvement pass (see docs/architecture.md §9 for the full list with dates):
README now points to the right doc and notes the historical status ofDocs/mardonar-encounter-engine.mddescribes a Go bot with embedded MCP — superseded bydocs/architecture.mdbut still referenced by the README.Docs/mardonar-encounter-engine.md.README tree now reflects the actual 8-command layout andREADME.md's project-structure tree is out of date (mentionssrc/mcp/, missing commands).src/graphmcp/(Neo4j/oldsrc/mcp/were both retired).src/types/index.tsEncounterSpecdiverged fromsrc/spec/loader.tsZod schema (missingtone,tools,randomizable,nameKey).EncounterSpecis nowz.infer<typeof EncounterSpecSchema>— the static type and the runtime validator cannot drift.src/types/index.tsre-exports it.DuplicateExtracted totrimHistorybetweensessionManager.tsandcontextAssembler.ts.src/lib/historyTrim.ts;tests/unit/historyTrim.test.tscovers the shared module at 100%.
Still open:
- No production
docker-compose.yml, no CI/CD, no HTTP health endpoint. CI was added in 2026-06-19 (.gitea/workflows/test.yml) — the other two remain open. DISCORD_ALLOWED_USERSempty by default — channel-scoped access only.- Two tracked dead-code files at the project root:
index.tsandpromptBuilder.ts. These are stale duplicates from before the project was reorganized intosrc/;tsconfig.jsonhasrootDir: "src"so they are never compiled or imported.git rmis pending user approval.
See docs/architecture.md §9 for the canonical drift log.
When you're ready to plan new features
Point the PRD workflow at docs/index.md as input. For UI-facing work, architecture.md §5.1 is the primary reference. For backend/LLM feature work, architecture.md §5.2 and docs/data-models.md are the primary references.