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>
7.5 KiB
7.5 KiB
Product Requirements Document: Dynamic Encounters and Goal Registration
1. Overview
The Mardonar Encounter Engine is a Discord-native, LLM-driven D&D encounter system. Currently, encounters rely on static specs with a predefined list of goals. If player actions diverge significantly from the spec's options, the LLM has to force an awkward resolution or ignore the player's creative agency.
This document defines the requirements for a new Dynamic Goal Registration mechanic. This feature enables the LLM to register custom goals on the fly during play. It also outlines three updated encounter specs showcasing increased versatility and randomizable elements.
2. Dynamic Goal Registration Mechanic
2.1 User Experience / Gameplay Flow
- The encounter starts with 2–3 static base goals.
- Players take an unexpected or highly creative action (e.g., instead of fighting or running, they bribe a hostile NPC and invite them to a heist).
- The LLM detects that the current predefined goals are unrealistic or do not match the player's direction.
- The LLM calls the
goal_registertool to define a new goal with a custom ID, description, and status (primary vs. secondary). - The bot acknowledges the new goal in the session logs.
- For all subsequent turns, the system prompt includes this newly registered goal under
<hidden_goals>. - Once the conditions are met, the LLM resolves the encounter using the custom
outcomeIdvia theencounter_resolvetool. - The ending embed in Discord displays the custom goal's description.
2.2 System Architecture & Integration
sequenceDiagram
participant LLM as LLM (Gemma)
participant Dispatcher as Tool Dispatcher
participant Registry as Redis Session Manager
participant PromptBuilder as Prompt Builder
LLM->>Dispatcher: goal_register(id, label, isPrimary, reason)
Dispatcher->>Registry: Update SessionState.spec.goals
Registry-->>Dispatcher: Success
Dispatcher-->>LLM: Confirm tool result (system message)
Note over LLM, PromptBuilder: Next message turn starts...
PromptBuilder->>Registry: Get SessionState
Registry-->>PromptBuilder: SessionState (with new goal)
PromptBuilder->>LLM: Injected System Prompt (contains new goal in <hidden_goals>)
2.3 Technical Specifications
A. Tool: goal_register
- Name:
goal_register - File Location:
src/harness/tools/goalRegister.ts - Arguments:
id(string): Unique kebab-case ID (e.g.,bribe_and_recruit). Must match regex^[a-z0-9-_]+$.label(string): Precise description of the trigger conditions.isPrimary(boolean): Whether it is a primary driver or a secondary fallback.reason(string): Justification for the on-the-fly goal registration (used for logging and debugging).
- Behavior:
- Auto-prefixes the goal ID with
dynamic_(if not already present) to separate custom runtime goals from spec goals. - Validates that the goal ID matches the regex
^[a-z0-9-_]+$and doesn't conflict with existing goals. - Enforces a cap of at most 2 dynamic goals per session to prevent infinite loop goal creation.
- Enforces a cap of at most 20 messages in the session history before blocking any new goal registrations, ensuring the encounter winds down.
- Updates the active
SessionStatein Redis, appending the new goal tospec.goals.primary(ifisPrimaryis true) orspec.goals.secondary. - Returns a system message confirmation:
[TOOL] New hidden goal registered on the fly: <finalId> - <label>.
- Auto-prefixes the goal ID with
B. Prompt Assembly
- No changes to promptBuilder.ts are required, as it already maps over
spec.goals.primaryandspec.goals.secondary. By mutating the session's copies of these arrays, the prompt builder automatically picks them up for subsequent LLM turns.
C. Resolution Integration
- resolution.ts already handles unregistered
outcomeIds gracefully, but with this change, the newly registered goal is present inspec.goals, allowing it to find the goal and show its description text as the "Outcome" field.
3. Versatile Encounter Specs
The following specs demonstrate the use of deep randomization combined with dynamic goal generation.
3.1 "The Silt Leak" (mardonar-silt-leak-005)
- Location: Lower Silt District sewer junction.
- Base Goals:
leak_patched: Seal the pipeline directly using tools or magic (DC 13).refinery_sabotaged: Force a shutdown at the release valve with community help.district_evacuated: Give up on containment and prioritize fleeing safely.
- Randomizable Fields:
leak_substance: Spills either Caustic Rust-Blight Silt (corrodes gear), Sleeping Ether-Vapor (induces sleep/exhaustion), or Wild-Magic Slurry (magic surges).leak_complication: Random blockage (e.g.mutated_silt_ratsnesting in the pipes, acorroded_lockon the manual valve, or agreedy_scavengerholding the master key).
- Dynamic Goal Trigger: If players use magic to freeze or transmute the pipes, the LLM registers a new containment method outcome.
3.2 "The Velvet Quill Auction" (mardonar-velvet-auction-006)
- Location: Hidden lounge in the Velvet Quill parlor.
- Base Goals:
artifact_stolen: Steal the item from the vault or target’s pocket.fake_swapped: Replace the item with a forged replica during a distraction.brawl_outbreak: Force a chaotic combat, resulting in a blind grab for the item.
- Randomizable Fields:
auction_item: Ancient Mawfang Bloodstone, Consortium Black Book, or Fossilized Phase-Spider Egg.buyer_leverage: Karr's secret vulnerability (e.g.superstitious,heavy_drinker, orimpostorwho is actually broke).
- Dynamic Goal Trigger: If players manipulate the bidding process to ruin the rival financially or get him arrested, the LLM registers an economic victory outcome.
3.3 "The Whispering Stone" (mardonar-whispering-stone-007)
- Location: Misty road near Stormscar Peak.
- Base Goals:
spirit_calmed: Calm the spectral captain using history or oaths.ward_restored: Repair the runic stone before the frost barrier collapses.spirit_destroyed: Banish the ghost in combat, fracturing the stone.
- Randomizable Fields:
spectral_origin: The spirit is either a Shattered Vanguard Captain, a Terrified Child Acolyte, or a Corrupted Witch Hunter.frost_hazard: Extreme environment (e.g.sapping_colddraining spells,ice_shardsarea damage, ormisty_mirroringclones).
- Dynamic Goal Trigger: If players use emotional/ritual containment to siphon the spirit's sorrow into a vessel, the LLM registers a binding outcome.
4. Acceptance Criteria
- Verification of
goal_registertool registration: The tool must be recognized byVALID_TOOL_NAMESand listed in the<tool_contract>system prompt. - Session Persistence: When the tool is called, the Redis state must be successfully updated.
- Goal Recognition: In subsequent LLM calls, the dynamically generated goal must be present in the system prompt
<hidden_goals>list. - Resolution Verification: The encounter can be successfully resolved with the new custom outcome ID, producing a Discord embed that lists the custom goal's description.
- Robust Tests: Unit tests must cover the tool handler, validation of arguments (e.g., regex checks on
id), and the updates to the session state.