Technology

How DragonSpire is built.

Three services, two databases, one rule: only one service may call an AI model, and the game rules never depend on which model it is.

The system

DragonSpire's parts and where they run: Fantasy Grounds and FGU Link on the GM's PC; Cory, Dee Emma, Campaign Manager and DragonLibrary on a cloud server; AI model providers and players on the internet; Cory is the only part that calls an AI model. GM'S PC (WINDOWS) CLOUD SERVER INTERNET JSON files in the campaign folder; local wake-up call on 127.0.0.1 WebSocket, token + TLS, sequence numbers, resend effect code lookup world reads and writes effect code prefetch WebSocket + speech-to-text HTTPS HTTPS + WebSocket Discord voice Fantasy Grounds Unity with the FGU Link extension (Lua) FGU Link daemon Python, on the GM's PC: clock server, state mirror, command runner. No AI. Cory FastAPI engine, one Docker container per campaign; SQLite campaign data The only part that calls AI Campaign Manager The world's memory: PocketBase database and web app. Worlds, places, NPCs, quests DragonLibrary The rules library: public read-only API. D&D 2024 rules, Fantasy Grounds effect codes Dee Emma Discord bot, own container, 24/7 Hears players, speaks Cory's lines AI model providers Gemini, fal.ai, TypeSafe Called by Cory only Players' browsers DragonSpire web client Players in Discord In the voice channel Cory is the brain. FGU Link and Dee Emma decide nothing on their own and call no AI model.

Cory, the engine, runs on a cloud server beside Dee Emma, the Discord voice, and Campaign Manager, the world database. FGU Link runs on the GM’s PC beside Fantasy Grounds. Cory is the only part that makes AI calls.

The parts

PartRuns onBuilt withCalls AI?
Cory cloud server, one container per campaign Python, FastAPI, SQLite yes, the only one
Dee Emma cloud server, 24/7 Python Discord bot no (speech-to-text goes through Cory)
FGU Link GM’s Windows PC Lua extension + Python daemon no, enforced by tests
Campaign Manager cloud server, shared by every Cory PocketBase database + React web app no
DragonLibrary cloud server PocketBase, public read-only API + browse website no (codes were drafted offline, then checked and tiered)

The data services: memory and rules

Campaign Manager, the world’s memory.

The GM writes the world in its own web app: a World Explorer tree of nested places (continent, region, town, tavern) with the NPCs in each, an Adventure Explorer of quests, and public pages players open from links in the chat. Fifteen collections: worlds, campaigns, locations, NPCs with backstories and motives, items, inventories, adventures, quests and quest progress. On every turn Cory hands the AI the party’s surroundings from it: the place, its parent places, what is nearby and who is there. When the party travels, Cory reuses a real place before it invents one. Party state (hit points, sheets, position) never goes there; that belongs to Fantasy Grounds and Cory’s own database.

The GM writes the world once in the Campaign Manager web app, a tree of nested places from Bethica down to a tavern with its NPCs and a quest list; each turn Campaign Manager hands Cory a context card about where the party is, while party state stays in Fantasy Grounds and is never stored in Campaign Manager. CAMPAIGN MANAGER WEB APP Bethica Rockmire region town The Rusty Lantern (tavern) Bram, innkeeper two dock workers market square Quests the GM writes Campaign Manager 15 collections: worlds, places, NPCs, items, adventures, quests CONTEXT CARD every turn Where: The Rusty Lantern (tavern) in Rockmire. Nearby: market square. Here: Bram the innkeeper, two dock workers. Coryspeaks with the card in hand Fantasy Grounds party state: hit points, sheets, the map never stored in Campaign Manager

The GM writes the world once. The AI is handed it every turn.

DragonLibrary, the rules library.

D&D 2024 rules content and thousands of Fantasy Grounds effect codes, the short strings that make Fantasy Grounds automate a spell or feature. A wrong code fails silently, so every code records whether its variants are alternatives (pick one) or parts (apply all), which Fantasy Grounds extensions it needs, and a trust tier from “generated” to “verified at a real table”. When a turn involves an effect, Cory fetches the codes first and keeps only the ones this table’s extensions support. A failed lookup is reported, not guessed.

A player casts Bless; Cory looks up the code in DragonLibrary, gets two alternative variants tagged with the Fantasy Grounds extensions they need and a trust tier, drops the one this table cannot load, sends the other to FGU Link as a ready command and gets a receipt back from Fantasy Grounds; a code made of parts is applied in full, in order. “I cast Bless.” CoryAI game master DragonLibraryeffect codes Bless alternatives: pick one, two would double the effect VARIANT A needs CORE tier verified VARIANT B needs CORE + BCEG tier documented this table loads: CORE needs BCEG: not loaded, dropped APPLY_TIMED_EFFECT: Bless; SAVE: 1d4, 10 rounds FGU Linkruns it in Fantasy Grounds RECEIPT from Fantasy Grounds ANOTHER CODE, MADE OF PARTS PARTS 1part 1 2part 2 components: apply all, in order trust tiers verified documented undocumented generated

Looked up, matched to the table, receipted. A failed lookup is reported, not guessed.

One turn, end to end

