Happenings: weather and seasons, minor threats, hardships, fortunes, dragons

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-19 09:34:18 +01:00
parent a4e4bd4385
commit 75327f30e0
6 changed files with 903 additions and 38 deletions

View file

@ -828,5 +828,164 @@ class ConstructionTest(unittest.TestCase):
self.assertIsNone(p["equip"]["tool_construction"])
class HappeningsTest(unittest.TestCase):
def setUp(self):
self.world = world_with(("ann", "warrior"), ("sam", "scout"))
self.world["next_event_at"] = None
for track in ("minor", "hardship", "fortune"):
self.world[f"next_{track}_at"] = None
self.city = self.world["city"]
self.rng = random.Random(4)
def test_weather_rolls_and_seasons_turn(self):
seen = set()
for hour in range(1, 24 * 14):
game.tick(self.world, T0 + hour * game.HOUR, self.rng)
seen.add(self.city["weather"]["kind"])
self.assertGreaterEqual(len(seen), 3)
self.assertEqual(game.season(self.world, T0)["name"], "Spring")
self.assertEqual(game.season(self.world, T0 + 8 * game.DAY)["name"], "Summer")
self.assertEqual(game.season(self.world, T0 + 22 * game.DAY)["name"], "Winter")
def test_weather_changes_skill_speed(self):
p = self.world["players"]["ann"]
farm = game.ACTION_BY_ID["farm_wheat"]
plain = game.action_time(self.world, p, farm)
self.city["weather"]["kind"] = "heat"
self.assertAlmostEqual(game.action_time(self.world, p, farm), plain * 0.85)
self.assertAlmostEqual(game.city_rate(self.world, "food"), 1.15)
def test_sickness_makes_the_city_eat_more(self):
game._hardship(self.world, T0, self.rng, "sickness")
self.assertAlmostEqual(game.city_rate(self.world, "food"), 1.5)
self.assertEqual(self.world["events"][-1]["report"]["icon"], "🤒")
def test_locusts_and_rot_take_from_stores_and_buildings_protect(self):
self.city["stockpile"] = {"bread": 100, "wheat": 100, "logs": 100, "stone": 100}
game._hardship(self.world, T0, self.rng, "locusts")
self.assertEqual(self.city["stockpile"]["bread"], 60)
self.assertEqual(self.city["stockpile"]["stone"], 100)
self.city["buildings"]["lumber_shed"]["level"] = 2
game._hardship(self.world, T0, self.rng, "rot")
self.assertEqual(self.city["stockpile"]["logs"], 85)
def test_storm_damages_a_building(self):
self.city["buildings"]["walls"]["level"] = 1
game._hardship(self.world, T0, self.rng, "storm")
self.assertTrue(self.city["buildings"]["walls"]["damaged"])
def test_fire_is_put_out_by_water_barrels(self):
self.city["stockpile"] = {"water_barrel": 6, "logs": 100}
game._hardship(self.world, T0, self.rng, "fire")
self.assertEqual(self.city["stockpile"], {"water_barrel": 1, "logs": 100})
self.assertIn("the fire brigade", game.city_uses("water_barrel"))
def test_winter_burns_firewood_or_the_city_goes_cold(self):
game._hardship(self.world, T0, self.rng, "winter")
self.city["stockpile"] = {"logs": 3}
game.tick(self.world, T0 + 4 * game.HOUR, self.rng)
self.assertNotIn("logs", self.city["stockpile"])
self.assertTrue(self.city["cold"])
def test_wolves_halve_farming_until_fighters_clear_them(self):
game._minor_threat(self.world, T0, self.rng, "wolves")
p = self.world["players"]["ann"]
self.assertEqual(game.skill_mods(self.world, game.ACTION_BY_ID["farm_wheat"])["yield"], 0.5)
p["xp"]["combat"] = game.XP_TABLE[30]
p["hp"] = game.max_hp(p)
self.assertIsNone(game.start_action(self.world, p, "clear_wolves", T0))
summary = game.process_player(self.world, p, T0 + 3600, random.Random(1))
self.assertIsNone(game.active_problem(self.world, "wolves"))
self.assertEqual(summary["stopped"], "That's been dealt with")
def test_collapse_blocks_ore_until_planks_shore_it_up(self):
game._minor_threat(self.world, T0, self.rng, "collapse")
p = self.world["players"]["ann"]
self.assertIn("collapsed", game.start_action(self.world, p, "mine_copper", T0))
self.assertIsNone(game.start_action(self.world, p, "mine_stone", T0))
p["bank"] = {"plank": 50}
self.assertIsNone(game.aid_problem(self.world, p, "collapse", "plank", "bank", T0))
self.assertEqual(p["bank"], {"plank": 20})
self.assertIsNone(game.active_problem(self.world, "collapse"))
def test_flood_recedes_on_its_own(self):
game._minor_threat(self.world, T0, self.rng, "flood")
game.tick(self.world, T0 + 37 * game.HOUR, self.rng)
self.assertIsNone(game.active_problem(self.world, "flood"))
def test_goblins_steal_every_hour(self):
self.city["stockpile"] = {"stone": 1000}
game._minor_threat(self.world, T0, self.rng, "goblins")
game.tick(self.world, T0 + 5 * game.HOUR, self.rng)
self.assertLess(self.city["stockpile"]["stone"], 1000)
def test_fortunes(self):
game._fortune(self.world, T0, self.rng, "harvest")
self.assertEqual(game.skill_mods(self.world, game.ACTION_BY_ID["farm_wheat"])["yield"], 2)
game._fortune(self.world, T0, self.rng, "master")
self.assertTrue(any(e["kind"] == "master" for e in self.city["effects"]))
before = self.city["treasury"]
game._fortune(self.world, T0, self.rng, "noble")
self.assertGreater(self.city["treasury"], before)
game._fortune(self.world, T0, self.rng, "pilgrims")
self.assertEqual(self.world["players"]["ann"]["bank"]["bones"], 10)
game.tick(self.world, T0 + 13 * game.HOUR, self.rng)
self.assertFalse(any(e["kind"] == "harvest" for e in self.city["effects"]))
def test_festival_boosts_favour(self):
game._fortune(self.world, T0, self.rng, "festival")
p = self.world["players"]["ann"]
p["bank"] = {"stone": 100}
game.donate(self.world, p, "stone", 100, T0)
self.assertEqual(p["favour"], 150)
def test_mercenaries_join_the_next_defence(self):
self.world["next_event_at"] = T0
game.tick(self.world, T0, self.rng)
game._fortune(self.world, T0, self.rng, "mercenaries")
p = self.world["players"]["ann"]
self.city["treasury"] = 5000
before = game.defence(self.world)["total"]
self.assertIsNone(game.hire_mercenaries(self.world, p, T0))
self.assertEqual(game.defence(self.world)["total"], before + 25)
def test_dragons_burn_when_they_win(self):
self.city["buildings"]["houses"]["level"] = 1
self.city["stockpile"] = {"logs": 500, "bread": 200}
self.world["events"].append({"id": 99, "kind": "dragon", "status": "upcoming", "announced": T0,
"fire_at": T0 + 10, "recon": 0, "sabotage": 0.0, "jitter": 0,
"strength": 1000, "group": "a green dragon", "trait": "x"})
self.assertEqual(game.upcoming_raid(self.world)["kind"], "dragon")
game.tick(self.world, T0 + 20, self.rng)
report = self.world["events"][-1]["report"]
self.assertIn("ablaze", report["title"])
self.assertTrue(self.city["buildings"]["houses"]["damaged"])
self.assertLess(self.city["stockpile"]["logs"], 500)
def test_tracks_fire_on_their_own_cooldowns(self):
self.world["next_fortune_at"] = T0
self.world["next_hardship_at"] = T0
game.tick(self.world, T0 + 1, self.rng)
tracks = [e.get("track") for e in self.world["events"]]
self.assertIn("fortune", tracks)
self.assertIn("hardship", tracks)
self.assertGreater(self.world["next_fortune_at"], T0 + 30 * game.HOUR)
def test_v7_worlds_get_weather_and_tracks(self):
world = world_with()
world["version"] = 7
for key in ("weather", "effects", "problems", "hired"):
del world["city"][key]
for key in ("granary", "lumber_shed", "well"):
del world["city"]["buildings"][key]
for track in ("minor", "hardship", "fortune"):
del world[f"next_{track}_at"]
game.migrate(world, T0)
self.assertEqual(world["city"]["weather"]["kind"], "clear")
self.assertIn("granary", world["city"]["buildings"])
self.assertIsNotNone(world["next_fortune_at"])
if __name__ == "__main__":
unittest.main()