Bank: a full stack spills into another slot

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-19 10:52:07 +01:00
parent fb37bcef98
commit 71d7ca704b
5 changed files with 41 additions and 15 deletions

View file

@ -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

1
app.py
View file

@ -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"]}

15
game.py
View file

@ -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]:

View file

@ -239,7 +239,7 @@ function sidebar() {
${nav("roster", "👥", "Guild roster", `${S.players.length}`)}
<div class="navhead">You</div>
${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]) => `
<button class="slot ${selected === id ? "on" : ""}" data-select="${id}" title="${esc(item(id).name)}">
// An item past one full stack spills into more slots: 2,500 coal shows as 1,000 + 1,000 + 500.
const stacks = entries.flatMap(([id, q]) => {
const parts = [];
for (let left = q; left > 0; left -= me.stack) parts.push([id, Math.min(left, me.stack)]);
return parts;
});
let slots = stacks.map(([id, q]) => `
<button class="slot ${selected === id ? "on" : ""}" data-select="${id}" title="${esc(item(id).name)}: ${fmt(me.bank[id])} in all">
<span class="qty ${q >= me.stack ? "full" : ""}">${fmt(q)}</span>${item(id).icon}<span class="nm">${esc(item(id).name)}</span>
</button>`).join("");
for (let i = entries.length; i < me.slots; i++) slots += `<div class="slot empty"></div>`;
for (let i = stacks.length; i < me.slots; i++) slots += `<div class="slot empty"></div>`;
return `
<div class="bank-wrap">
<div>
<div class="panel">
<h2>Bank · ${entries.length} / ${me.slots} slots · up to ${fmt(me.stack)} each</h2>
<h2>Bank · ${me.slots_used} / ${me.slots} slots · ${fmt(me.stack)} per stack</h2>
<div class="slots">${slots}</div>
<p class="small muted">When an item doesn't fit:
<button class="btn sm ${me.overflow === "city" ? "gold" : ""}" data-overflow="city">Send it to the city</button>
@ -912,7 +918,7 @@ function characterSheet(uid) {
${mine ? `<div class="stat"><div class="k">Prayer points</div><div class="v">${fmt(p.pp)}</div><div class="note">${p.prayers.length ? p.prayers.map((id) => esc(C.prayers.find((x) => x.id === id).name)).join(", ") : "No prayers active"}</div></div>` : ""}
<div class="stat"><div class="k">Armour</div><div class="v">${p.armour}</div><div class="note">${Math.round(p.reduction * 100)}% less damage</div></div>
<div class="stat"><div class="k">Given to the city</div><div class="v favour">${fmt(p.donated)}</div><div class="note">${fmt(p.favour)} favour to spend</div></div>
${mine ? `<div class="stat"><div class="k">Purse</div><div class="v gold">${fmt(p.gold)}</div><div class="note">${Object.keys(p.bank).length} / ${p.slots} bank slots</div></div>` : ""}
${mine ? `<div class="stat"><div class="k">Purse</div><div class="v gold">${fmt(p.gold)}</div><div class="note">${p.slots_used} / ${p.slots} bank slots</div></div>` : ""}
</div>
<div class="cols">
<div>

View file

@ -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"]