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

Revision 2 — pivoted toward Ultima Ratio Regum’s deep, qualitative culture/religion/politics generation, per feedback (“URR is more what I’m after”). Neighborly’s town-life social sim is demoted from core engine to an optional future add-on.

Goal#

A tool that generates an in-universe-consistent world — cultures, religions, political systems, naming conventions/dialects, and centuries of macro-history (wars, successions, schisms, rises and falls of states) — 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, the way Dwarf Fortress players compare worldgens.

What changed from the first draft#

The first draft centered the whole thing on Neighborly, a Python social-simulation library. That’s a poor fit for what you actually asked for: Neighborly simulates a single settlement’s social life — romance, jobs, businesses, day-to-day relationships — which is the Talk of the Town lineage, not the URR lineage. URR’s actual subject matter is macro-level and qualitative: pantheons with creation myths and eschatologies, political ideologies that generate laws and punishments, relic archetypes, per-nation dialects. This revision replaces Neighborly as the core engine with a purpose-built “qualitative generation” layer modeled directly on URR’s published design philosophy, plus a proper terrain/culture/state/religion seeding layer borrowed from the closest existing open-source analog to URR: Azgaar’s Fantasy Map Generator.

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

(Unchanged from draft 1 — I re-checked tsunaminoai/nix-flake-final rather than re-deriving conventions.)

  • pkgs/agentswriteShellScriptBin CLIs sharing a lib.sh that wraps a local Ollama instance (default qwen2.5:3b, overridable via OLLAMA_MODEL/OLLAMA_HOST). Template for a “Loremaster” narration CLI.
  • modules/flake/packages/default.nix — shows self.callPackage + mkApp registration, e.g. llm wrapping Simon Willison’s llm CLI pointed at Ollama on mokou over Tailscale.
  • pkgs/paperless-ragbuildPythonApplication chunk → embed → qdrant → ask-with-citations pattern. Template for a “canon store” over generated history instead of documents.
  • pkgs/docs/content/proposals/ — home for design docs like observability-unification-plan.md, lightning-detector.md; this document belongs there.
  • justfile, treefmt, pre-commit-hooks, modules/flake/checks — existing lint/CI scaffolding to plug into.
  • pkgs/templates/new-host/ — scaffold-template convention, model for a possible pkgs/templates/new-plugin.

The landscape, revisited for URR#

