worldhistory

git clone https://git.godosa.eu/worldhistory

master

raw ยท 8510 bytes

import tempfile
import tomllib
import unittest
from pathlib import Path

from tests.helpers import make_history, make_race
from worldhistory.config import ConfigError, dumps_toml, link, load_config, parse_race, resolved

RACE = """
id = "stone"
name = "Stone folk"
colour = [140, 140, 150]
[demography]
density = 1.2
growth = 0.02
allee = 75
[seed]
clusters = 25
heads = 200
[habitat]
realm = "land"
terms = [{ p = "mountain", w = 0.7 }, { p = ["land", "coal"], w = 0.5 }]
gates = [{ penalty = ["above:o2_fraction:0.3", "lithology:3,4"], factor = 0.6 }]
[tolerance.temperature]
optimum = 10
width = 20
lo = -30
hi = 40
rate = 0.001
[special]
veins = 0.002
"""

SUB = """
id = "deep"
[demography]
density = 0.5
growth = 0.08
[habitat]
realm = "sea"
terms = [{ p = "vent", w = 1.0 }]
qmax = 0.7
[emerge]
parent = "stone"
birth_min = 0.5
[tolerance.temperature]
optimum = 30
width = 10
lo = 0
hi = 50
"""

HISTORY = """
[run]
years = 7500
seed = 3
[world]
eras = ["late"]
[regions.zone]
kind = "changed"
era = "late"
fields = ["gravity_g"]
[[event]]
year = 0
kind = "seed"
[[event]]
year = 20
kind = "cull"
region = "zone"
share = 0.6
by_race = { stone = 0.8 }
[[event]]
year = 20
kind = "era_switch"
era = "late"
"""


def write(d, name, text):
    p = Path(d) / name
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(text)


