Proposal: A Dwarf-Fortress-style world simulator for narrative support#

Goal#

A tool that generates an in-universe-consistent history — factions, dynasties, wars, migrations, founding/fall of settlements — that you can query for flavor text, side-character seeds, and place lore while drafting prose. Stretch goal: a visualizer for comparing different “civilization experiment” runs (different seeds/parameters), the way Dwarf Fortress players compare worldgens.

What’s already in nix-flake-final that this can build on#

I cloned tsunaminoai/nix-flake-final to check conventions rather than propose something that fights your existing structure. Relevant pieces:

  • pkgs/agents — a set of writeShellScriptBin CLIs sharing a lib.sh that wraps calls to a local Ollama instance (default qwen2.5:3b at localhost:11434, overridable via OLLAMA_MODEL/OLLAMA_HOST). This is exactly the shape a “turn structured facts into prose” tool wants — system prompt in, file/JSON in, generated text out.
  • modules/flake/packages/default.nix — the llm app wraps Simon Willison’s llm CLI pointed at Ollama running on mokou over Tailscale, with a template file installed from modules/flake/programs/*.yaml. So you already have a working, self-hosted LLM backend and a template mechanism.
  • pkgs/paperless-rag — a buildPythonApplication (chunk → embed → qdrant → ask-with-citations) for RAG over your Paperless library. This is the right template for a “canon store” — index simulated history events instead of documents, retrieve grounded facts, then generate.
  • pkgs/docs — an mkdocs site built from modules/* option docs plus hand-written pages in pkgs/docs/content/, including a content/proposals/ folder (observability-unification-plan.md, lightning-detector.md) — this document is written to drop in there.
  • justfile, treefmt, pre-commit-hooks, modules/flake/checks — existing formatting/lint/CI scaffolding a new package should plug into rather than duplicate.
  • pkgs/templates/new-host — shows the repo’s convention for scaffold templates, useful as a model for a pkgs/templates/new-plugin if the sim ends up plugin-based (see below).

The landscape (so we don’t reinvent a worse wheel)#

Dwarf Fortress’s own approach is the obvious reference point: civilizations spawn from seed sites (dwarves near mountains, elves in forests, humans on plains), spread, and generate conflict where territories overlap; individual “historical figures” are tracked and get named events, while ordinary population is handled with cruder heuristics (gaming.stackexchange, gaming.stackexchange on Legends). Legends mode can export the whole generated history as XML for exactly the kind of downstream consumption you want (DF Wiki: XML dump), and there’s precedent for turning that export into prose — JosquinDuchaine/LegendsReader is a small story generator built directly on DF’s legends files.

Academically, this exact problem has a name and a decade of work behind it: “Talk of the Town”-likes. James Ryan, Michael Mateas, and Noah Wardrip-Fruin’s Talk of the Town simulates a small American town’s social life over ~140 years purely from bottom-up character interactions, explicitly as a content generator for other narrative work (it powered the game Bad News) (GameAIPro chapter, AAAI dialogue paper). Its direct successor, Neighborly, is a “rational reconstruction” built explicitly as a reusable, general-purpose Python library rather than a one-off game (IEEE CoG 2022 paper):

  • Python, MIT licensed, pip install-able, entity-component-system architecture (“GameObjects” + “Systems” + a plugin API).
  • Explicitly inspired by Dwarf Fortress, Caves of Qud, Crusader Kings, RimWorld, and WorldBox.
  • Simulates hundreds of years of settlement history — births, deaths, marriages, jobs, businesses, relationships, legacies — in minutes.
  • Content (characters, locations, rules) is authored in YAML + light Python, not hardcoded, and is shareable as plugins.
  • Exports simulation data to JSON and uses Polars for analysis of the output.
  • Actively maintained (11 releases, last tagged March 2024, repo pushed to as recently as this year).

This is the single best “don’t build an ECS and a social-simulation model from scratch” starting point I found.

For depth beyond social mechanics — culture, religion, politics, dialects, naming conventions — Mark R. Johnson’s decade-long roguelike Ultima Ratio Regum is the deepest public work on “qualitative procedural generation”: generating internally-consistent religions, political systems, and even per-nation dialects/idiolects for named world history (Wikipedia, PCG Workshop paper on linguistics/naming, AISB paper on cultural/religious/political AI). Worth mining for design ideas (how to generate a plausible pantheon, a plausible naming convention per culture) even though the codebase itself is a hand-rolled game engine, not a library.

For the “turn events into prose” step specifically, there’s a 2025 system called StoryBox that does almost exactly what you’re describing: it runs a multi-agent sandbox, summarizes the event log, then has a separate “Storyteller Agent” retrieve relevant events and write long-form narrative chapter by chapter, keeping continuity with prior chapters (arXiv 2510.11618). That two-stage split — simulate first, narrate second, and ground the narration in retrieval over the event log — is the right shape for your use case and maps directly onto the paperless-rag pattern you already have.

Everything else I found (AI-worldbuilding SaaS tools, hobby DF clones like Andres6936/WorldGeneration.Dwarf or kevshakes/dwarf-fortress-simulation) is either pure-LLM-freeform (no persistent, queryable canon — it’ll contradict itself across a novel) or a terrain-first toy without a real social/historical model. Neither is a better foundation than Neighborly + a retrieval layer.

Three layers, cleanly separable, matching your existing repo’s separation of “batch tool that produces data” vs. “ask/query tool with citations”:

  1. Simulation core (“the world”) — Neighborly, vendored as a flake input, with a custom plugin defining your setting’s archetypes (peoples, factions, professions, deities, disasters) in YAML per Neighborly’s plugin API. Run for N in-simulation years; export JSON events + Polars dataframes.
  2. Canon store (“the annals”) — a local RAG index over the exported events (same shape as paperless-rag: chunk events → embed → local vector store → retrieve-with-citation), so later queries are grounded in what actually happened in that run instead of drifting.
  3. Loremaster (“the narrator”) — a CLI in the pkgs/agents style: lore "give me three taverns in the city of X and a rival merchant house" → retrieves relevant canon facts → local Ollama (fast/cheap default, e.g. your existing mokou-hosted model) turns them into flavor text, with an escalation path to a stronger model (Claude/GPT via your existing connectors) for scenes that need better prose than a 3-7B local model reliably produces.

Stretch — visualizer for civilization experiments: a small static site (timeline of ages/events, a faction/dynasty relationship graph, a settlement map) generated from the Polars export, in the spirit of the community “Legends Viewer” tools DF players use, but built for comparing runs: same plugin config, different seeds/parameters, side-by-side stats (wars per century, dynasties surviving to end, etc.) — this is where Polars earns its keep.

How this slots into nix-flake-final#

  • pkgs/worldsim/buildPythonApplication wrapping your setting plugin + Neighborly dependency, same shape as pkgs/paperless-rag/default.nix. Console script: worldgen (run a simulation, write JSON/Parquet).
  • pkgs/loremaster/ — either a Python package (if it needs the retrieval index) or a pkgs/agents-style shell CLI if you start with a simple “dump canon into context, ask Ollama” version before building real retrieval.
  • Register both via self.callPackage in modules/flake/packages/default.nix, exposed as nix run .#worldgen / nix run .#lore, following the existing mkApp pattern used for summarize/classify/etc.
  • Add a devShell entry (modules/flake/programs/shell.nix) with python3Packages.polars, pyyaml, and whatever vector-store client you pick, so nix develop gives a working environment without touching other hosts.
  • Drop this document at pkgs/docs/content/proposals/worldsim-plan.md — it already follows your content/proposals/ convention and will show up in the mkdocs site automatically.
  • Add a cheap smoke-test flake check (modules/flake/checks) that runs a short simulation (a handful of simulated years) and asserts it produces valid JSON — catches breakage from Neighborly upstream changes or plugin bugs in CI, same spirit as your existing checks.
  • If/when the canon store needs persistence across sessions, treat its data file like anything else you care about — a .db/.parquet under a data dir the existing borg backup modules already sweep up.

Phased plan#

Phase Scope
0 Decide setting scope (one region vs. a whole world; how “fantasy” vs. grounded) and pick a working name for the project.
1 Vendor Neighborly as a flake input; author a minimal plugin (a handful of peoples/factions/professions); run a short simulation; confirm JSON export is usable.
2 Wire the JSON export into a small local retrieval index (reuse paperless-rag’s chunk/embed pattern) so facts can be queried without re-reading the whole dump.
3 Build the lore CLI on top of retrieval + local Ollama, in the pkgs/agents style; add the escalation path to a stronger model for higher-stakes prose.
4 (stretch) Batch-run multiple seeds/parameter sets; build the timeline/map/relationship-graph visualizer over the Polars export; add run-comparison stats.

Open questions for you#

  • Setting fidelity: do you want Neighborly’s out-of-the-box “American small town” flavor (occupations, businesses, romance) reskinned, or do you want to lean harder into Ultima Ratio Regum-style generated religions/politics/dialects from the start? The former is much faster to stand up.
  • Scale: Neighborly is explicitly single-settlement (tens to hundreds of agents); if you want DF-scale “whole world, many civilizations,” that’s a heavier lift — either running many Neighborly instances and stitching a world-level layer on top, or accepting a smaller, deeper single-region simulation. I’d default to the latter for a v1.
  • Model budget for the Loremaster: local-only (free, always available, mediocre prose) vs. hybrid with occasional calls to a stronger hosted model for passages you’ll actually publish.

References#