One turn in thirteen steps across five lanes: a player in Discord says “I swing my axe at the goblin.”, Dee Emma, Cory, the FGU Link daemon and Fantasy Grounds pass it along and roll it, and Dee Emma answers “Your axe bites deep.”, with the Fantasy Grounds receipt back in 0.07 to 0.16 seconds. Playerin Discord Dee EmmaDiscord voice CoryAI game master FGU Link daemonon the GM's PC Fantasy Grounds5E ruleset + extension 1 “I swing my axe at the goblin.” 2 voice clip 3 speech to text, then “for the GM? whose turn?” 4 game master model picks a tool 5 command, attack roll 6 validator checks shape, names, slots 7 command file + wake-up 8 ruleset rolls, applies damage 9 receipt + state change 10 receipt (0.07 to 0.16 s) 11 narration line 12 still current? then text to speech 13 “Your axe bites deep.” 0.07 to 0.16 s Fantasy Grounds command to receipt (live).

From a spoken sentence to a narrated result. The roll happens in Fantasy Grounds and comes back as a receipt in 0.07 to 0.16 seconds.

Where the truth lives

Where the truth lives: the AI model, for narration, NPC voices and judging what a player meant, stands on four foundations that hand it the facts before it speaks: game state in Fantasy Grounds, world memory in Campaign Manager, rules content in DragonLibrary, and fairness and pacing in tested Python code; its commands go down to Fantasy Grounds, checked and receipted. facts handed over before it speaks commands, checked and receipted AI model narration, NPC voices, judging what a player meant GAME STATE Fantasy Grounds combat tracker, sheets, map WORLD MEMORY Campaign Manager places, NPCs, quests RULES CONTENT DragonLibrary effect codes, 5E ruleset FAIRNESS AND PACING Tested Python code never rolls for players, turn order, loot rules The model is told the facts. It is never trusted to remember them.

Narration and judgement come from the model; game state comes from Fantasy Grounds; world memory from Campaign Manager; rules content from DragonLibrary; fairness from tested code. The model is given the facts before it speaks, instead of being trusted to remember them.

Hexagonal architecture

Cory follows ports and adapters. The core holds the game rules (combat and initiative, party, location and scene, dice, spell slots) and imports only the Python standard library. It reaches databases, AI models, Discord and Fantasy Grounds only through ports, and one composition root wires the adapters in. So a model, a database or a voice engine can be swapped without touching the rules, and the rules can be tested without any of them.

Model adapter: Gemini. Core: unchanged.

Cory's hexagonal architecture: a core of game rules that uses only the Python standard library, called by four driving adapters and reaching storage, AI models, Fantasy Grounds and voice only through seven groups of ports, with one composition root installing the adapters. DRIVING ADAPTERS call into the core PORTS DRIVEN ADAPTERS implement the ports installs the adapters PORTS Core src/core · standard library only combat and initiative party and claims location and scene dice engine spell slots Web and API routers adapters/api Engine loop main_loop.py FGU Link router Fantasy Grounds, WebSocket Dee Emma router Discord voice, WebSocket GameStore EngineStatePort SQLite game storecampaign data CampaignRepository Campaign ManagerPocketBase JudgementModel SpeechToText Gemini adaptersgame master, speech to text Another modelsame ports, core untouched TableEvents GmScreen Table events PartyRoster Party rosterFantasy Grounds party sheet ConfigurationPort UserDirectory Settings ToolExecutor VoiceEnginePort Tools and voiceFGU Link; Kokoro voice Composition root config/dependencies.py: wire() Checked on every push by tests: the core imports only the standard library, and only listed adapters may call a model.

The tabletop defines the AI’s abilities

Each time FGU Link connects, it sends Cory the full list of 92 Fantasy Grounds actions and 42 skills. Cory’s tool list is built from it. A new procedure is a Markdown skill file, and a lint step rejects any skill that names an action the engine does not implement.

On every connect, FGU Link sends Cory a stack of 92 Fantasy Grounds actions and 42 skills over the WebSocket, and it becomes Cory's tool list; a new skill file passes the skill lint, while one naming an unknown action is rejected. WebSocket, on every connect Fantasy Grounds with FGU Link 92 actions 42 skills Cory tool list 92 actions 42 skills 92 actions 42 skills NEW PROCEDURE skill file unknown action rejected new_skill.md Markdown passes skill lint: every action must exist

Fantasy Grounds, not Cory’s code, defines what the AI can do there.

Engineering discipline

112automated tests (97 FGU Link, 15 Cory)
6build gates, on every push
0.07 to 0.16 scommand round trip, Fantasy Grounds command to receipt
  • 112 automated tests (97 FGU Link, 15 Cory) and 6 build gates, on every push.
  • Architecture tests fail the build if the core imports an outside library, or if any file not on an approved list calls an AI model.
  • Every Fantasy Grounds command passes a validator before it runs, and returns a receipt after.
  • One authenticated WebSocket between the GM’s PC and the cloud, with sequence numbers and resend after a reconnect, so nothing is lost when the connection drops.
  • Model use is configured per job (game master, scene judge, voice gate, transcription, images), each with a primary, a backup and a timeout. A missing key means unavailable, never borrowed.