From fa960e1d6c06444d3dbd98ec5879974f739c168b Mon Sep 17 00:00:00 2001 From: rope Date: Wed, 28 Oct 2026 08:23:14 +0000 Subject: [PATCH] Track donations per item Co-Authored-By: Claude Opus 5.5 --- game.py | 47 +++++++++++++++++++++++++++------------------- tests/test_game.py | 20 ++++++++++++++++++++ 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/game.py b/game.py index a74541c..a613023 100644 --- a/game.py +++ b/game.py @@ -1542,7 +1542,7 @@ RAID_TRAITS = ["lightly armoured, heavy on archers", "mounted and fast, few bows # --- world --------------------------------------------------------------- -WORLD_VERSION = 33 +WORLD_VERSION = 34 def new_world(now: float, speed: float = 1.0) -> dict: @@ -1798,6 +1798,11 @@ def migrate(world: dict, now: float | None = None) -> dict: for player in world["players"].values(): player.setdefault("bury", True) world["version"] = 33 + if world["version"] < 34: + # Donation history per item, from here on. + for player in world["players"].values(): + player.setdefault("gave", {}) + world["version"] = 34 return world @@ -2058,6 +2063,7 @@ def new_player(uid: str, name: str, cls: str, now: float) -> dict: "rented": 0, "boosts": [], "bought": None, "deeds": {}, "owned": [], "look": dict(DEFAULT_LOOK), "garrison": False, "donated": 0, + "gave": {}, # item (or "gold"): {qty, worth, favour} given to the city, see credit() "pinned": [], # skills pinned to the top of the sidebar "tracked": None, # recipe shown under the top bar } @@ -2131,13 +2137,26 @@ def bank_add(world: dict, player: dict, item: str, qty: int, now: float, summary overflow = summary["overflow"] stockpile = world["city"]["stockpile"] stockpile[item] = stockpile.get(item, 0) + spill - gain = spill * ITEMS[item]["value"] - player["favour"] += gain * OVERFLOW_FAVOUR - player["donated"] += gain + credit(player, item, spill, OVERFLOW_FAVOUR) overflow[item] = overflow.get(item, 0) + spill return keep +def credit(player: dict, item: str, qty: int, rate: float) -> None: + """Favour for giving `qty` of an item (or gold) to the city, at `rate` per point of worth. + + Every gift is tallied per item in `gave`: how many, their worth at the time, and the + favour paid for them, so a later revalue can recompute favour exactly. + """ + worth = qty * (1 if item == "gold" else ITEMS[item]["value"]) + player["favour"] += worth * rate + player["donated"] += worth + tally = player["gave"].setdefault(item, {"qty": 0, "worth": 0, "favour": 0}) + tally["qty"] += qty + tally["worth"] += worth + tally["favour"] += worth * rate + + def bank_take(player: dict, item: str, qty: int) -> None: left = player["bank"][item] - qty if left > 0: @@ -4036,7 +4055,6 @@ def donate(world: dict, player: dict, item: str, qty: int, now: float) -> str | return "Not enough gold" player["gold"] -= qty city["treasury"] += qty - gain = qty label = f"{qty:,} gold" else: if not city_uses(item): @@ -4045,10 +4063,8 @@ def donate(world: dict, player: dict, item: str, qty: int, now: float) -> str | return "You don't have that many" bank_take(player, item, qty) city["stockpile"][item] = city["stockpile"].get(item, 0) + qty - gain = qty * ITEMS[item]["value"] label = amount(item, qty) - player["favour"] += gain * city_rate(world, "favour") - player["donated"] += gain + credit(player, item, qty, city_rate(world, "favour")) log(world, now, "donation", f"{player['name']} donated {label}", player["id"]) return None @@ -4115,9 +4131,7 @@ def fund(world: dict, player: dict, key: str, item: str, qty: int | str, source: player["gold"] -= qty else: bank_take(player, item, qty) - gain = qty * (1 if item == "gold" else ITEMS[item]["value"]) - player["favour"] += gain * city_rate(world, "favour") - player["donated"] += gain + credit(player, item, qty, city_rate(world, "favour")) elif item == "gold": city["treasury"] -= qty else: @@ -4148,9 +4162,7 @@ def install(world: dict, player: dict, fitting: str, source: str, now: float) -> if not player["bank"].get(fitting): return "You don't have one" bank_take(player, fitting, 1) - gain = ITEMS[fitting]["value"] - player["favour"] += gain * city_rate(world, "favour") - player["donated"] += gain + credit(player, fitting, 1, city_rate(world, "favour")) elif source == "city": if not city["stockpile"].get(fitting): return "There isn't one in the stockpile" @@ -4379,8 +4391,7 @@ def pay_wages(world: dict, player: dict, qty, source: str, now: float) -> str | qty = min(qty, room) if source == "bank": player["gold"] -= qty - player["favour"] += qty * city_rate(world, "favour") - player["donated"] += qty + credit(player, "gold", qty, city_rate(world, "favour")) log(world, now, "donation", f"{player['name']} paid {qty:,} gold of guard wages", player["id"]) else: city["treasury"] -= qty @@ -5417,9 +5428,7 @@ def aid_problem(world: dict, player: dict, kind: str, item: str, source: str, no return "Nothing to give" if source == "bank": bank_take(player, item, qty) - gain = qty * ITEMS[item]["value"] * city_rate(world, "favour") - player["favour"] += gain - player["donated"] += qty * ITEMS[item]["value"] + credit(player, item, qty, city_rate(world, "favour")) else: take_stock_qty(stock, item, qty) problem["progress"][item] = problem["progress"].get(item, 0) + qty diff --git a/tests/test_game.py b/tests/test_game.py index ee36140..d54205c 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -261,6 +261,26 @@ class ActionTest(unittest.TestCase): self.assertEqual(world["city"]["stockpile"]["stone"], 3) self.assertEqual(p["favour"], 1.5) # half favour for overflow self.assertEqual(p["donated"], 3) + self.assertEqual(p["gave"], {"stone": {"qty": 3, "worth": 3, "favour": 1.5}}) + + def test_every_gift_is_tallied_by_item(self): + world = world_with(("fred", "commoner")) + p = world["players"]["fred"] + p["bank"], p["gold"] = {"bricks": 20}, 100 + brick = game.ITEMS["bricks"]["value"] + self.assertIsNone(game.donate(world, p, "bricks", 5, T0)) + self.assertIsNone(game.donate(world, p, "gold", 40, T0)) + self.assertIsNone(game.fund(world, p, "walls", "bricks", 3, "bank", T0)) + self.assertEqual(p["gave"]["bricks"], {"qty": 8, "worth": 8 * brick, "favour": 8 * brick}) + self.assertEqual(p["gave"]["gold"], {"qty": 40, "worth": 40, "favour": 40}) + self.assertEqual(sum(t["favour"] for t in p["gave"].values()), p["favour"]) + + def test_migrate_starts_an_empty_donation_tally(self): + world = world_with(("fred", "commoner")) + del world["players"]["fred"]["gave"] + world["version"] = 33 + game.migrate(world, T0) + self.assertEqual(world["players"]["fred"]["gave"], {}) def test_full_bank_sells_what_the_city_cannot_use(self): world = world_with(("fred", "commoner"))