aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/README.md
blob: e99e6126f7a1d4c5e912f3bba7c6bc00860cb248 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# 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<res>/cells.npz`, optionally `eras/<era>/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/<race>.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

`<world>/history/<trial>/`: `run.toml` (everything resolved, seed, engine version), `snap/y<year>.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.