From 71d7ca704b9106ec3a9fd549c64b5bc4fbecc4e2 Mon Sep 17 00:00:00 2001 From: rope Date: Mon, 19 Oct 2026 10:52:07 +0100 Subject: [PATCH] Bank: a full stack spills into another slot Co-Authored-By: Claude Opus 5.5 --- README.md | 5 +++-- app.py | 1 + game.py | 15 +++++++++++---- static/app.js | 18 ++++++++++++------ tests/test_game.py | 17 ++++++++++++++--- 5 files changed, 41 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 872ca79..a3eafa6 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,9 @@ Alpha. One season, bandit raids plus two friendly events, six buildings. city). Gear needs a combat level to wear, tools a skill level to use. - **Classes:** warrior, scout, artisan, pioneer. Every class can train every skill; a class makes some skills 20% faster and has one signature ability. -- **Bank:** 12 slots of up to 1,000 each to start. When something doesn't - fit it goes to the city stockpile (or the action stops, if you prefer). +- **Bank:** 12 slots of 1,000 each to start; more than a stack of one item + spills into another slot. When something doesn't fit it goes to the city + stockpile (or the action stops, if you prefer). Raids never touch your bank. Players can give items to each other. - **City:** each building collects its own fund, from players' banks or the city's stockpile and treasury. Funds are safe from raiders, and a funded diff --git a/app.py b/app.py index 5f4b7de..323e83d 100644 --- a/app.py +++ b/app.py @@ -107,6 +107,7 @@ def player_view(world: dict, player: dict, now: float) -> dict: view["labour_per_action"] = game.labour_per_action(player) view["levels"] = {skill: game.skill_level(player, skill) for skill in game.SKILL_IDS} view["slots"] = game.bank_slots(world, player) + view["slots_used"] = game.slots_used(world, player) view["stack"] = game.stack_size(world) view["power"] = game.combat_power(player) view["perk_costs"] = {perk: game.perk_cost(player, perk) for perk in player["perks"]} diff --git a/game.py b/game.py index 5da8349..088e4e4 100644 --- a/game.py +++ b/game.py @@ -718,12 +718,19 @@ def stack_size(world: dict) -> int: return BASE_STACK + 250 * building_level(world, "storehouse") +def slots_used(world: dict, player: dict) -> int: + """Bank slots taken: an item spills into another slot every full stack.""" + stack = stack_size(world) + return sum(-(-qty // stack) for qty in player["bank"].values()) + + def bank_room(world: dict, player: dict, item: str) -> int: - """How many more of `item` fit in the bank.""" + """How many more of `item` fit: the rest of its last stack, plus any free slots.""" + stack = stack_size(world) have = player["bank"].get(item, 0) - if have == 0 and len(player["bank"]) >= bank_slots(world, player): - return 0 - return max(0, stack_size(world) - have) + partial = -have % stack # room left in the item's last, part-filled stack + free = max(0, bank_slots(world, player) - slots_used(world, player)) + return partial + free * stack def city_uses(item: str) -> list[str]: diff --git a/static/app.js b/static/app.js index 19c832a..deb3ea8 100644 --- a/static/app.js +++ b/static/app.js @@ -239,7 +239,7 @@ function sidebar() { ${nav("roster", "👥", "Guild roster", `${S.players.length}`)} ${nav("character", cls.icon, "Character", `${skillOf("combat").icon} ${me.levels.combat}`)} - ${nav("bank", "🏦", "Bank", `${Object.keys(me.bank).length}/${me.slots}`)}`; + ${nav("bank", "🏦", "Bank", `${me.slots_used}/${me.slots}`)}`; let group = null; for (const skill of C.skills) { if (skill.class && skill.class !== me.cls) continue; @@ -518,16 +518,22 @@ function bank() { const me = S.me; const entries = Object.entries(me.bank).sort((x, y) => Object.keys(C.items).indexOf(x[0]) - Object.keys(C.items).indexOf(y[0])); if (selected && !me.bank[selected]) selected = null; - let slots = entries.map(([id, q]) => ` - `).join(""); - for (let i = entries.length; i < me.slots; i++) slots += `
`; + for (let i = stacks.length; i < me.slots; i++) slots += `
`; return `
-

Bank · ${entries.length} / ${me.slots} slots · up to ${fmt(me.stack)} each

+

Bank · ${me.slots_used} / ${me.slots} slots · ${fmt(me.stack)} per stack

${slots}

When an item doesn't fit: @@ -912,7 +918,7 @@ function characterSheet(uid) { ${mine ? `

Prayer points
${fmt(p.pp)}
${p.prayers.length ? p.prayers.map((id) => esc(C.prayers.find((x) => x.id === id).name)).join(", ") : "No prayers active"}
` : ""}
Armour
${p.armour}
${Math.round(p.reduction * 100)}% less damage
Given to the city
${fmt(p.donated)}
${fmt(p.favour)} favour to spend
- ${mine ? `
Purse
${fmt(p.gold)}
${Object.keys(p.bank).length} / ${p.slots} bank slots
` : ""} + ${mine ? `
Purse
${fmt(p.gold)}
${p.slots_used} / ${p.slots} bank slots
` : ""}
diff --git a/tests/test_game.py b/tests/test_game.py index ef31d47..7a12c4a 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -100,10 +100,10 @@ class ActionTest(unittest.TestCase): def test_full_bank_overflows_to_city(self): world = world_with(("fred", "warrior")) p = world["players"]["fred"] - p["bank"] = {"stone": game.BASE_STACK - 2} + p["bank"] = {"stone": game.BASE_STACK * game.BASE_SLOTS - 2} game.start_action(world, p, "mine_stone", T0) summary = game.process_player(world, p, T0 + 100, random.Random(1)) - self.assertEqual(p["bank"]["stone"], game.BASE_STACK) + self.assertEqual(p["bank"]["stone"], game.BASE_STACK * game.BASE_SLOTS) self.assertEqual(summary["overflow"], {"stone": 3}) self.assertEqual(world["city"]["stockpile"]["stone"], 3) self.assertEqual(p["favour"], 1.5) # half favour for overflow @@ -112,7 +112,7 @@ class ActionTest(unittest.TestCase): def test_full_bank_sells_what_the_city_cannot_use(self): world = world_with(("fred", "warrior")) p = world["players"]["fred"] - p["bank"] = {"copper_ore": game.BASE_STACK - 1} + p["bank"] = {"copper_ore": game.BASE_STACK * game.BASE_SLOTS - 1} game.start_action(world, p, "mine_copper", T0) summary = game.process_player(world, p, T0 + 100, random.Random(1)) self.assertEqual(summary["sold"], {"copper_ore": 3}) @@ -131,6 +131,17 @@ class ActionTest(unittest.TestCase): self.assertIn("Walls", game.city_uses("bricks")) self.assertEqual(game.city_uses("bread"), ["the granary"]) + def test_a_full_stack_spills_into_another_slot(self): + world = world_with(("fred", "warrior")) + p = world["players"]["fred"] + p["bank"] = {"coal": game.BASE_STACK, "logs": 10} + self.assertEqual(game.slots_used(world, p), 2) + self.assertEqual(game.bank_room(world, p, "coal"), (game.BASE_SLOTS - 2) * game.BASE_STACK) + self.assertEqual(game.bank_room(world, p, "logs"), game.BASE_STACK - 10 + (game.BASE_SLOTS - 2) * game.BASE_STACK) + p["bank"]["coal"] = 2500 + self.assertEqual(game.slots_used(world, p), 4) # 1000 + 1000 + 500, plus the logs + self.assertEqual(game.bank_room(world, p, "coal"), 500 + (game.BASE_SLOTS - 4) * game.BASE_STACK) + def test_full_bank_can_stop_instead(self): world = world_with(("fred", "warrior")) p = world["players"]["fred"]