Bank: a full stack spills into another slot
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fb37bcef98
commit
71d7ca704b
5 changed files with 41 additions and 15 deletions
|
|
@ -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.
|
city). Gear needs a combat level to wear, tools a skill level to use.
|
||||||
- **Classes:** warrior, scout, artisan, pioneer. Every class can train every
|
- **Classes:** warrior, scout, artisan, pioneer. Every class can train every
|
||||||
skill; a class makes some skills 20% faster and has one signature ability.
|
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
|
- **Bank:** 12 slots of 1,000 each to start; more than a stack of one item
|
||||||
fit it goes to the city stockpile (or the action stops, if you prefer).
|
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.
|
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:** each building collects its own fund, from players' banks or the
|
||||||
city's stockpile and treasury. Funds are safe from raiders, and a funded
|
city's stockpile and treasury. Funds are safe from raiders, and a funded
|
||||||
|
|
|
||||||
1
app.py
1
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["labour_per_action"] = game.labour_per_action(player)
|
||||||
view["levels"] = {skill: game.skill_level(player, skill) for skill in game.SKILL_IDS}
|
view["levels"] = {skill: game.skill_level(player, skill) for skill in game.SKILL_IDS}
|
||||||
view["slots"] = game.bank_slots(world, player)
|
view["slots"] = game.bank_slots(world, player)
|
||||||
|
view["slots_used"] = game.slots_used(world, player)
|
||||||
view["stack"] = game.stack_size(world)
|
view["stack"] = game.stack_size(world)
|
||||||
view["power"] = game.combat_power(player)
|
view["power"] = game.combat_power(player)
|
||||||
view["perk_costs"] = {perk: game.perk_cost(player, perk) for perk in player["perks"]}
|
view["perk_costs"] = {perk: game.perk_cost(player, perk) for perk in player["perks"]}
|
||||||
|
|
|
||||||
15
game.py
15
game.py
|
|
@ -718,12 +718,19 @@ def stack_size(world: dict) -> int:
|
||||||
return BASE_STACK + 250 * building_level(world, "storehouse")
|
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:
|
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)
|
have = player["bank"].get(item, 0)
|
||||||
if have == 0 and len(player["bank"]) >= bank_slots(world, player):
|
partial = -have % stack # room left in the item's last, part-filled stack
|
||||||
return 0
|
free = max(0, bank_slots(world, player) - slots_used(world, player))
|
||||||
return max(0, stack_size(world) - have)
|
return partial + free * stack
|
||||||
|
|
||||||
|
|
||||||
def city_uses(item: str) -> list[str]:
|
def city_uses(item: str) -> list[str]:
|
||||||
|
|
|
||||||
|
|
@ -239,7 +239,7 @@ function sidebar() {
|
||||||
${nav("roster", "👥", "Guild roster", `${S.players.length}`)}
|
${nav("roster", "👥", "Guild roster", `${S.players.length}`)}
|
||||||
<div class="navhead">You</div>
|
<div class="navhead">You</div>
|
||||||
${nav("character", cls.icon, "Character", `${skillOf("combat").icon} ${me.levels.combat}`)}
|
${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;
|
let group = null;
|
||||||
for (const skill of C.skills) {
|
for (const skill of C.skills) {
|
||||||
if (skill.class && skill.class !== me.cls) continue;
|
if (skill.class && skill.class !== me.cls) continue;
|
||||||
|
|
@ -518,16 +518,22 @@ function bank() {
|
||||||
const me = S.me;
|
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]));
|
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;
|
if (selected && !me.bank[selected]) selected = null;
|
||||||
let slots = entries.map(([id, q]) => `
|
// An item past one full stack spills into more slots: 2,500 coal shows as 1,000 + 1,000 + 500.
|
||||||
<button class="slot ${selected === id ? "on" : ""}" data-select="${id}" title="${esc(item(id).name)}">
|
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>
|
<span class="qty ${q >= me.stack ? "full" : ""}">${fmt(q)}</span>${item(id).icon}<span class="nm">${esc(item(id).name)}</span>
|
||||||
</button>`).join("");
|
</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 `
|
return `
|
||||||
<div class="bank-wrap">
|
<div class="bank-wrap">
|
||||||
<div>
|
<div>
|
||||||
<div class="panel">
|
<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>
|
<div class="slots">${slots}</div>
|
||||||
<p class="small muted">When an item doesn't fit:
|
<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>
|
<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>` : ""}
|
${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">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>
|
<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>
|
||||||
<div class="cols">
|
<div class="cols">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -100,10 +100,10 @@ class ActionTest(unittest.TestCase):
|
||||||
def test_full_bank_overflows_to_city(self):
|
def test_full_bank_overflows_to_city(self):
|
||||||
world = world_with(("fred", "warrior"))
|
world = world_with(("fred", "warrior"))
|
||||||
p = world["players"]["fred"]
|
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)
|
game.start_action(world, p, "mine_stone", T0)
|
||||||
summary = game.process_player(world, p, T0 + 100, random.Random(1))
|
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(summary["overflow"], {"stone": 3})
|
||||||
self.assertEqual(world["city"]["stockpile"]["stone"], 3)
|
self.assertEqual(world["city"]["stockpile"]["stone"], 3)
|
||||||
self.assertEqual(p["favour"], 1.5) # half favour for overflow
|
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):
|
def test_full_bank_sells_what_the_city_cannot_use(self):
|
||||||
world = world_with(("fred", "warrior"))
|
world = world_with(("fred", "warrior"))
|
||||||
p = world["players"]["fred"]
|
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)
|
game.start_action(world, p, "mine_copper", T0)
|
||||||
summary = game.process_player(world, p, T0 + 100, random.Random(1))
|
summary = game.process_player(world, p, T0 + 100, random.Random(1))
|
||||||
self.assertEqual(summary["sold"], {"copper_ore": 3})
|
self.assertEqual(summary["sold"], {"copper_ore": 3})
|
||||||
|
|
@ -131,6 +131,17 @@ class ActionTest(unittest.TestCase):
|
||||||
self.assertIn("Walls", game.city_uses("bricks"))
|
self.assertIn("Walls", game.city_uses("bricks"))
|
||||||
self.assertEqual(game.city_uses("bread"), ["the granary"])
|
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):
|
def test_full_bank_can_stop_instead(self):
|
||||||
world = world_with(("fred", "warrior"))
|
world = world_with(("fred", "warrior"))
|
||||||
p = world["players"]["fred"]
|
p = world["players"]["fred"]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue