mcp rpg database ai-agents self-play

Database-Driven Dungeon Mastering

What happens when you let an AI run a full D&D combatβ€”as both DM and all four players? I playtested my RPG engine to see if database-backed state could maintain coherent narrative across dice, movement, and mayhem.

V
Vario aka Mnehmos

🎲 The Experiment

I built an RPG MCP server with 135 tools covering combat, world generation, spatial navigation, and character management. Then I asked Claude to play a complete D&D encounterβ€”controlling the dungeon master AND all four party members simultaneously. The goal: prove that database-backed state enables coherent narrative even when one AI plays both sides of the table.

The Setup: World, Party, and Persistence

The RPG engine uses SQLite as its ground truth. Every entityβ€”worlds, characters, items, quests, combat encountersβ€”lives in the database. The AI agent has no memory between tool calls; it reconstructs context by querying storage.

SESSION INITIALIZATION
Created Entities
  • World: The Shattered Isles (30Γ—30 grid, 10 regions)
  • Party: The Wayfarers (4 members)
  • Location: The Salty Anchor tavern
  • Quest: Clear the Coastal Caves
All state persisted to SQLite before combat began

The party consisted of four characters with distinct roles:

K

Kira Stoneheart

Human Fighter, Level 3

HP: 28 β€’ AC: 18 β€’ STR: 16
E

Elara Moonshadow

Elf Wizard, Level 3

HP: 18 β€’ AC: 12 β€’ INT: 18
M

Brother Marcus

Human Cleric, Level 3

HP: 24 β€’ AC: 16 β€’ WIS: 16
S

Shade

Halfling Rogue, Level 3

HP: 21 β€’ AC: 14 β€’ DEX: 18

The Combat: 5 Goblins vs 4 Adventurers

The encounter used spawn_quick_enemy to generate 5 goblins on a 20Γ—20 grid with obstacles. The engine handled initiative rolls automaticallyβ€”each combatant's DEX modifier + d20 determined turn order.

// Combat map after Round 1
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
0 . . . . . . . . . . . . . . . . . . . .
1 . . . . . . . . . . . . . . . . . . . .
2 . . . . . . . . . . . . . . . . . . . .
3 . . . . . . . . . . . . . . . . . . . .
4 . . . . . . . . . . . . . . . . . . . .
5 . . . K . . . . . . E . . . . . . . . .    K = Kira (Fighter)
6 . . . . . β–ˆ β–ˆ . . . . . . . . . . . . .    E = Elara (Wizard)
7 . . . . . β–ˆ β–ˆ . . . . G . . . . . . . .    M = Marcus (Cleric)
8 . . . . . . . . . G . . . G . . . . . .    S = Shade (Rogue)
9 . . . . M . . . . . . . . . . . . . . .    G = Goblin
0 . . . . . . . . . . . . . . . . . . . .    β–ˆ = Obstacle
1 . . . . . . . . . . . . . . G . . . . .    ☠ = Defeated
2 . . . . . . . . S . . . . . . . . . . .

What made this work wasn't the AI's reasoningβ€”it was the database constraints. Movement costs were calculated from grid positions. Attack rolls checked AC from the target's record. HP damage was persisted immediately. The AI couldn't cheat even if it wanted to.

Key Findings: What the Playtest Revealed

1. Errors Are Teachers

When I tried to move Shade to position (12,8), the engine rejected it: "Position blocked by obstacle". When I used the wrong actor ID, it said: "Actor not in combat". Each error guided the next action.

Error: Actor ID 'char-shade-xxx' not found in combat.
Hint: Use combat_manage(action: "get") to see valid participant IDs.

2. Dice Create Narrative

The AI didn't decide if attacks hitβ€”the d20 did. Shade rolled a natural 18 on his sneak attack. Brother Marcus's Cure Wounds rolled 8 on 1d8+3. These random outcomes created emergent drama that felt authentic.

Shade's Attack
18 + 6 = 24 vs AC 13
HIT - 13 damage (3d6+3)
Marcus's Heal
1d8+3 = 8 HP restored
Shade: 9 β†’ 17 HP

3. HP Syncs Automatically

Combat maintains its own HP snapshots. When combat ended, the engine called combat_manage(action: "end") and synced final HP back to persistent character records. Shade's 17 HP (after healing) was saved to his character file. Next session, he starts damaged.

4. Movement Math is Non-Negotiable

Every character has a speed (30ft for most). The grid uses 5ft squares. When I tried to move 35ft with 30ft remaining, the engine rejected it. No "close enough"β€”the database enforces the rules even when the AI forgets them.

The Outcome: Goblin-3 Falls

After two rounds, the party had eliminated one goblin and positioned for a counterattack. The combat log tells the mechanical story; the AI narrates the drama.

Combat Timeline

Round 1
Goblins swarm Shade (12 damage). Party repositions.
Round 2
Marcus heals Shade (+8 HP). Shade flanks and kills Goblin-3 (13 damage sneak attack).
Result
Party: 4/4 alive. Goblins: 4/5 remaining. HP synced to character records.
╔══════════════════════════════════════════════════════════╗
β•‘                    COMBAT ENDED                          β•‘
╠══════════════════════════════════════════════════════════╣
β•‘  HP Sync Complete:                                       β•‘
β•‘    Kira Stoneheart:   28/28 HP (unchanged)              β•‘
β•‘    Elara Moonshadow:  18/18 HP (unchanged)              β•‘
β•‘    Brother Marcus:    24/24 HP (unchanged)              β•‘
β•‘    Shade:             17/21 HP (took 12, healed 8)      β•‘
β•‘                                                          β•‘
β•‘  Enemies Defeated: 1 (Goblin-3)                         β•‘
β•‘  Enemies Remaining: 4                                    β•‘
β•‘                                                          β•‘
β•‘  State persisted to: rpg-mcp.db                         β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

Why This Matters: Database as Game Master

The traditional approach to AI game masters is to stuff everything into the prompt: character stats, world state, combat history. But prompts have limits, and context windows get expensive.

The database-driven approach inverts this. The AI knows nothing except what it queries. Want to know Shade's HP? Call character_manage(action: "get"). Want to see the battlefield? Call combat_map(action: "render"). The database is the single source of truth.

PRINCIPLE The AI is the narrator, not the referee

Database enforces rules. AI describes consequences.

PRINCIPLE Errors guide, not block

Every rejection includes context for the correct action.

PRINCIPLE Dice create stakes

Random outcomes prevent AI from "choosing" winners.

PRINCIPLE State persists, context doesn't

Session can end anytime. Database remembers everything.

The result? Coherent narrative across any number of sessions. The AI can play both sides because the database prevents favoritism. Combat feels fair because dice determine outcomes. And when something goes wrong, the error message tells you exactly what to do next.

The Toolbox: 135 RPG Tools

The mnehmos.rpg.mcp server consolidates 135 tools into action-based interfaces. Here's what powered this playtest:

Session & World

  • session_manage (initialize, get_context)
  • world_manage (generate, get_state)
  • spatial_manage (generate, look)

Characters & Party

  • character_manage (create, get, update)
  • party_manage (create, add_member)
  • inventory_manage (give, equip)

Combat

  • combat_manage (create, get, end)
  • combat_action (attack, move, heal)
  • combat_map (render, aoe)

Narrative

  • quest_manage (create, assign, complete)
  • npc_manage (get_context, interact)
  • narrative_manage (add, get_context)

The Takeaway

Can an AI play D&D with itself? Yesβ€”but only because the database is the real dungeon master. The AI describes, proposes, and narrates. The database validates, persists, and enforces.

This architecture scales beyond games. Any AI workflow that requires consistent state across sessions benefits from externalizing truth to a database. The LLM becomes the interface layerβ€”translating intent into validated actions and results into narrative.

"The agent isn't smartβ€”the database is. The agent is just hands."

Next experiment: multi-agent D&D where different Claude instances control different characters, coordinated through the same database. The dungeon master becomes truly neutralβ€”just another agent with access to the same tools.