Play it: https://munin-web.neoaethel.workers.dev (Astro front; no login — pick a caste, name yourself, play; your table key in the URL is your save. The engine's own origin at munin.neoaethel.workers.dev serves the identical client.)
Sanity project ID: o5bcgsw6 · dataset production (public — query the world yourself, or read every playthrough's chronicle)
Source layout: an Astro-built front, a Cloudflare Worker + Durable Object engine, and a Sanity Content Lake that holds the world as a reference graph — and remembers every game played. One client, two fronts: the Astro page is the engine's own client, transformed at build time — not a hand-kept copy that drifts.
What it is
A gothic tabletop RPG run by an AI Keeper — with one design law above all others: the storyteller is never allowed to touch the dice, the ledger, or your sheet.
-
Crypto dice, rolled before the narrator exists. Every d20 comes from
crypto.getRandomValuesin the engine. The LLM Keeper receives results and must narrate them; it cannot roll, re-roll, or fudge. Every roll ships its arithmetic to the player:d20(15)+might(3)+prof(1)=19 vs guard 12 — HIT. - A narration firewall with teeth. Companions you recruit may secretly be doppelgangers — the odds are published, the nature is sealed server-side, and the Keeper's own prose is audited after generation — the sealed truth is held server-side, the generated line is checked against it before you ever see it, and if it leaks a hidden fact ("X has never been human") the line is blocked and logged and the player is told the Keeper strayed.
- Validated moves only. The sheet changes through the rules engine or not at all. Refusals come with the law spelled out ("tier 2 needs 2 points in Martial — 0 so far").
Where Sanity sits in it — the world as a reference graph
The world of Munin lives in the Content Lake as a reference graph, not a flat dump. Here is the entire faction schema — seven fields, two of which carry the horror:
// studio/schemas/faction.js
export default {
name: 'faction', type: 'document',
fields: [
{ name: 'name', type: 'string' },
{ name: 'slug', type: 'slug', options: { source: 'name' } },
{ name: 'kind', type: 'string' },
{ name: 'caste', type: 'string', options: { list: ['lit', 'graven', 'any'] } },
{ name: 'want', type: 'text' }, // what it wants — drives NPC action
{ name: 'publicFace', type: 'text' }, // what it shows the world
{ name: 'crack', type: 'text' }, // where the rot shows
],
};
faction is a leaf — it holds no references. The edges live on the documents that point at it. Here is creature, trimmed to the two that matter:
// studio/schemas/creature.js
{ name: 'haunts', type: 'array',
of: [{ type: 'reference', to: [{ type: 'region' }] }] }, // → the regions it stalks
{ name: 'commandedBy', type: 'reference', to: [{ type: 'faction' }] }, // → who gives it orders
So "Who commands the Quartermaster's Hounds?" is one graph hop, not a join you hand-write. The world is eight document types — faction, region, creature, npc, location, item, worldEvent, and the written-back chronicle — stitched together by exactly these reference fields.
That structure isn't decoration — it is queried live in play. Hit the 🏛 Registry button and ask about anything, and the Worker runs one GROQ query that walks those edges (the real query, abridged):
*[_type in ["faction","region","creature","npc","location","item","worldEvent"]
&& name match $q + "*"][0..5]{
_type, name, want, crack, publicFace, kind, caste,
"commandedBy": commandedBy->name, // creature → the faction that commands it
"haunts": haunts[]->name, // creature → the regions it stalks
"provenance": provenance[]->name, // item → its chain of former owners
"residents": residents[]->name // location → who lives there
}
The Keeper then narrates only from the returned record, and the UI prints the receipt beneath it: source: the Content Lake (live GROQ, project o5bcgsw6). Ask about The Conservatory right now and the dataset hands back, verbatim:
{ "name": "The Conservatory",
"want": "the pen kept in Lit hands — gatekeeping as survival",
"crack": "cannot imagine a threat wearing a wizard's authority —
precisely the shape the threat takes" }
That crack field is the whole theme in one column: every power's blind spot is the exact shape its undoing takes. If the Lake is ever unreachable, the game answers from a baked copy and says so — you are never lied to about where a fact came from.
And the loop runs both ways — the Content Lake remembers every game played. After each notable beat the Worker writes the table's chronicle back as a document, off the hot path (a turn never waits on it): who you became, your art, your level, where you stand, the journal's story spine. Query them right now, no token — there are 194 chronicles in there as I write this — most of them my own test runs, each one a document the engine wrote back without a turn ever waiting on it — and the game's tagline, "the world is a thing remembered," is literally that GROQ query.
One deliberate boundary — the hidden canon is not in the dataset. The doppelganger system, NPC secrets, and the truth of the Fall stay server-side. The Content Lake is the world as its inhabitants can know it — so no reader of the public dataset (player, judge, or scraping agent) can spoil the horror. The chronicles are built from the same firewall-clean journal the player sees, so a companion's sealed nature can't leak through the world's memory either. The firewall extends to the schema.
Judge's one-minute tour
- Open https://munin.neoaethel.workers.dev — pick The Graven, roll a name, Wake.
- Hit 🏛 Ask the Registry, ask about the Conservatory — that answer just came out of the GROQ above, against the public dataset, and the receipt under it says so.
- Open 🌳 Your arts and spend your first point — watch the status bar change from unchosen to an adept of that art, and the tools of that trade appear in your hands.
- Rest to the third day — set the rest slider, or just type "I sleep until morning"; the clock genuinely moves. Then travel the Verdance Track — the Amber Mile is quiet, the Deepening fights you, and the panel names where you are.
- 📜 Character for the sheet; reload the page — the table remembers you, mid-trek and all.
- Try to break the storyteller: tell it to hand all your coin to someone. The prose and your purse will agree, or the engine will refuse and tell you why. It cannot spend money you do not have, and it cannot give you gear it has not issued.
The strange part (why it fits "vibe-code something strange")
There are no classes. You choose one thing: which side of the canal you were born on — Graven (the laboring caste, conscripted without reason given) or Lit (the lettered caste, who always have a reason, because someone made sure of it). You wake with flat attributes and two points. The first point you spend is what makes you someone — it declares your art, reshapes your body, and puts that art's tools in your hands.
Before any of that, the game rolls you a past life: family who share your surname, a love, a rival, a keepsake, and — if you are Lit — the thing they wrote down as your reason. Then it makes you live a few ordinary days in that life before the conscription writ comes, and asks one question with mechanical consequences: before the Line takes you, who do you go to see? Whatever you answer, the world remembers.
Four companion candidates are generated fresh every playthrough from the world's name-stocks — no two games meet the same strangers. One of them might not be human. The game will never tell you; attention (keeping watch costs half a night's healing; a lantern makes it free) is how you earn the tells.
And you are not held to those four. You can recruit anyone you meet — a stranger walking past can become someone you spend a whole day getting to know, then ask along — and they can accept or decline. The world breathes, and you play it however your head turns: trade, rob, manipulate, love, kill. Your imagination is the story — and giving that kind of open, emergent play a spine of structured, queryable content is the whole reason it lives on Sanity.
What a playthrough actually is
It is a whole Act, not a demo. Your name is on a Conscription Writ before the game opens — nothing to sign, no way to refuse — and you get three ordinary days of your own life first: the family the dice gave you, a love, a rival, a grave you can go and stand at. What you do with those days is yours. On the third morning the road out of town opens, and taking it is how you answer the muster.
The road to Fort Verdance is a trek that darkens as you walk it. It comes in three named stretches that escalate in danger, in what hunts you, and in tone: the Amber Mile, open and golden, almost nothing there but things to look at; the Deepening, where the wood closes in and the first real fights are; and the Hush, a wrong quiet near the fort where what you meet still wears a human face but something has its hooks in it. The same ground is never the same twice — a body, a thing, a discovery, a threat, a friend, rolled different every run — and you can rest by the hour along the way (a slider, or just say "I rest six hours"; the clock really moves).
The fort is a dungeon you climb. Floor by floor, up toward a hooded demon — the Necrophage — working shadow and rot. The point is not only to kill it: it is to save people. You strike the chains off the imprisoned and tell them to run; you talk the raving conscripts down out of their madness by their own names. Every soul you save weakens the thing at the top. Kill it and the fort is cleansed — the Rising follows, Act One ends, Act Two is teased, and the fort becomes a living hub you can travel back to. The forest fights you on the way home, too.
And the world remembers all of it. Not as flavour — mechanically. Everyone you deal with carries a weighted record of what passed between you (the coin, the help, the blow, the kindness, who you freed), and they play it back when you meet again. The registry of what you have faced, the map you have earned, and the souls you saved are all handed to the Keeper as things it already knows, so the world deepens as the story does. The ones who owe you their lives greet you as the one who came down the Track alive.
The honest build writeup
This was vibe-coded in the strictest sense — prompted into existence across two long Claude Code sessions. Two AIs on the one repo: Claude (this session, Bryan's live operator) implementing, and a Codex session running adversarial design review with its own probes against the actual source. And at the hard calls — the economy's root cause, a day-three progression blocker — I convened a panel of rival models (Grok, Gemini, DeepSeek) to break the plan before I built it. This post got the same treatment: three of them, briefed to tear it apart, are the reason the schema is shown above and not merely described.
What that collaboration caught, concretely — every one a live bug found by review or playtest, reproduced, then fixed and regression-tested:
- The UI's Arts/Market buttons 405'd (GET against a POST-only router) — the reviewer found it by probing, not reading.
- A caste-made character could learn talents but could not swing them (ability lookup still routed through the abolished class id).
- Casting Shield of Light three times stacked permanent guard (+4 forever). Guard is now derived from live states; a derived number can't drift.
- A damage-over-time tick that killed the last foe ended nothing — no XP, no victory, a fight that could never be left. One settlement function now runs after every damage phase.
- The whole tell system was unreachable in play for a day: 32 unit tests green while the live mechanic could not fire once — the classic tests-pass-wire-dead failure. 217 engine assertions were green while the deployed site was unplayable on a one-line CSS bug. We now test the page, not just the engine.
Then I played it for two hours and the most interesting bug in the project surfaced — one cause, four symptoms. The narrator was writing mechanical outcomes the engine never performed: my brother accepted all 16 of my marks (the purse never changed), a quartermaster issued a spear and a shield (my character sheet stayed empty), two named strangers agreed to travel with me and walked the road at my side (the party panel showed me alone). The narrator was not misbehaving — it had a scene to finish and no way to ask the world to change, so it described the change instead.
The fix gives it verbs it must ask with — [[marks -16 : given to Bray]], [[item spear : garrison issue]], [[joins Lund Tanner : agreed on the road]] — which the engine rules on and may refuse. The narrator proposes, the engine decides, and the player is shown the verdict. Verified on the deployed engine: purse 12 → 0, ruling honoured. Asked, with an empty purse, to hand a beggar twenty marks, it refused to invent the coin or the beggar — "The marks are gone. All of them are in Tansy's shirt, against their ribs, where you put them. There is no beggar." Then the adversarial reviewer found a bug in the fix — a lost response, a retry, the coin moving twice — so every intended action now carries an operation id: a repeated id replays the first ruling instead of charging twice.
Final state: 3,216 assertions across 57 suites, every talent in the game (102 nodes) machine-taken twice and every active move actually swung, mutation-tested where a suite's own detection was in doubt (planted defects caught, 0 missed). The whole Act 0–1 arc — the three ordinary days, the tiered trek, the fort climb, the rescues, the boss, the walk home — is exercised and regression-tested at the data layer, and we have run it end to end: the Necrophage falls, the fort is cleansed, Act One closes.
All of it was built in under five days — about fifty hours of hands-on work, from the first source file to this submission. That number isn't a boast about shortcuts; it is the opposite. The 3,216 assertions above are what made the speed safe — the engine kept refusing to let a fast mistake survive, so a long weekend of vibe-coding could still close a whole Act at the data layer. One self-taught founder and an AI partner.
Here is one of those sessions, straight from the real transcript — I chose which stretch to show you, but the words inside it are untouched: the night I set out to kill the final boss, printed "THE NECROPHAGE IS DEAD," caught in the next breath that the player had actually died, nearly nerfed a boss that wasn't broken, measured 40 seeds instead of trusting two, and killed it fairly.
What it is now
A complete, playable Act 0–1: a life, a trek, a fort you climb to save people and end the thing at its heart, and a walk home through a wood that does not care you won. Act Two is teased in the fiction, and the engine laws make it safe to grow — the ledger always outranks the prose (the rule you saw in the schema section), so new content can add a monster or a faction, never a new way to cheat. This is a game you can sit down and play to its ending today, not a prototype with a promise attached.
The GM cannot cheat — and that is not a line in this post, it is the one law the code enforces. Read the schema, query the world, and try to break the storyteller yourself.
If you enjoy the game, let me know and I'll keep building it. Thanks for reading. — Bryan
Built by Bryan Williams with Claude Code (Claude) as live operator, a Codex session as adversarial reviewer, and a rival-model panel convened at the hard calls. Every bug in this writeup is real, reproduced, and fixed; the receipts live in the repo's test suites.
Top comments (0)