World-Sim
World-Sim is a local narrative simulation with split storage, grounded play modes, and a companion World Builder.
The problem
Most LLM “worlds” keep structure and prose in one blurry store, so a chat turn can invent a room that never existed, or a builder can write layout without anyone approving the plan.
I needed a local simulation where structured state answers what exists and where it is, and canonical lore answers what it means. Play, admin edit, and sandboxed chat also had to stay in separate modes. What was missing was an MVP platform with appdir bootstrap, auth, a Quiet Manor seed, and a Builder that proposes structure without unsupervised apply.
Why I built it
Day to day, I wanted to walk a seeded manor and talk to an NPC when one is present. As admin, I also needed to edit canon through drafts and grow the world from approved lore with an explicit apply step.
Packaged commands were enough for that. world-sim runs the session shell, world-builder builds structure from lore, and world-sim-serve came later for thin multi-session web. Providers default to Grok, OpenAI and Anthropic are available, and WORLD_SIM_LLM=fake covers offline play.
What I built
World-Sim is a Python narrative simulation, and its Phase 1 MVP (slices 1 through 5) is complete. That MVP includes appdir bootstrap, .env / config.yaml, auth, SQLite structured state plus ChromaDB canonical lore, and Quiet Manor with Mrs. Hale. It also includes grounded play_mode, admin edit_mode, admin sandboxed chat_mode, and the World Builder companion (propose / preview / validate / apply, including propose_from_brief).
Later phases in the same repo add WorldAuthority over play mutations, multi-session WebSockets with presence, public say, and a schematic map. They also add contested co-op through a serial mutation queue and exclusive Player Chat leases, plus optional memory, retrieval, and lore_guard, which all default off. Dynamic frontier expansion also defaults off. Deferred work still includes nicer web polish, scale-out beyond small co-op, broad CRUD, inventory handoff in Player Chat, and unsupervised Builder apply (Exp-007).
You install it with pip install -e . (Python 3.11+) and run world-sim once to bootstrap. Then you set keys in .env and either play or serve.
Three decisions
These are the three choices I would defend in a design review.
1. SQLite holds what exists, and Chroma holds what it means.
SQLite holds users, characters, rooms, items, NPCs, sessions, relationships, runtime state, and lore-key references. ChromaDB holds canonical lore text (system, room, item, NPC). Play-tool mutations and authoritative structured reads go through WorldAuthority (SQLite-backed via WorldStore today). I was counting on that split so a narration pass cannot invent durable layout unnoticed, and so lore can be revised without rewriting every row that points at a key. The grounding hierarchy runs from system to room to item to NPC to mutable runtime on purpose.
2. Play, edit, and chat stay separate modes.
play_mode is normal grounded gameplay. edit_mode is admin-only constrained canon management, including draft create/revise paths that wait on approve_draft. chat_mode is admin-only sandboxed NPC conversation and is intended to stay non-canonical, so it should not mutate world state or persistent NPC memory. In-play Player Chat (talk to / end_chat) is conversation-only for a present NPC, with exclusive leases under contested co-op. The mode boundaries are there to keep rehearsal and canon edits from leaking into play state, because mixing those surfaces is how worlds lose integrity.
3. Builder proposes, and humans apply.
world-builder shares the same appdir, config, SQLite, and Chroma store, and it seeds and validates structure from approved lore. Proposals stay drafts until you explicitly apply_seed_plan. You preview and validate first, and apply asks you to type apply or pass --yes. Briefs supply intent and never become silent canon, and if named lore keys are missing, Builder fails closed. The bet was that unsupervised structure writes belong in an experiment (Exp-007) and stay out of the MVP. Play narration never creates rooms, and only stub realize (when dynamic expansion is on) or Builder apply writes structure.
Proof
You can follow the same paths from the public README, docs/OPERATOR.md, and docs/WORLD-AUTHORING.md.
pip install -e . WORLD_SIM_LLM=fake world-sim WORLD_SIM_LLM=fake world-sim-serve --host 127.0.0.1 --port 8765 world-builder
In play after login, starting in the Quiet Manor foyer, you can run look, go north, take brass key, talk to Mrs. Hale, and end_chat. As admin, you run mode edit, then the draft lore commands and approve_draft. In Builder, the sequence is propose_from_brief examples/seed-brief-cellar.yaml, preview_seed_plan, validate_world, and apply_seed_plan.
The public repo also confirms the Grok / OpenAI / Anthropic providers and that the optional flags default off (world.dynamic_expansion, memory.enabled, retrieval.enabled, player_chat.lore_guard). It lists the deferred work for unsupervised apply and scale-out as well.
WorldAuthority over mutations and SQLite versus Chroma split storage. World Builder then goes through propose, preview, and validate before a human applies the plan.
Why I built it this way
I hold these boundaries whenever a simulation has to survive more than one session. Structure and meaning both have to be durable, and the apply step has to be one a human can refuse.
Keeping SQLite and Chroma apart stops existence and meaning from collapsing into one blob, and separate modes for play, edit, and chat mean rehearsal cannot rewrite the manor by accident. Builder stays on propose and apply so layout remains a decision. If I am going to simulate a world with an LLM in the loop, I want to be able to explain the storage split and the apply gate to a peer in under two minutes.
The code lives at github.com/rdegraci/world-sim, and you can go back home from here.