An open-source AI Game Master you self-host — and can run 100% locally. The model narrates; a real RNG rolls the dice.
Most "AI dungeon master" toys let the model type "you rolled a 17" — which means the model quietly decides your fate. AetherTable doesn't. It runs a real tabletop session with an AI DM — or an AI player at your table — across five real systems, and every die is rolled server-side by an actual RNG; the model only ever asks to roll. Play in the browser or in your terminal, by text or voice, on a frontier model or a local 7B.
Built with Python and React — runs on Google Gemini, or fully offline via Ollama / LM Studio.
- The AI can't cheat the dice. The model never invents a number. When a roll is needed it emits a tag —
[[ROLL:1d20+5]]— and the server rolls with a real RNG, then substitutes the result. The die is honest by construction. - Five real systems, real math. Dungeons & Dragons 5e · Pathfinder 2e · Call of Cthulhu 7e · Cyberpunk RED · Ordem Paranormal — each with its authentic resolution (roll-under percentile, exploding d10s, keep-highest attribute pools), not a generic "roll a d20."
- No keys baked in — bring your own. This repo ships with no API key and never stores one. You pick a backend in the web UI and paste your own key; it stays in your browser. Or pick a local model and need no key at all.
- Runs on your machine. Point it at Ollama or LM Studio and it plays fully offline — no API bill, nothing leaves the box. Prefer the cloud? Gemini and OpenAI-compatible endpoints work too.
- DM or player. The AI can run the world, or join your party and play one character — declaring actions and waiting on the DM like everyone else.
- Remembers long sagas. A live window keeps recent turns verbatim; older turns are folded into a rolling summary instead of being forgotten.
- A full web client. Character creation with per-system attributes and derived stats, a live character sheet, five themes that re-skin the whole app, EN/PT-BR, and voice play.
- Ships with evals. A tiny harness scores whether a model keeps the dice contract and stays in character — so you can measure a local 7B against a frontier model.
git clone https://github.com/IPedrax/AetherTable
cd AetherTable
# spin up the local model (free, offline)
ollama serve
ollama pull IPedrax/firefly-v5
docker compose up --build # then open http://localhost:8080Prefer to run it without Docker:
pip install -r requirements.txt
uvicorn server:app --reload # the engine, on :8000
cd web && npm install && npm run dev # the web client, on :5173No .env needed for the web client — choose a backend and paste a key (if the backend needs one) on the Engine Settings screen. To play in the terminal instead, see Play.
There is no API key in this repository, and the server has none of its own.
- Pick a backend in the UI. Ollama and LM Studio run on your machine and need no key whatsoever.
- For a cloud backend, paste your own key. It's kept in your browser's
localStorageand sent with each turn, used to build that one provider and then dropped — never written to disk, never logged, never shared between players. - Self-hosting for yourself only? Put the key in
.envinstead (see.env.example) and leave the UI field blank — the server falls back to the environment. This is whatplay.pyand the CLI evals use.
Anything you type still reaches whichever model you chose, so a cloud backend sees your session; a local one doesn't. If you expose the engine beyond localhost, put it behind your own auth — it deliberately ships with none.
| Backend | Key needed | Notes |
|---|---|---|
| Ollama (local) | no | free, offline — the default |
| LM Studio (local) | no | free, offline |
| Google Gemini (cloud) | yes | also the only backend that does voice input |
| OpenAI / any compatible endpoint | yes | cloud or proxy |
Out of the box it points at IPedrax/firefly-v5 — a 6 GB, 128K-context local model that fits on an ordinary GPU and keeps the dice contract. Any other Ollama model works: change the name in Engine Settings, or set OLLAMA_MODEL for the terminal client.
Ollama and LM Studio both speak the OpenAI API, so one small provider class covers every local and cloud backend. Adding another is a ~20-line class in aethertable/providers.py.
In the terminal the same choice is made with LLM_PROVIDER=ollama | lmstudio | gemini | openai in .env.
Running the engine in Docker while Ollama runs on your host? Use
http://host.docker.internal:11434/v1as the endpoint —localhostwould point at the container.
cd web && npm run dev- Engine Settings — pick a backend, model, and endpoint; paste a key only if the backend needs one.
- Campaign setup — the AI runs the world as DM, or joins your party as a player while you DM.
- Character creation — each system brings its own attributes, classes/ancestries, skills, and derived stats (D&D hit points, Cyberpunk RED Humanity, Ordem PV/PE/Sanity, CoC Sanity and damage bonus). Let the AI roll one up for you, then edit it.
- The table — narration on parchment with resolved rolls inline, a live character sheet, push-to-talk voice (Gemini), and spoken replies when unmuted.
- Five themes, one per system, applied app-wide — and EN / PT-BR, toggled with
Alt+B.
The engine underneath is REST + WebSocket with no accounts, no quotas, no database — session history lives in memory for the life of the connection. Send {"type":"message","content":"I open the door","rpgSystem":"coc","settings":{…}} to /ws and stream back narration (and audio, if unmuted).
No browser required — the terminal client is the zero-setup demo:
python play.py # AI Dungeon Master, D&D 5e
python play.py --system cyberpunk # dnd5e | pathfinder | coc | enigma | cyberpunk
python play.py --role player # flip it: the AI joins your party, you DMIt reads LLM_PROVIDER and the matching key from .env.
The prompt forbids inventing numbers; to roll, the model must emit a tag like [[ROLL:1d20+5]]. The server intercepts it, rolls with a real RNG in aethertable/dice.py, and replaces the tag with [1d20+5 -> 17]. Because the contract is plain text, it works on any model — no provider-specific function-calling to maintain.
One parser covers every supported mechanic:
| Expression | Meaning | System |
|---|---|---|
1d20+5 |
flat modifier | D&D / Pathfinder |
1d100 |
roll-under percentile | Call of Cthulhu |
1d10!+7 |
exploding die | Cyberpunk RED |
3d20kh1 |
keep highest of a pool | Ordem Paranormal |
python -m aethertable.evals selftest # offline scorer self-check, no network
python -m aethertable.evals # live run against your configured providerThe live run reports dice-tag compliance and stayed-in-character rates — a quick way to see how playable a given local model actually is.
aethertable/
providers.py swappable LLM backends (Gemini, OpenAI-compatible: Ollama/LM Studio/OpenAI)
game_master.py the narrative brain — provider-agnostic
prompts.py DM/Player instructions + the 5 game systems
dice.py deterministic dice (pure, self-tested)
memory.py rolling-window + summary memory (pure, self-tested)
evals.py behavioral evals for the dice + character contracts
play.py terminal client (zero-setup demo)
server.py FastAPI WebSocket + REST engine — no key of its own
web/ React + Vite client
src/settings.ts bring-your-own-key store (localStorage)
src/data/ per-system character models, skills, derived stats
src/components/ landing · settings · setup · character creator · sheet · table
src/i18n.tsx EN / PT-BR
Self-tests, no network:
python -m aethertable.dice # -> dice OK
python -m aethertable.memory # -> memory OK
python -m aethertable.providers # -> providers OK (credential precedence)- React reference web UI
- Local voice input via Whisper (STT for Ollama/LM Studio)
- More systems: Blades in the Dark, Mothership, FATE
- Token streaming over the WebSocket
- Optional SQLite persistence adapter for the server
- Save / export a campaign transcript from the browser
Contributions welcome — pick one, open a PR.
MIT © 2026 Pedro Medeiros
ipedrax.com.br · The Harvest of Minds
Built by Pedro Medeiros. I build production LLM applications for companies too: multi-provider backends, the infrastructure under them, and the billing on top. Available for contract work, remote from Brazil on US hours. [email protected]