# worldhistory Trials of how peoples spread, settle, adapt and change over thousands of years on a planet made with [worldgen](../worldgen). Each people ("race") is a population field on worldgen's hexagonal cells: it grows toward what the land can carry, competes with its neighbours, migrates (drift, founder groups, rare long jumps), adapts to depth, gravity, air and temperature, can split into sub-species, and learns (or loses) tech — magic included. Scheduled events (culls, era switches) and hazards (roaming monsters) shape it. Results are **trials of algorithms**, not a history to take as given: you tune the config and compare runs. ## Requirements - Python ≥ 3.11, `pip install -r requirements.txt` (numpy, scipy, pillow, h3) - A worldgen build (`out/r/cells.npz`, optionally `eras//cells.npz`) - RAM: < 2 GB at res 4 (~290k cells) for the default races; about 10 minutes for 7,500 years. ## Quick start ```bash python3 -m venv .venv && .venv/bin/pip install -r requirements.txt .venv/bin/python history.py check-config ../myworld/config/history --world ../myworld/out/r4 .venv/bin/python history.py run --world ../myworld/out/r4 --config ../myworld/config/history --trial t1 --seed 1 ``` ## How it works One step = 10 years, in this order: 1. **events** — scheduled: `seed` (first placement), `cull` / `die_off` (a share of people in a region), `era_switch` 2. **hazards** — static mortality layers; roaming units that raid, breed in clutches and die 3. **capacity** — habitat quality × density × tech gain (∝ q²: poor land stays in low gear) × harvest shocks × tolerance fitness × comfort × nudges 4. **growth** — logistic with an Allee effect; races share capacity (Lotka–Volterra); optional "veins" 5. **conflict** — background war losses by temperament; yielding turns losses into emigration 6. **migration** — drift toward headroom, founder groups into empty land, fat-tailed long jumps; with `[demography] current_bias` > 0 (default 0; e.g. 1–3 for swimmers) sea jumps and drift between sea cells favour going with the ocean current 7. **adaptation** — each condition's optimum moves toward where people live; the widths are fixed, so the old is lost; past the race's adaptable range [`lo`, `hi`] drift is braked (fuzzy, not a line) 8. **sub-species** — a group's progress toward a related race is read from its adapted optima (weighted over the conditions where the two races' native curves differ); a birth (once per sub-species) resets the group to the new race's curves; displacement squeezes the less fit of the pair in shared cells; parent groups adapted past `mix_min` bear the dominant race; adapted-back groups give **returns** (parent-race births, any number); `mode = "ritual"` births by chance and wins converts at a blood price 9. **tech** — domains (farming, crafts, travel, magic, war, land) grow with connected population, lost when isolated ## Config A config dir holds `history.toml` (run length, regions, events, hazards, nudges, tech and migration parameters) and `races/.toml` (demography, seed, habitat terms and gates over named predicates, travel costs, tolerance envelopes, temperament, conflict, tech priorities, sub-species `emerge` rules). Unknown keys are errors. Tolerances: `optimum`, `width` (or `width_lo` / `width_hi` for a lopsided bell), `lo`, `hi` (adaptable range, soft), `rate`, `comfort`. Conditions: `depth`, `air_pressure`, `gravity`, `o2`, `temperature`, `rain` (log10 mm/yr; no effect at sea). Strain: an adapted curve's peak is 0.35^(x²), x = (optimum − native) / (range edge − native) — adapted groups are livable, not good; a `comfort` list ([[value, peak], …]) replaces this for its condition. Displacement compares fit × comfort. A sub-species inherits the parent's curves per condition and overrides the ones that define it. `[emerge]` (defaults): `parent`, `mode` ("adapt" | "ritual"), `birth_min` 0.4, `birth_sure` 0.8, `birth_rate` 0.2 (chance per group per step at `birth_sure`, × P/(P+founder)), `mix_min` 0.3, `dominance` 1, `birth_shift` 1, `displace` 0.9, `return_min` / `return_sure` (= birth values), `convert` 1, `region`, `condition`, `settlement_density`, `isolated`, `spread` ("contact" | "region"); ritual only: `trigger` 0.01, `ritual_rate` 0.02 (rituals per step per √person), `ritual_cost` 300 deaths, `ritual_converts` 30, `victims` ("parent" | "all": the dead come from every nearby non-ritual people by presence), `crisis_drop` 0 (off; > 0: birth only where parents fell to ≤ (1 − drop) of their recent peak, `crisis_min` 1000 people, memory `crisis_years` 200; `crisis_km` 0: the survivors are born into the nearest cell within that range where the new race can live, 0 = the crisis cell itself must fit), `backlash_victims` 0 (off; after that many dead in all, one smackdown: the race loses `backlash_loss` 0.7 and holds rituals at `backlash_calm` 0.2 × the rate), `raid_rate` 0 (off; before the backlash, groups of ≥ `raid_min` 1000 send ~Poisson(rate) cells of `raid_size` 100 to occupied non-ritual land within `raid_km` 300; stats `raids`), `ritual_power` 0.5 (rituals ~ Poisson(rate · P^power); 1 = every cultist sacrifices), `backlash_years` 0 (off; the smackdown also fires this many years after the birth). The smackdown's losses fall by exposure (own people + non-ritual people in and next to the cell): big exposed groups are wiped out, small remote ones survive. Any mode: `after` 0 (earliest birth year), `return_rate` (= `birth_rate`). The old `class` / `years` keys and `[exposure]` are gone (errors name the replacement). Full configs live in the world project that runs the sim (e.g. its `map/config/history/`). Ocean fields from newer worldgen builds (`current`, `productivity`, `upwelling`, `sst`) load when present; older builds behave as still water. The habitat predicate `productivity` (0–1 sea life) can weight a race's terms. `World.sail_cost(ship_ms)` gives hours per directed sea edge (still-water ship speed helped or slowed by the current) for route work; the engine does not use it yet. ## CLI ``` history.py run --world DIR --config DIR --trial NAME [--seed N] [--years N] [--out DIR] [--no-preview] history.py preview TRIAL_DIR # PNGs into TRIAL_DIR/preview/ history.py compare TRIAL_A TRIAL_B [--out DIR] # seed agreement: same dominant race per occupied cell history.py check-config CONFIG_DIR [--world DIR] ``` Long runs (> 10 min or > 2 GB): reserve and cap via the shared ledger, e.g. `wf res run --mem 4G --for 30m --title "history t1" -- python history.py run …` (busy → exit 3, retry later). ## Output `/history//`: `run.toml` (everything resolved, seed, engine version), `snap/y.npz` (sparse population, adapted optima, tech per race and cell), `stats.json` (per-step totals, regions, progress toward each sub-species, events, emergence, returns, rituals, hazards, timing), `preview/*.png`. ## Layout ``` history.py CLI worldhistory/ engine modules (one per phase; engine.py drives them) tests/ python -m unittest discover -s tests -t . ``` ## Licence and credits MIT, see [LICENSE](LICENSE). Third-party notices: [licenses.md](licenses.md). Credits and the models used: [CREDITS.md](CREDITS.md). ## From the author These projects are things that have been tumbling about in my head for a long time, and I now feel like trying to do something with. The bulk of my focus here is on some of the games that I love, but every time I boot them up, I just fiddle about in the menu and set up mods and fixes for a couple hours, with my drive to play them fizzling out. This is my hope to solve that, and while I am a professional software engineer this would not have happened were it not for the rise of (relatively) cheap AI that could do the bulk of the work with me. If you don't approve, that's fine. I did these things for me, sharing them is something I do in the hopes to help others in similar situations, that just want to play the games they love. Thank you to all that made these playable in the first place, with some luck this finds you, and can bring some joy. Written with AI assistance (Claude, by Anthropic), used as an engineering tool.