An AI-powered Game Master that runs a fully-rules-aware 5e (SRD) tabletop roleplaying game — solo or with friends, in the browser, on any device.
DiceOnRails pairs a large language model (for narration, roleplay, and intent parsing) with a deterministic game engine (for dice, combat, inventory, spells, leveling, and all the math). The LLM tells the story; the engine enforces the rules. Neither one fudges the other.
- AI Storyteller + Rules Engine — The LLM narrates the world; the engine rolls every die, tracks every HP, and validates every spell. No hallucinated crits.
- Full 5e SRD mechanics — Classes, subclasses, races, feats, skills, spell slots, concentration, conditions, death saves, multi-character parties, and a 1–20 XP progression.
- Function-calling agent loop — The GM "thinks" by calling tools (
player_attack,cast_spell,check_skill,move_to,narrate_turn…) up to 20 iterations per turn, then narrates the result. - Generated atmosphere art — Auto-commissioned scene art (via ImageRouter / SDXL-Turbo) for every tavern, dungeon, and forest you enter.
- Character portraits — Each hero gets an auto-generated portrait on creation (seeded from their appearance text), shown in the sheet, the input bar, and beside their chat bubbles. Regenerate anytime from the persona modal.
- Voice narration — In-browser text-to-speech reads the GM's prose aloud.
- Solo or multiplayer — Play alone (anonymous/local) or invite friends to a shared cloud campaign with realtime sync.
- Interactive Virtual Tabletop (VTT) — Live 5e tactical grid map featuring drag-and-drop tokens, Chebyshev distance calculations, and LLM spatial awareness.
- Cross-platform UI — The "Emberlight" interface (obsidian + ember-gold design system): one responsive shell for desktop and mobile, voice input, multiplayer pending-batch chat with typing indicators, dice popups, and a searchable / exportable chat log.
- Cloud saves & campaigns — Powered by Supabase (Postgres + Realtime + Auth).
- Works with any OpenAI-compatible provider — Default is OpenRouter; bring your own API key.
┌────────────┐ player text ┌──────────────┐ tool calls ┌──────────────┐
│ Your │ ───────────────────▶ │ LLM Agent │ ───────────────▶ │ Game Engine │
│ Input │ │ (narrator) │ ◀─── results ─── │ (deterministic)│
└────────────┘ └──────────────┘ └──────────────┘
│ │
│ final prose │ mutated state
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Chat Log + │ │ Supabase / │
│ Dice Cards │ │ LocalSave │
└──────────────┘ └──────────────┘
- You type an action ("I attack the goblin with my longsword").
- The LLM agent loop translates that into structured tool calls (
player_attack,next_turn,narrate_turn). - The game engine executes each tool deterministically — rolling real cryptographic dice, applying damage, ticking conditions, awarding XP.
- Tool results are fed back to the LLM until it produces a final narration.
- State is synced to Supabase (or
localStoragein solo mode) and rendered with dice cards, HP bars, and atmosphere art.
- Node.js 20+ (the test suite targets Node 20)
- An LLM API key — get one free at https://openrouter.ai/keys
- (Optional) An ImageRouter API key for atmosphere art — https://imagerouter.io
- (Optional) A Supabase project for cloud saves & multiplayer — https://supabase.com
npm install
npm run devThe first time you run npm run dev, the preflight script (scripts/preflight.js) detects the missing .env and automatically launches a Web Setup Wizard in your browser. Fill in your API keys there and it will write the .env for you.
Alternatively, copy .env.example to .env and fill it in manually:
cp .env.example .env# Required — narration
VITE_LLM_API_KEY=sk-or-v1-xxxxxxxxxxxxxxxxxxxxxxxx
VITE_LLM_API_BASE=https://openrouter.ai/api/v1
VITE_LLM_MODEL=deepseek/deepseek-v4-flash
# Optional — scene art (leave blank to disable)
VITE_IMAGE_ROUTER_API_KEY=
VITE_IMAGE_MODEL=stabilityai/sdxl-turbo
# Optional — cloud saves & multiplayer (leave blank for solo/local only)
VITE_SUPABASE_URL=
VITE_SUPABASE_ANON_KEY=Tip: Any OpenAI-compatible endpoint works. Point
VITE_LLM_API_BASEathttps://api.openai.com/v1, your local Llama.cpp/Ollama server, or any other provider.
Open http://localhost:3000, watch the splash, then either:
- Play anonymously — solo, saved to
localStorage. No account needed. - Sign in — create an account (Supabase Auth) to enable cloud campaigns.
Create a campaign → choose Quick Start (a pre-made hero) or Custom creation (Name → Race → Class → Stats → Skills → Feats → Spells → Gear → Starting Grounds) → start playing.
The Emberlight UI is a single responsive shell: a collapsible, drag-resizable dock on desktop becomes a bottom-tab navigator on mobile.
| Area | What it does |
|---|---|
| Chat Log (center) | The GM's narration, your actions, system logs. Filterable, searchable, and exportable. |
| Hero panel (dock/tabs) | Your hero's HP, AC, stats, skills, inventory, spellbook, conditions, feats, and resources. Edit anything inline; click spells, items, and conditions for detail popups. Casters get a "Manage" button next to Spellcasting to prepare/unprepare spells (prepared casters) or swap a known spell on level-up (known casters, Tasha's rule) — locked during combat. |
| Party panel (multiplayer) | The roster: every hero's portrait, HP, AC, and live conditions. Click a hero to inspect their sheet; a "writing…" hint shows who is typing. |
| Journal tab | Active quests + categorized lore (NPCs, Locations, History, Items) the GM has logged. |
| Compendium (📖) | In-app reference browser: glossary, all conditions + exhaustion levels, rules tables, the full spell catalog, and SRD items. Read-only, pulls from the same data the engine uses. |
| Combat Banner | Slides in during fights — shows initiative order, current turn, HP bars, conditions. |
| Atmosphere image | Generated scene art behind the chat, tracking the LOCAL player's location. Click to expand fullscreen. In multiplayer, each player sees the atmosphere for their own character's location, so a traveling companion's move won't flip your background. |
| Pending-batch chat (multiplayer) | Each player types into the chat to add a pending message; any player presses "Take the Turn" to flush the batch through one GM response. Only shown when 2+ party members are present. |
| Quick Actions | One-tap buttons for your prepared spells, equipped weapons, class resources, skills, saves, and potions. |
| Suggested Actions (opt-in) | 2-3 clickable next-action chips after each turn. A 4-tier fallback chain (inline → opt-in LLM call → deterministic → generic) keeps the tray populated; never blank when enabled. |
| Onboarding tour | Auto-launches once on first play; replayable from Settings. |
| Voice input & TTS | Microphone button for speech-to-text; speaker icon to hear the GM. |
Just type naturally — the GM figures out the mechanics.
I swing my longsword at the goblin→ engine rolls to-hit + damageI cast fireball at the two orcs→cast_spellrolls saves + AoE damageI try to pick the lock→check_skillrolls Sleight of Hand vs DCI search the room for traps→ Perception checkI take a long rest→ full heal, hit dice recovered, time advances 8hI travel to Neverwinter→move_toin short legs (≤4h each) with rest stops; the engine blocks lethal over-exertionI buy a healing potion→ inventory update + gold deducted
- Click the rewind arrow next to your last message to undo & retry a turn.
- Click any dice result to see the breakdown (d20, modifier, total, DC/AC, crit/fumble).
- The Resolve Turn button appears when it's the enemy's turn — the engine auto-rolls their attacks.
- Settings (⚙) lets you toggle debug mode, atmosphere art, voice, and TTS speed/pitch.
When you start a new campaign you first choose between two paths:
- Quick Start — pick one of 10 pre-made level-1 heroes (spanning every race and a spread of classes), then choose your starting grounds. Fastest way into the game.
- Custom Character — the full creation wizard.
The 11–13 step wizard walks you through a fully SRD-compliant build:
- Name — your hero's identity
- Race — from Human to Dragonborn (pick your Draconic Ancestry)
- Class — Barbarian, Bard, Cleric, Druid, Fighter, Monk, Paladin, Ranger, Rogue, Sorcerer, Warlock, Wizard
- Subclass (early-path classes only — e.g. Cleric Domain at L1)
- Stats — point-buy / standard array style allocation
- Skills — choose proficiencies based on class skill list
- Feats & ASI — at ASI levels (1, 4, 8, 12, 16, 19) take a feat or +2 stat points
- Subclass (late-path classes — e.g. Wizard Arcane Tradition at L2)
- Spells (casters only) — pick cantrips + known/prepared spells
- Gear — starting equipment from your class
- Background & Persona — SRD 5.1 background, alignment, and rollable personality/ideals/bonds/flaws tables (all optional)
- Review — final summary, finalize to enter the world
- Starting Grounds — pick one of 4 LLM-generated taverns/inns (new campaigns only)
When signed in via Supabase:
- Create a campaign from the dashboard — you become the host.
- Share the Campaign ID (copy button in the header) with friends.
- They join by entering the ID at the dashboard, then pick Quick Start (a pre-made hero) or Custom creation — same choice the host sees, but the campaign's starting ground is already chosen by the host.
- Every action syncs to Supabase in realtime — all players see the same narration, dice, and state.
- The active player's turn locks the campaign to prevent race conditions; in multiplayer, each player types into the chat to add a pending message, then any player presses "Take the Turn" to flush the batch through one GM response.
- Live typing indicators ("NAME is writing…") show when other players are composing their next move, and clear the instant a message is sent (no lingering "is writing" after send).
- Each player has private character notes (visible only to your own character's owner); notes are never sent to the GM/LLM.
- Delete / rename campaigns from the dashboard.
Don't want an account? Anonymous mode works fully offline — your save lives in localStorage.
The repo is Vercel-ready:
- Push to GitHub.
- Import the repo at https://vercel.com/new.
- Set your environment variables in the Vercel dashboard.
- For production, set
VITE_LLM_API_BASE=/apito route through the built-in proxy atapi/chat/completions.ts(avoids CORS and hides the upstream URL).
vercel.json already rewrites all routes to index.html for SPA behavior.
npm run build # outputs to dist/
npm run preview # local preview of the buildUpload dist/ to any static host (Netlify, Cloudflare Pages, GitHub Pages, S3, etc.). Make sure your LLM provider allows CORS from your domain, or run your own proxy.
| Script | What it does |
|---|---|
npm run dev |
Preflight check + Vite dev server (port 3000) |
npm run build |
Production build to dist/ |
npm run preview |
Serve the production build locally |
npm test |
Run unit/component tests (Vitest, bails on first failure) |
npm run test:watch |
Watch mode |
npm run test:coverage |
Run tests with V8 coverage report |
npm run test:ci |
Verbose test run (run locally before pushing — no CI is currently configured) |
npm run test:live |
Run live LLM integration tests in tests/live/ |
npm run test:live:tier3 |
Run scenario-based live tests via tsx |
npm run test:all |
Unit tests + tier-3 live scenarios |
npm run lint |
ESLint over all .ts / .tsx |
npm run lint:fix |
ESLint with --fix |
npm run check:jsonclone |
Counts JSON.parse(JSON.stringify(...)) deep-clone usages (diagnostic) |
npm run install-app |
Guided CLI installer (scripts/install.js) |
npm run prepare |
Sets up Husky pre-commit hooks |
| Variable | Required | Default | Purpose |
|---|---|---|---|
VITE_LLM_API_KEY |
Yes | — | API key for your LLM provider |
VITE_LLM_API_BASE |
No | https://openrouter.ai/api/v1 |
OpenAI-compatible endpoint. Use /api in prod for the built-in proxy. |
VITE_LLM_MODEL |
No | deepseek/deepseek-v4-flash |
Model ID for narration + tool-use |
VITE_SUMMARIZATION_MODEL |
No | xiaomi/mimo-v2.5 |
Fast non-thinking model for context compression |
VITE_IMAGE_ROUTER_API_KEY |
No | — | ImageRouter key (blank disables art) |
VITE_IMAGE_MODEL |
No | stabilityai/sdxl-turbo |
Image generation model |
VITE_SUPABASE_URL |
No | — | Supabase project URL (blank = local-only) |
VITE_SUPABASE_ANON_KEY |
No | — | Supabase anon/public key |
VITE_CONTEXT_RAW_CAP |
No | 30000 |
Token threshold to compress raw history into a checkpoint |
VITE_CONTEXT_BUDGET |
No | 180000 |
Max token budget before old checkpoints are evicted |
VITE_LLM_DISABLE_THINKING |
No | — | Set to true to disable reasoning traces on supporting models |
The GM isn't responding / errors out.
Check your VITE_LLM_API_KEY and VITE_LLM_API_BASE. Open Settings → enable Debug Mode to see every request, tool call, and response in the console.
No scene art is generating.
You need a valid VITE_IMAGE_ROUTER_API_KEY. Leave it blank to disable the feature gracefully.
Multiplayer isn't syncing. Make sure both players are signed in and share the exact Campaign ID. The host must be online — state syncs through their campaign row in Supabase.
My saves disappeared.
Anonymous play saves to localStorage only — clearing your browser wipes it. Create a Supabase account for persistent cloud saves.
A tool call failed / state got weird. Click the rewind arrow next to your last action — it restores the snapshot from before the turn and retries. There's also an emergency snapshot fallback.
MIT © 2026 Preston Michael
SRD 5.1 content. This work includes material taken from the System Reference Document 5.1 by Wizards of the Coast, available at https://dnd.wizards.com/resources/systems-reference-document, and licensed under the Creative Commons Attribution 4.0 International License (full text in LICENSE-CC-BY-4.0.md). See NOTICE.md for the complete third-party attribution.
This is a personal project but issues and PRs are welcome. Before submitting:
npm run lint— must passnpm test— must pass (runnpm run test:cilocally for verbose output before pushing)- Live tests (
npm run test:live) require a real LLM key and shouldn't be run in a CI runner
For the full developer reference — file structure, services, the agent loop, context pipeline, and how everything ties together — see ARCHITECTURE.md.