global-leaders / tests /test_engine.py
Leonardo Camilo
Global Leaders — political-strategy game on a ≤32B model
cc5d530
Raw
History Blame Contribute Delete
13.1 kB
"""Engine unit tests — run with: python -m unittest discover -s tests
No third-party deps; uses stdlib unittest. Variance is pinned with FixedDice."""
from __future__ import annotations
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from engine.countries import get_country # noqa: E402
from engine.dice import Dice, Roll # noqa: E402
from engine.events import Event, event_weight, meets_preconditions # noqa: E402
from engine.game import Game # noqa: E402
from engine.llm import FakeLLM # noqa: E402
from engine.resolver import check_game_over, end_of_month_upkeep, resolve # noqa: E402
from engine.schemas import JudgeOutput, SchemaError, parse_objectives # noqa: E402
from engine.state import WorldState # noqa: E402
class FixedDice(Dice):
"""Deterministic dice for tests: always the same factor/mode."""
def __init__(self, factor: float = 1.0, mode: str = "normal"):
super().__init__(0)
self._factor, self._mode = factor, mode
def roll(self, turn: int, idx: int = 0) -> Roll:
return Roll(self._factor, self._mode)
def fresh_china() -> tuple[WorldState, object]:
c = get_country("china")
s = WorldState(country="china", month=4, indicators=dict(c.indicators), factions=dict(c.factions))
for k, a in c.agents.items():
s.agent_favor[k] = a.start_favor
return s, c
def clique_event() -> Event:
from engine.events import CHINA_EVENTS
return next(e for e in CHINA_EVENTS if e.id == "cn_2025_04_pla_clique")
class TestDeltasAndGuardrails(unittest.TestCase):
def test_magnitude_clamp(self):
s, c = fresh_china()
judge = JudgeOutput.parse({
"interpretation": "huge econ push", "magnitude": "major", "plausibility": "ok",
"effects": [{"indicator": "economy", "direction": "+", "proposed_delta": 50}],
})
res = resolve(s, c, clique_event(), judge, FixedDice(1.0), turn=4)
# |50| clamped to major hi (10); factor 1.0 -> +10.
self.assertEqual(res.applied["economy"], 10)
def test_no_free_lunch_injects_cost(self):
s, c = fresh_china()
judge = JudgeOutput.parse({
"interpretation": "all upside", "magnitude": "major", "plausibility": "ok",
"effects": [{"indicator": "economy", "direction": "+", "proposed_delta": 10}],
})
res = resolve(s, c, clique_event(), judge, FixedDice(1.0), turn=4)
self.assertTrue(any(v < 0 for v in res.applied.values()), "major move must carry a trade-off")
def test_impossible_is_noop(self):
s, c = fresh_china()
before = dict(s.indicators)
judge = JudgeOutput.parse({
"interpretation": "turn rivers to gold", "magnitude": "minor",
"plausibility": "impossible", "effects": [],
})
res = resolve(s, c, clique_event(), judge, FixedDice(1.0), turn=4)
self.assertEqual(res.mode, "rejected")
self.assertEqual(s.indicators, before)
def test_clamped_to_0_100(self):
s, c = fresh_china()
s.indicators["fiscal_health"] = 3
judge = JudgeOutput.parse({
"interpretation": "spend big", "magnitude": "radical", "plausibility": "ok",
"effects": [{"indicator": "fiscal_health", "direction": "-", "proposed_delta": 20}],
})
resolve(s, c, clique_event(), judge, FixedDice(1.0), turn=4)
self.assertGreaterEqual(s.indicators["fiscal_health"], 0)
class TestDice(unittest.TestCase):
def test_reproducible(self):
d1, d2 = Dice(123), Dice(123)
for t in range(1, 13):
r1, r2 = d1.roll(t, 1), d2.roll(t, 1)
self.assertEqual((r1.factor, r1.mode), (r2.factor, r2.mode))
def test_backfire_inverts_sign(self):
roll = Roll(1.0, "backfire")
self.assertLess(roll.apply(10), 0)
class TestSchemas(unittest.TestCase):
def test_bad_magnitude_rejected(self):
with self.assertRaises(SchemaError):
JudgeOutput.parse({"magnitude": "huge", "plausibility": "ok", "effects": []})
def test_objectives_must_be_eight(self):
with self.assertRaises(SchemaError):
parse_objectives({"objectives": [{"title": "x", "metric": {"indicator": "economy",
"op": ">=", "target": 50}}]})
def test_fake_llm_objectives_valid(self):
s, c = fresh_china()
objs = parse_objectives(FakeLLM().setup_objectives(c, s))
self.assertEqual(len(objs), 8)
class TestGameOver(unittest.TestCase):
def test_dem_removal(self):
c = get_country("usa")
s = WorldState(country="usa", indicators=dict(c.indicators))
s.indicators["approval"] = 10
s.indicators["institutional_stability"] = 20
check_game_over(s, c)
self.assertEqual(s.game_over, "removed_from_office")
def test_china_party_ouster(self):
s, c = fresh_china()
s.factions["party_loyalty"] = 10
check_game_over(s, c)
self.assertEqual(s.game_over, "party_ouster")
def test_china_coup_when_pla_collapses(self):
s, c = fresh_china()
s.factions["pla_loyalty"] = 15
check_game_over(s, c)
self.assertEqual(s.game_over, "pla_coup")
def test_china_coup_plot_matures_when_neglected(self):
s, c = fresh_china()
s.flags["pla_clique_handled"] = False
s.factions["pla_loyalty"] = 35
for t in range(4, 13):
end_of_month_upkeep(s, c, FixedDice(1.0), turn=t)
if s.game_over:
break
self.assertEqual(s.game_over, "pla_coup")
class TestElites(unittest.TestCase):
def test_every_country_has_an_elite(self):
from engine.countries import COUNTRIES
for key, c in COUNTRIES.items():
elites = [f for f in c.roster if f.faction == "elite"]
self.assertEqual(len(elites), 1, f"{key} should have exactly one elite figure")
self.assertGreater(elites[0].influence, 0)
def test_elite_influence_differs_by_country(self):
from engine.countries import get_country
br = next(f for f in get_country("brazil").roster if f.faction == "elite")
cn = next(f for f in get_country("china").roster if f.faction == "elite")
self.assertGreater(br.influence, cn.influence) # Brazil elite >> China elite
def test_hostile_elite_drags_economy(self):
from engine.countries import get_country
from engine.resolver import _elite_pressure
c = get_country("brazil")
s = WorldState(country="brazil", indicators=dict(c.indicators))
s.agent_favor["elites"] = -80.0 # furious elite
eco0, fis0 = s.indicators["economy"], s.indicators["fiscal_health"]
_elite_pressure(s, c)
self.assertLess(s.indicators["economy"], eco0)
self.assertLess(s.indicators["fiscal_health"], fis0)
class TestCalibration(unittest.TestCase):
def test_resilience_lifts_extreme_lows(self):
from engine.resolver import _resilience
s = WorldState(country="usa", indicators={"economy": 20, "approval": 50, "security": 90})
_resilience(s)
self.assertEqual(s.indicators["economy"], 22) # <25 -> +2
self.assertEqual(s.indicators["approval"], 50) # mid -> unchanged
self.assertEqual(s.indicators["security"], 89) # >88 -> -1
def test_pla_coup_warning_fires_before_the_coup(self):
s, c = fresh_china()
s.flags["pla_clique_handled"] = False
s.factions["pla_loyalty"] = 35
warned = False
for t in range(4, 12):
end_of_month_upkeep(s, c, FixedDice(1.0), turn=t)
if s.pending_alert:
warned = True
if s.game_over:
break
self.assertTrue(warned, "the player must be warned before the PLA coup")
class TestChinaArc(unittest.TestCase):
def test_purge_defuses_plot(self):
s, c = fresh_china()
ev = clique_event()
from engine.schemas import parse_options
options = parse_options(FakeLLM().options(ev, s))
purge = next(o for o in options if o.id == "A")
judge = JudgeOutput.parse(FakeLLM().judge(ev, purge, s))
resolve(s, c, ev, judge, FixedDice(1.0), turn=4)
self.assertTrue(s.flags.get("pla_clique_handled"))
self.assertEqual(s.factions["coup_plot_progress"], 0)
class TestPersonas(unittest.TestCase):
def test_every_figure_has_a_persona_file(self):
from engine.countries import COUNTRIES
from engine.personas import load_persona
for key, c in COUNTRIES.items():
for f in c.roster:
self.assertIsNotNone(load_persona(key, f.key),
f"missing persona file for {key}/{f.key}")
def test_persona_strips_frontmatter(self):
from engine.personas import load_persona
body = load_persona("france", "opposition_rn")
self.assertFalse(body.startswith("---"))
self.assertIn("#", body) # has the markdown heading / prose
def test_converse_records_transcript(self):
game = Game("france", FakeLLM(), seed=7)
game.start()
r1 = game.converse("opposition_rn", "Will you back me?")
self.assertTrue(r1)
game.converse("opposition_rn", "And on immigration?")
self.assertEqual(len(game.conversations["opposition_rn"]), 2)
def test_converse_reply_schema(self):
from engine.schemas import SchemaError, parse_chat_reply
self.assertEqual(parse_chat_reply({"reply": "hi"}), "hi")
with self.assertRaises(SchemaError):
parse_chat_reply({"reply": ""})
class TestEventSelection(unittest.TestCase):
def test_preconditions(self):
ev = Event(id="x", type="conditional", title_seed="t", fact="f",
touches=["security"], preconditions={"security": "<35"})
s = WorldState(country="china", indicators={"security": 30})
self.assertTrue(meets_preconditions(ev, s))
s.indicators["security"] = 60
self.assertFalse(meets_preconditions(ev, s))
def test_weakness_raises_weight(self):
ev = Event(id="x", type="conditional", title_seed="t", fact="f", touches=["economy"])
weak = WorldState(country="china", indicators={"economy": 10})
strong = WorldState(country="china", indicators={"economy": 90})
self.assertGreater(event_weight(ev, weak), event_weight(ev, strong))
class TestSeeds(unittest.TestCase):
def test_global_event_hook_manifests_per_country(self):
from engine.seeds import GlobalEvent, manifest_hook
ge = GlobalEvent(
id="iran_war", title="Israel-Iran war", fact="Strikes escalate.", month=6,
hooks={"usa": {"touches": ["international_power", "security"],
"dilemma": "Intervene or stay out?", "figures": ["sec_state"]}},
)
ev = manifest_hook(ge, "usa")
self.assertEqual(ev.id, "iran_war__usa")
self.assertEqual(ev.month, 6)
self.assertIn("international_power", ev.touches)
self.assertIsNone(manifest_hook(ge, "brazil")) # no hook for brazil
def test_world_modifier_applies_and_decays(self):
from engine.seeds import GlobalEvent, WorldModifier, apply_active_modifiers, fire_global_events
from engine.countries import get_country
c = get_country("russia")
s = WorldState(country="russia", indicators=dict(c.indicators))
eco0 = s.indicators["economy"]
ge = GlobalEvent(id="oil_shock", title="Oil spikes", fact="Prices jump.", month=6,
world_modifier=WorldModifier("oil_shock", "Oil price spike", months=2,
deltas={"russia": {"economy": +5}}))
injected = fire_global_events([ge], s, month=6)
self.assertEqual(injected, []) # russia has no hook -> background modifier instead
self.assertEqual(len(s.active_modifiers), 1)
apply_active_modifiers(s)
self.assertEqual(s.indicators["economy"], eco0 + 5)
apply_active_modifiers(s) # second (last) month
self.assertEqual(s.active_modifiers, []) # expired
def run_neglect(seed: int) -> Game:
"""Inline 'always tolerate / stand down' playthrough (no CLI dependency)."""
game = Game("china", FakeLLM(), seed=seed)
game.start()
while not game.is_over and game.state.month <= 12:
for ev in game.month_events():
_narr, options = game.present(ev)
choice = next((o for o in options if o.id == "B"), options[0])
game.act(ev, choice)
if game.is_over:
break
game.end_month()
return game
class TestFullPlaythrough(unittest.TestCase):
def test_neglect_china_runs_to_an_ending(self):
game = run_neglect(seed=2025)
self.assertTrue(game.is_over)
self.assertIsNotNone(game.state.game_over)
if __name__ == "__main__":
unittest.main(verbosity=2)