class ConfigTest(unittest.TestCase):
    def test_load_defaults_and_inheritance(self):
        with tempfile.TemporaryDirectory() as d:
            write(d, "history.toml", HISTORY)
            write(d, "races/stone.toml", RACE)
            write(d, "races/deep.toml", SUB)
            h, races = load_config(d)
        self.assertEqual([r.id for r in races], ["stone", "deep"])
        stone, deep = races
        self.assertEqual(h.step, 10)                       # default
        self.assertEqual(h.years, 7500)
        self.assertEqual(stone.founder, 75)                # founder defaults to allee
        self.assertEqual(stone.family, "stone")
        self.assertEqual(deep.family, "stone")             # sub-species joins the parent's family
        self.assertEqual(deep.tolerance["temperature"].width, 10)   # own curve
        self.assertEqual(deep.clusters, 0)                 # sub-species are never seeded
        self.assertEqual(stone.travel["river"], -1.0)      # default
        self.assertEqual(stone.gates[0]["factor"], 0.6)
        self.assertEqual(h.events[1]["by_race"], {"stone": 0.8})

    def test_unknown_key(self):
        with self.assertRaisesRegex(ConfigError, r"test:x.*demography.*desnity"):
            parse_race({"id": "x", "demography": {"desnity": 1}}, "test:x")

    def test_unknown_predicate(self):
        with self.assertRaisesRegex(ConfigError, "nosuch"):
            make_race(habitat={"terms": [{"p": "nosuch", "w": 1}]})

    def test_bad_tolerance_condition(self):
        with self.assertRaisesRegex(ConfigError, "wind"):
            make_race(tolerance={"wind": {"optimum": 0, "width": 1, "lo": 0, "hi": 1}})

    def test_tolerance_needs_widths(self):
        with self.assertRaisesRegex(ConfigError, "width"):
            make_race(tolerance={"depth": {"optimum": 0, "lo": 0, "hi": 10, "width_lo": 5}})

    def test_link_checks_references(self):
        h = make_history(event=[{"year": 20, "kind": "cull", "region": "nowhere", "share": 0.5}])
        with self.assertRaisesRegex(ConfigError, "nowhere"):
            link(h, [make_race()])
        h = make_history(event=[{"year": 20, "kind": "cull", "share": 0.5, "by_race": {"ghost": 0.1}}])
        with self.assertRaisesRegex(ConfigError, "ghost"):
            link(h, [make_race()])
        h = make_history(event=[{"year": 20, "kind": "era_switch", "era": "late"}])
        with self.assertRaisesRegex(ConfigError, "late"):
            link(h, [make_race()])
        sub = make_race("s", emerge={"parent": "p"})
        with self.assertRaisesRegex(ConfigError, "p"):
            link(make_history(), [sub])
        same = {"gravity": {"optimum": 1.0, "width": 0.5, "lo": 0.3, "hi": 1.3}}
        with self.assertRaisesRegex(ConfigError, "never be born"):            # progress stays 0 < birth_min
            link(make_history(), [make_race("p", tolerance=same), make_race("s", tolerance=same, emerge={"parent": "p"})])
        link(make_history(), [make_race("p", tolerance=same),                 # birth_min 0 or ritual: fine
                              make_race("s", emerge={"parent": "p", "birth_min": 0.0, "birth_sure": 0.0})])
        link(make_history(), [make_race("p", tolerance=same), make_race("s", emerge={"parent": "p", "mode": "ritual"})])

    def test_emerge_defaults_and_merge(self):
        with tempfile.TemporaryDirectory() as d:
            write(d, "history.toml", HISTORY)
            write(d, "races/stone.toml", RACE.replace("[special]", "[tolerance.depth]\noptimum = 0\nwidth = 60\n"
                                                                    "lo = 0\nhi = 6000\n[special]"))
            write(d, "races/deep.toml", SUB)
            h, (stone, deep) = load_config(d)
        em = deep.emerge
        self.assertEqual((em["mode"], em["birth_min"], em["birth_sure"], em["birth_rate"]), ("adapt", 0.5, 0.8, 0.2))
        self.assertEqual((em["return_min"], em["return_sure"]), (0.5, 0.8))      # default to the birth values
        self.assertEqual((em["mix_min"], em["displace"], em["spread"]), (0.3, 0.9, "contact"))
        self.assertEqual(deep.tolerance["temperature"].optimum, 30)              # own curve
        self.assertEqual(deep.tolerance["depth"].width_hi, 60)                   # inherited condition

    def test_removed_keys_name_the_replacement(self):
        with self.assertRaisesRegex(ConfigError, "removed.*birth_min"):
            make_race("s", emerge={"parent": "p", "class": "deep_sea"})
        with self.assertRaisesRegex(ConfigError, "removed.*birth_min"):
            make_race("s", emerge={"parent": "p", "years": [1, 2]})
        with self.assertRaisesRegex(ConfigError, "exposure.*removed"):
            make_history(exposure={"hot": "above:T_mean:10"})
        with self.assertRaisesRegex(ConfigError, "trigger"):
            make_race("s", emerge={"parent": "p", "trigger": 0.1})               # ritual only
        with self.assertRaisesRegex(ConfigError, "crisis_drop"):
            make_race("s", emerge={"parent": "p", "crisis_drop": 0.9})           # ritual only
        with self.assertRaisesRegex(ConfigError, "victims"):
            make_race("s", emerge={"parent": "p", "mode": "ritual", "victims": "everyone"})
        with self.assertRaisesRegex(ConfigError, "crisis_drop"):
            make_race("s", emerge={"parent": "p", "mode": "ritual", "crisis_drop": 1.0})
        self.assertEqual(make_race("s", emerge={"parent": "p", "birth_rate": 0.4}).emerge["return_rate"], 0.4)
        for key, bad in [("displace", 1.5), ("dominance", -0.1), ("birth_shift", 1.2), ("convert", 0.0),
                         ("mix_min", 1.1), ("birth_rate", 2.0), ("return_rate", -1.0), ("after", -5)]:
            with self.assertRaisesRegex(ConfigError, key):
                make_race("s", emerge={"parent": "p", key: bad})
        for key, bad in [("ritual_rate", -0.1), ("ritual_cost", -1), ("ritual_converts", -1), ("trigger", 1.5),
                         ("raid_size", 0), ("raid_km", -1), ("raid_rate", -1), ("ritual_power", 0),
                         ("backlash_years", -1), ("crisis_years", 0)]:
            with self.assertRaisesRegex(ConfigError, key):
                make_race("s", emerge={"parent": "p", "mode": "ritual", key: bad})
        with self.assertRaisesRegex(ConfigError, "mode"):
            make_race("s", emerge={"parent": "p", "mode": "magic"})
        with self.assertRaisesRegex(ConfigError, "birth_min"):
            make_race("s", emerge={"parent": "p", "birth_min": 0.9, "birth_sure": 0.5})

    def test_dumps_roundtrip(self):
        h = make_history(regions={"z": {"kind": "box", "lat": [0, 1], "lon": [2, 3]}},
                         event=[{"year": 0, "kind": "seed"}, {"year": 20, "kind": "cull", "share": 0.3,
                                                                "by_race": {"a": 0.5}}])
        doc = resolved(h, [make_race()], seed=5, engine="abc")
        back = tomllib.loads(dumps_toml(doc))
        self.assertEqual(back["seed"], 5)
        self.assertEqual(back["history"]["events"][1]["by_race"], {"a": 0.5})
        self.assertEqual(back["race"][0]["id"], "a")
        self.assertEqual(back["history"]["regions"]["z"]["lat"], [0, 1])