Ultima Ratio Regum’s own generation approach is not reusable code — only a design pattern. Mark R. Johnson (the sole developer) never released the engine, and his own devlogs are explicit that religions and nations are built with hand-rolled, “template-driven / constructive” generators: each religion gets a creation myth, an eschatology, one or more gods with domains, an ethical framework, an attitude to magic, and a “perception of the world,” and then physical representations (altars, robes, 25+ relic archetypes, holy books, statues) are derived from those choices, not generated independently (Johnson, dev blog: URR 0.11 update #3, Rock Paper Shotgun interview: “How To Procedurally Generate A Religion”). Nations similarly pick ideology values across categories (attitude to trade, violence, religion, etc.) and laws + punishment severities are derived from those values, not authored per-nation. A GameDeveloper.com retrospective on the project’s ten-year run describes this explicitly as “qualitative procedural generation”: objects are generated in relation to one another through predefined rule sets and building blocks, not in isolation (GameDeveloper.com: “The 10-Year Journey of Ultima Ratio Regum”). The implication: there’s no library to depend on here — this layer has to be custom-written, informed by this published design pattern.

The closest existing open-source analog to URR — culture, state, and religion generation with an interactive map — is Azgaar’s Fantasy Map Generator (FMG). MIT licensed (explicitly permitting derivative/commercial use of generated output), it runs a ~19-stage generation pipeline: heightmap → biomes → cultures (seeded origins, cost-based flood-fill expansion shaped by terrain/biome/habitability) → states (grown from capitals, with diplomacy/expansionism) → religions (seeded after states/cultures exist, so expansion can weigh political and cultural affinity — organized/folk/cult types, deity naming, per-culture name bases) → provinces/burgs, with the whole thing rendered as an interactive, layerable map (toggle cultural/political/religious overlays) (Azgaar/Fantasy-Map-Generator on GitHub, live app, DeepWiki: cultures & religions pipeline detail). This solves the geography/culture/state/religion seeding problem and, because the map UI already exists and already renders these layers, effectively solves the stretch-goal visualizer too — for free, if you’re willing to depend on it as a live web app.

Important constraint discovered while checking this: FMG has no documented headless/Node/CLI mode. Its culture and religion generators read straight from HTMLInputElement/HTMLSelectElement globals in the page and write into an in-memory pack object with no documented JSON-export-without-the-UI path, no batch-generation API, and no confirmed DOM-independence (DeepWiki architecture page, explicit on this gap; confirmed by browsing the project’s own GitHub Discussions, where “JSON documentation?” and “Customization of Exports” are open community questions, not solved problems). Practically: if you want FMG in the loop, you’re either driving the live browser app with automation (Playwright/Puppeteer) and scraping its export, or you’re not using its code directly at all.

One data point that supports “port the algorithm, don’t depend on the app”: a small, explicitly experimental Go project — flokey82/go_gens — has already independently reimplemented pieces of FMG’s logic headlessly: genreligion ports FMG’s religions-generator.js deity-naming and classification logic into plain Go structs and functions with no DOM dependency at all (pkg.go.dev/genreligion), and the same repo has genbiome (ports FMG’s biome classification) and genstory (flavor-text generation, explicitly described by its author as “a playground”). The repo’s own README says “DO NOT USE YET” — it’s not a stable dependency — but it’s useful proof that FMG’s generation algorithms (as opposed to its app) are small enough to port cleanly, and it’s a second worked reference (alongside FMG’s own source) for the “weighted category → template string” mechanics you’d want in your own qualitative-depth layer.

Naming/dialect generation — URR gives every nation its own dialect/idiolect; the closest reusable tooling:

  • Lexifer — Python 3, open source, generates a phonology + lexicon from a compact spec file. Best fit: run once per generated culture to get a consistent, distinct name-generator for that culture.
  • Vulgarlang — web-based conlang generator, paid tiers, not ideal for local automation.
  • PolyGlot — full conlang construction tool, heavier than needed here.
  • PLGL — restyles real sentences into a fictional language via phonotactics/affix rules; more relevant if you want quoted “in-dialect” phrases, not just names.
  • Isaac Karth’s phonotactics → orthography → morphology pipeline write-up — a good from-scratch reference if you’d rather write your own minimal version than adopt Lexifer.

What’s retained unchanged from the original research (still directly applicable regardless of which engine produces the underlying history):

  • Dwarf Fortress’s own worldgen: civilizations spawn from seed sites, spread, generate conflict on territory overlap, individual “historical figures” get tracked and named events (gaming.stackexchange: civ worldgen, gaming.stackexchange: legends generation); Legends mode exports the whole history as XML (DF Wiki: XML dump), and JosquinDuchaine/LegendsReader is direct precedent for turning that export into prose.
  • StoryBox (2025) — simulate first, then a separate “Storyteller Agent” retrieves relevant events and writes long-form narrative grounded in that retrieval, keeping cross-chapter continuity (arXiv 2510.11618). This two-stage “simulate, then narrate via retrieval” shape is still exactly right and is unaffected by the engine swap.
  • Talk of the Town / Neighborly academic lineage — kept only as an optional future add-on (see Open Questions), not the core engine.

Five pieces instead of three — the qualitative-depth and naming layers are the parts that actually make this “URR-like” rather than “map generator with labels.”

  1. Terrain & seed layer — geography, starter cultures, states, and religions. Two options, not mutually exclusive over time: - Option A (recommended to start): drive the live FMG app with browser automation (Playwright), export its generated pack JSON (cultures, states, religions, burgs, borders). Gets you real, visually-inspectable maps and a working visualizer immediately, at the cost of a live-browser dependency that’s slower per run and somewhat fragile to upstream FMG changes. - Option B (fallback): skip FMG’s code entirely and write a minimal Python terrain/seed generator (e.g., Voronoi cells + Perlin noise for biomes, simple flood-fill for culture origins). Geography doesn’t need to be sophisticated for narrative-flavor purposes — the interesting work is layers 2–4 below. Keep this as a lower-effort fallback if Option A proves too fragile for repeated batch experiments.
  2. Qualitative-depth layer (the core deliverable — custom Python, no off-the-shelf dependency) — implements URR’s trait-combination + rule-derivation pattern. Each culture/state/religion entity is assigned values across a fixed set of axes (cosmology type, ethics, attitude to trade/violence/outsiders, aesthetic motifs, government form, succession rule, religious hierarchy depth), and deterministic rule tables turn those axis combinations into concrete content: creation myths, deity domains and epithets, relic/artifact archetypes, laws and punishments by category, religious ranks (acolyte → priest → high priest), architectural/aesthetic motifs. Use FMG’s religions-generator.js and flokey82’s genreligion port as worked examples of the “weighted category → template string” mechanic, and Johnson’s blog posts/interview as the authoritative spec for which axes and categories actually matter and how they interconnect.
  3. History/event-log layer (custom Python) — a year-by-year simulation loop driven by layer 2’s axis values: wars triggered by adjacency + ideology clash, successions per each state’s own succession rule, religious schisms, founding/destruction of settlements — logged as a structured, DF-Legends-style timeline. This is what turns a static snapshot into centuries of consistent history you can actually pull “flavor text” out of.
  4. Naming/dialect layer — Lexifer generates one phonology + lexicon per culture at genesis time, used to name every person, place, and institution for that culture downstream, so a culture’s names sound coherently related to each other — a direct analog of URR’s per-nation dialects.
  5. Canon store + Loremaster CLI (unchanged from draft 1) — chunk/embed the generated history + culture + religion JSON using the same paperless-rag retrieval pattern already in the repo, then a small pkgs/agents-style CLI (“Loremaster”) that answers prompts like “give me a minor noble house from the eastern religion, hostile to the capital” with retrieval-grounded, in-universe-consistent prose — local Ollama by default, escalation to a stronger hosted model for prose you’ll actually publish.

How this slots into nix-flake-final#

  • pkgs/worldsim/buildPythonApplication, same shape as pkgs/paperless-rag/default.nix. Contains layers 2–4 (qualitative depth, history simulation, naming) and an orchestration CLI, e.g. nix run .#worldsim generate --seed 42 --years 500.
  • If pursuing Option A for layer 1: either fold Playwright + Chromium into pkgs/worldsim’s inputs, or split it into its own pkgs/worldsim-mapgen package so the browser-automation dependency doesn’t bloat the core package — using nixpkgs’ playwright-driver.
  • pkgs/agents/loremaster/writeShellScriptBin + lib.sh, following the existing agents/default.nix pattern, wrapping Ollama on mokou.armadillo-banfish.ts.net:11434, retrieving over the canon store.
  • modules/flake/packages/default.nix — register worldsim and loremaster via self.callPackage + the existing mkApp pattern.
  • pkgs/docs/content/proposals/worldsim-plan.md — this document’s eventual home, alongside observability-unification-plan.md and lightning-detector.md.
  • Devshell (modules/flake/programs/shell.nix) — add python3, lexifer (or vendor it as a flake input if not packaged), and Playwright/Chromium if Option A is in play.
  • CI (modules/flake/checks, treefmt, pre-commit-hooks) — smoke test: nix run .#worldsim generate --seed 1 --years 10 completes and produces valid JSON; lint Python with the existing treefmt config.

Phased roadmap (v2)#

Phase Scope
0 Prototype the qualitative-depth layer (layer 2) in isolation against a hand-fed dummy list of 3-4 cultures — skip terrain entirely. This is the actual novel, high-value work; prove the trait → rule → content pipeline before building anything around it.
1 Add the naming layer (Lexifer) so prototype output has real per-culture names instead of placeholders.
2 Wire in the terrain & seed layer — start with Option A (FMG via browser automation) to get real geography/starter cultures/states/religions feeding layer 2’s enrichment.
3 Add the history/event-log layer to advance centuries and produce a Legends-style timeline.
4 Canon store + Loremaster CLI, packaged to nix-flake-final conventions, CI smoke test.
5 (stretch) Civilization-experiment tooling: batch-run layers 1–3 across many seeds/parameter sets, compare outcomes (e.g. “how often does a monotheistic seafaring culture end up at war with its neighbors by year 300?”), visualized either via FMG’s own map UI per seed or a lightweight custom dashboard over the exported JSON.

Open questions for you#

  • Layer 1 approach: comfortable depending on browser automation + Chromium in the flake for Option A (real FMG-generated geography/visualizer, some fragility), or would you rather layer 1 be pure Python/Nix-native from day one (Option B, less visual polish but no browser dependency)?
  • Neighborly: drop it from the plan entirely, or keep it as an optional future add-on for settlement-level social texture (romance/jobs/business relationships) layered under the URR-style macro-history, for when you want to zoom into a specific town rather than stay at the civilization level?
  • Naming depth at launch: full Lexifer-generated phonology + lexicon per culture from day one (more setup, most authentic), or start with simple Markov-chain name lists (à la FMG’s own name bases) and add Lexifer once the qualitative-depth layer — the actually hard part — is proven out?

References#