Townsfolk mood: needs, kindling, clothes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-28 10:46:43 +00:00
parent 002e990dc4
commit 58a3e66454
6 changed files with 385 additions and 30 deletions

View file

@ -3242,5 +3242,116 @@ class PartyTest(unittest.TestCase):
self.assertEqual(world["parties"], {})
class TownsfolkTest(unittest.TestCase):
def setUp(self):
self.world = world_with()
self.city = self.world["city"]
self.city["population"] = 100.0
self.city["stockpile"] = {}
self.now = self.city["last_tick"]
def test_a_new_city_starts_content(self):
self.assertEqual(self.city["happiness"], 0.5)
self.assertEqual(game.mood(self.world)["name"], "Content")
def test_each_tier_adds_a_need(self):
for tier, keys in enumerate([["food"], ["food", "warmth"], ["food", "warmth", "clothes"]]):
self.city["tier"] = tier
self.assertEqual(list(game.happiness(self.world, self.now)["needs"]), keys)
def test_food_counts_supply_and_kinds(self):
# 100 people eat 50 meals a day; a hamlet wants 2 days stored and 4 kinds.
self.city["stockpile"] = {"bread": 100, "cooked_meat": 49}
food = game.happiness(self.world, self.now)["needs"]["food"]
self.assertAlmostEqual(food["score"], 0.5 * 1 + 0.5 * 1 / 4) # meat is short of a day's worth
self.city["starving"] = True
self.assertEqual(game.happiness(self.world, self.now)["needs"]["food"]["score"], 0)
def test_the_biggest_pile_is_eaten_first(self):
self.city["stockpile"] = {"bread": 10, "cooked_meat": 500}
game._city_hour(self.world, self.now)
self.assertEqual(self.city["stockpile"]["bread"], 10)
self.assertLess(self.city["stockpile"]["cooked_meat"], 500)
def test_hearths_burn_kindling_first_and_only_from_a_village(self):
self.city["stockpile"] = {"kindling": 10, "logs": 10}
self.city["weather"]["kind"] = "clear"
game._happenings_hour(self.world, self.now, random.Random(1))
self.assertEqual(self.city["stockpile"], {"kindling": 10, "logs": 10}) # a hamlet has no hearths
self.city["tier"] = 1
game._happenings_hour(self.world, self.now, random.Random(1))
self.assertLess(self.city["stockpile"]["kindling"], 10)
self.assertEqual(self.city["stockpile"]["logs"], 10)
def test_no_fuel_means_cold_homes(self):
self.city["tier"] = 1
self.city["weather"]["kind"] = "clear"
game._happenings_hour(self.world, self.now, random.Random(1))
self.assertTrue(self.city["unheated"])
self.assertFalse(self.city["cold"]) # no extra food unless it's snowing
self.assertEqual(game.happiness(self.world, self.now)["needs"]["warmth"]["score"], 0)
def test_clothes_for_the_season_wear_out(self):
self.city["tier"] = 2
garment = game.SEASON_CLOTHES[game.season(self.world, self.now)["index"]]
self.city["stockpile"] = {garment: 100}
self.assertEqual(game.happiness(self.world, self.now)["needs"]["clothes"]["score"], 1)
for _hour in range(24):
game._townsfolk_hour(self.world, self.now, random.Random(1))
# 100 people wear out 100 in a 7-day season: about 14 a day.
self.assertEqual(self.city["stockpile"][garment], 100 - int(100 / game.SEASON_DAYS))
def test_mood_drifts_slowly(self):
self.assertEqual(game.happiness(self.world, self.now)["target"], 0) # nothing to eat
game._townsfolk_hour(self.world, self.now, random.Random(1))
self.assertAlmostEqual(self.city["happiness"], 0.5 - game.MOOD_DRIFT)
def test_unsolved_problems_weigh_on_the_mood(self):
self.city["stockpile"] = {"bread": 1000, "cooked_meat": 100, "stew": 100, "cooked_shrimp": 100}
self.assertEqual(game.happiness(self.world, self.now)["target"], 1)
self.city["problems"].append({"kind": "goblins", "started": self.now, "progress": {}, "work": 0})
self.assertAlmostEqual(game.happiness(self.world, self.now)["target"], 1 - game.PROBLEM_MOOD)
def test_a_lost_raid_shocks_the_mood(self):
event = {"strength": 10_000, "sabotage": 0, "group": "the Red Hand"}
game._resolve_raid(self.world, event, self.now, random.Random(1))
self.assertAlmostEqual(self.city["happiness"], 0.5 + game.MOOD_SHOCKS["raid_lost"])
def test_misery_drives_people_away_and_weakens_the_guards(self):
self.city["buildings"]["barracks"]["level"] = 1
self.city["wages"] = 10_000
self.city["stockpile"] = {"bread": 1000}
content = game.defence(self.world)["parts"]["militia"]
self.city["happiness"] = 0.1
self.assertAlmostEqual(game.defence(self.world)["parts"]["militia"], round(content * 0.8, 1))
self.city["population"] = game.housing(self.world) # full: no growth to hide the leaving
before = self.city["population"]
game._city_hour(self.world, self.now)
self.assertAlmostEqual(self.city["population"], before * (1 - 0.03 / 24))
def test_unhappy_halts_growth(self):
self.city["stockpile"] = {"bread": 1000}
self.city["happiness"] = 0.3
self.city["population"] = game.housing(self.world) - 5.0 # room to grow
before = self.city["population"]
game._city_hour(self.world, self.now)
self.assertEqual(self.city["population"], before)
self.assertTrue(game.population_view(self.world)["halted"])
def test_misery_makes_trouble(self):
self.city["happiness"] = 0.05
rng = random.Random(1)
rng.random = lambda: 0.0
game._townsfolk_hour(self.world, self.now, rng)
self.assertIn(self.world["events"][-1]["kind"], ("strike", "theft", "riot"))
def test_kindling_is_split_under_firekeeping(self):
split = game.ACTION_BY_ID["split_willow_logs"]
self.assertEqual((split["skill"], split["inputs"], split["outputs"]),
("firekeeping", {"willow_logs": 1}, {"kindling": 4}))
self.assertIn("the hearths", game.city_uses("kindling"))
if __name__ == "__main__":
unittest.main()