The player acts, the engine resolves it against chapter data, emits events and saves; the narrator turns those facts into text through the server route, or locally if the LLM fails.
Architecture · 9 nodes · 2 flows
1Game interface → Game engineplayer action
Service / compute
Data store
Queue / messaging
AI
Client
Synchronous
Async / loop
Scroll sideways to see the full diagram
How it flows, step by step
Click a step to jump to it. Click a component for details.
What it does
A browser-based, bilingual dark-fantasy RPG with character creation, turn-based D20 combat, puzzles, inventory and a multi-chapter campaign. The game engine owns every rule and outcome, while an LLM turns resolved events into narration and generates new chapters that must pass schema and playability validation before they can be played.
The problem
LLM-driven games drift: models forget the campaign, invent outcomes and break rules. The goal was an AI Dungeon Master that is expressive but can never change authoritative game state.
What I built
Strict separation between a pure game engine (combat, dice, intent, puzzles, campaign) and an LLM narration layer that never mutates state.
Chapters are standardized, validated data, with ten authored chapters and a story graph of choices and conditions instead of hardcoded flow.
Headless playthrough harness drives the engine across every archetype and origin, solving and abandoning puzzles, and asserts the player is never stranded.
Save system with legacy migration and campaign-progress folding, so persistence never depends on the model remembering the story.
Domain event bus feeds the narrator; narration length is a single budget shared by prompt and client so output is written within limits, not truncated.
LLM chapter generation: a server route asks the model for an outline and a full bilingual chapter, then validates it with a Zod schema and a graph validator, repairing it for up to three attempts.
Validator rejects any chapter with a reachable state that can no longer finish (inescapable loops), a defect found when running real generated chapters; transport failures retry separately from model repair attempts.
Optional WebSocket server for multiplayer rooms.
Key decisions and why
01
The engine is authoritative, the LLM only narrates
Dice, combat and story state are deterministic code. The model receives resolved events and returns prose, so outcomes are reproducible and testable and the AI cannot cheat or contradict the rules.
02
Chapters as data, not code
Moving chapters into a standard validated format removes hardcoded ids from the engine and makes new chapters, including LLM-generated ones, safe to load and verifiable offline.
03
LLM calls go through a server route
The browser calls a Next.js API route which holds the API key and talks to the provider, keeping the secret server-side and the client simple.
04
Puzzles are always solvable offline
Puzzles never call the LLM and share one shape across engine, validator and chapter contract, so progression can't stall on a bad generation.
05
Generated chapters pass the same gate as authored ones
The server never returns a chapter that has not passed both the schema and the full reachability validator, so a bad generation returns an error (422) instead of a broken campaign.