Only donate what the city uses; sell useless overflow
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e3a5f519fc
commit
b82f979b4c
3 changed files with 63 additions and 7 deletions
32
game.py
32
game.py
|
|
@ -539,13 +539,31 @@ def bank_room(world: dict, player: dict, item: str) -> int:
|
||||||
return max(0, stack_size(world) - have)
|
return max(0, stack_size(world) - have)
|
||||||
|
|
||||||
|
|
||||||
def bank_add(world: dict, player: dict, item: str, qty: int, now: float, overflow: dict) -> int:
|
def city_uses(item: str) -> list[str]:
|
||||||
"""Store what fits; the rest goes to the city (or is lost). Returns qty kept."""
|
"""What the city does with an item: the buildings that need it, the granary, the armoury."""
|
||||||
|
uses = [spec["name"] for spec in BUILDINGS.values()
|
||||||
|
if any(item in spec.get(key, {}) for key in ("base", "advanced", "late", "final"))]
|
||||||
|
spec = ITEMS[item]
|
||||||
|
if "food" in spec:
|
||||||
|
uses.append("the granary")
|
||||||
|
if spec.get("slot") in ("weapon", "shield") or item == "arrows":
|
||||||
|
uses.append("the armoury")
|
||||||
|
return uses
|
||||||
|
|
||||||
|
|
||||||
|
def bank_add(world: dict, player: dict, item: str, qty: int, now: float, summary: dict) -> int:
|
||||||
|
"""Store what fits; the rest goes to the city if it has a use, else is sold. Returns qty kept."""
|
||||||
keep = min(qty, bank_room(world, player, item))
|
keep = min(qty, bank_room(world, player, item))
|
||||||
if keep:
|
if keep:
|
||||||
player["bank"][item] = player["bank"].get(item, 0) + keep
|
player["bank"][item] = player["bank"].get(item, 0) + keep
|
||||||
spill = qty - keep
|
spill = qty - keep
|
||||||
if spill and player["overflow"] == "city":
|
if spill and player["overflow"] == "city" and not city_uses(item):
|
||||||
|
gold = int(spill * sell_price(world, item, now))
|
||||||
|
player["gold"] += gold
|
||||||
|
summary["sold"][item] = summary["sold"].get(item, 0) + spill
|
||||||
|
summary["sold_gold"] += gold
|
||||||
|
elif spill and player["overflow"] == "city":
|
||||||
|
overflow = summary["overflow"]
|
||||||
stockpile = world["city"]["stockpile"]
|
stockpile = world["city"]["stockpile"]
|
||||||
stockpile[item] = stockpile.get(item, 0) + spill
|
stockpile[item] = stockpile.get(item, 0) + spill
|
||||||
gain = spill * ITEMS[item]["value"]
|
gain = spill * ITEMS[item]["value"]
|
||||||
|
|
@ -717,7 +735,7 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
|
||||||
event["recon"] += action["recon"]
|
event["recon"] += action["recon"]
|
||||||
|
|
||||||
for item, qty in outputs.items():
|
for item, qty in outputs.items():
|
||||||
kept = bank_add(world, player, item, qty, now, summary["overflow"])
|
kept = bank_add(world, player, item, qty, now, summary)
|
||||||
if kept:
|
if kept:
|
||||||
summary["items"][item] = summary["items"].get(item, 0) + kept
|
summary["items"][item] = summary["items"].get(item, 0) + kept
|
||||||
gain_xp(world, player, action["skill"], xp, now, summary)
|
gain_xp(world, player, action["skill"], xp, now, summary)
|
||||||
|
|
@ -739,7 +757,8 @@ def gain_xp(world: dict, player: dict, skill: str, xp: float, now: float, summar
|
||||||
|
|
||||||
def empty_summary() -> dict:
|
def empty_summary() -> dict:
|
||||||
return {"done": 0, "items": {}, "overflow": {}, "xp": {}, "levels": [], "eaten": {}, "damage": 0,
|
return {"done": 0, "items": {}, "overflow": {}, "xp": {}, "levels": [], "eaten": {}, "damage": 0,
|
||||||
"gold": 0, "wins": 0, "losses": 0, "lucky": 0, "stopped": None, "resumed": None}
|
"gold": 0, "wins": 0, "losses": 0, "lucky": 0, "stopped": None, "resumed": None,
|
||||||
|
"sold": {}, "sold_gold": 0}
|
||||||
|
|
||||||
|
|
||||||
def process_player(world: dict, player: dict, now: float, rng: random.Random | None = None) -> dict:
|
def process_player(world: dict, player: dict, now: float, rng: random.Random | None = None) -> dict:
|
||||||
|
|
@ -828,6 +847,8 @@ def donate(world: dict, player: dict, item: str, qty: int, now: float) -> str |
|
||||||
gain = qty
|
gain = qty
|
||||||
label = f"{qty:,} gold"
|
label = f"{qty:,} gold"
|
||||||
else:
|
else:
|
||||||
|
if not city_uses(item):
|
||||||
|
return "The city has no use for that"
|
||||||
if player["bank"].get(item, 0) < qty:
|
if player["bank"].get(item, 0) < qty:
|
||||||
return "You don't have that many"
|
return "You don't have that many"
|
||||||
bank_take(player, item, qty)
|
bank_take(player, item, qty)
|
||||||
|
|
@ -1411,5 +1432,6 @@ def content() -> dict:
|
||||||
return {
|
return {
|
||||||
"items": ITEMS, "skills": SKILLS, "classes": CLASSES, "actions": ACTIONS,
|
"items": ITEMS, "skills": SKILLS, "classes": CLASSES, "actions": ACTIONS,
|
||||||
"buildings": buildings, "xp_table": XP_TABLE, "offline_cap": OFFLINE_CAP, "tiers": TIERS,
|
"buildings": buildings, "xp_table": XP_TABLE, "offline_cap": OFFLINE_CAP, "tiers": TIERS,
|
||||||
|
"city_uses": {item: city_uses(item) for item in ITEMS},
|
||||||
"equip_slots": EQUIP_SLOTS, "eat_below": EAT_BELOW, "regen_per_hour": REGEN_PER_HOUR,
|
"equip_slots": EQUIP_SLOTS, "eat_below": EAT_BELOW, "regen_per_hour": REGEN_PER_HOUR,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,8 @@ function showSummary(sum) {
|
||||||
if (sum.lucky) toast(sum.lucky === 1 ? "✨ Double yield!" : `✨ ${sum.lucky} double yields!`, "level");
|
if (sum.lucky) toast(sum.lucky === 1 ? "✨ Double yield!" : `✨ ${sum.lucky} double yields!`, "level");
|
||||||
const spilled = Object.entries(sum.overflow);
|
const spilled = Object.entries(sum.overflow);
|
||||||
if (spilled.length) toast(`📦 Bank full: ${spilled.map(([id, q]) => `${fmt(q)} ${item(id).name.toLowerCase()}`).join(", ")} went to the city`);
|
if (spilled.length) toast(`📦 Bank full: ${spilled.map(([id, q]) => `${fmt(q)} ${item(id).name.toLowerCase()}`).join(", ")} went to the city`);
|
||||||
|
const sold = Object.entries(sum.sold || {});
|
||||||
|
if (sold.length) toast(`📦 Bank full: sold ${sold.map(([id, q]) => `${fmt(q)} ${item(id).name.toLowerCase()}`).join(", ")} for ${fmt(sum.sold_gold)} gold`);
|
||||||
for (const [skill, level] of sum.levels) toast(`🎉 ${skillOf(skill).name} level ${level}!`, "level");
|
for (const [skill, level] of sum.levels) toast(`🎉 ${skillOf(skill).name} level ${level}!`, "level");
|
||||||
if (sum.stopped) toast(`Stopped: ${sum.stopped}`, "bad");
|
if (sum.stopped) toast(`Stopped: ${sum.stopped}`, "bad");
|
||||||
if (sum.resumed) toast(`Back to ${C.actions.find((x) => x.id === sum.resumed).name.toLowerCase()}`);
|
if (sum.resumed) toast(`Back to ${C.actions.find((x) => x.id === sum.resumed).name.toLowerCase()}`);
|
||||||
|
|
@ -143,6 +145,7 @@ function awayModal(sum) {
|
||||||
const xp = Object.entries(sum.xp).map(([id, q]) => `<li>${skillOf(id).icon} ${fmt(q)} ${esc(skillOf(id).name)} XP</li>`).join("");
|
const xp = Object.entries(sum.xp).map(([id, q]) => `<li>${skillOf(id).icon} ${fmt(q)} ${esc(skillOf(id).name)} XP</li>`).join("");
|
||||||
const levels = sum.levels.map(([id, l]) => `<li>🎉 ${esc(skillOf(id).name)} reached level ${l}</li>`).join("");
|
const levels = sum.levels.map(([id, l]) => `<li>🎉 ${esc(skillOf(id).name)} reached level ${l}</li>`).join("");
|
||||||
const spilled = Object.entries(sum.overflow).map(([id, q]) => `<li>${item(id).icon} ${fmt(q)} ${esc(item(id).name)}</li>`).join("");
|
const spilled = Object.entries(sum.overflow).map(([id, q]) => `<li>${item(id).icon} ${fmt(q)} ${esc(item(id).name)}</li>`).join("");
|
||||||
|
const sold = Object.entries(sum.sold || {}).map(([id, q]) => `<li>${item(id).icon} ${fmt(q)} ${esc(item(id).name)}</li>`).join("");
|
||||||
openModal(`
|
openModal(`
|
||||||
<h2>While you were away</h2>
|
<h2>While you were away</h2>
|
||||||
<p class="muted">You completed ${fmt(sum.done)} actions${sum.wins || sum.losses ? ` (${sum.wins} fights won, ${sum.losses} lost)` : ""}.</p>
|
<p class="muted">You completed ${fmt(sum.done)} actions${sum.wins || sum.losses ? ` (${sum.wins} fights won, ${sum.losses} lost)` : ""}.</p>
|
||||||
|
|
@ -151,6 +154,7 @@ function awayModal(sum) {
|
||||||
${sum.gold ? `<p class="gold">+${fmt(sum.gold)} gold</p>` : ""}
|
${sum.gold ? `<p class="gold">+${fmt(sum.gold)} gold</p>` : ""}
|
||||||
${xp ? `<ul>${xp}</ul>` : ""}
|
${xp ? `<ul>${xp}</ul>` : ""}
|
||||||
${levels ? `<ul>${levels}</ul>` : ""}
|
${levels ? `<ul>${levels}</ul>` : ""}
|
||||||
|
${sold ? `<p class="muted small">Your bank was full, so this was sold for ${fmt(sum.sold_gold)} gold:</p><ul>${sold}</ul>` : ""}
|
||||||
${spilled ? `<p class="muted small">Your bank was full, so this went to the city stockpile (for half the favour of giving it yourself):</p><ul>${spilled}</ul>` : ""}
|
${spilled ? `<p class="muted small">Your bank was full, so this went to the city stockpile (for half the favour of giving it yourself):</p><ul>${spilled}</ul>` : ""}
|
||||||
${sum.stopped ? `<p class="tag bad">Stopped: ${esc(sum.stopped)}</p>` : ""}
|
${sum.stopped ? `<p class="tag bad">Stopped: ${esc(sum.stopped)}</p>` : ""}
|
||||||
<p><button class="btn primary" data-close>Back to work</button></p>`);
|
<p><button class="btn primary" data-close>Back to work</button></p>`);
|
||||||
|
|
@ -453,7 +457,8 @@ function bank() {
|
||||||
<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>
|
||||||
<button class="btn sm ${me.overflow === "stop" ? "gold" : ""}" data-overflow="stop">Stop working</button>
|
<button class="btn sm ${me.overflow === "stop" ? "gold" : ""}" data-overflow="stop">Stop working</button>
|
||||||
<br>Raids never touch your bank. Overflow sent to the city earns half the favour of giving it yourself.</p>
|
<br>Raids never touch your bank. Overflow sent to the city earns half the favour of giving it yourself;
|
||||||
|
anything the city has no use for is sold instead.</p>
|
||||||
</div>
|
</div>
|
||||||
${favourPanel()}
|
${favourPanel()}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -476,13 +481,20 @@ function itemPanel(id) {
|
||||||
${it.heal ? `<div class="row"><div class="small muted">Heals ${it.heal} in fights.</div>
|
${it.heal ? `<div class="row"><div class="small muted">Heals ${it.heal} in fights.</div>
|
||||||
${S.me.food === id ? `<span class="tag good">Your fighting food</span>` : `<button class="btn sm" data-food="${id}">Eat this first in fights</button>`}</div>` : ""}
|
${S.me.food === id ? `<span class="tag good">Your fighting food</span>` : `<button class="btn sm" data-food="${id}">Eat this first in fights</button>`}</div>` : ""}
|
||||||
${it.food ? `<div class="row small muted">Feeds ${it.food} townsfolk for two days in the city granary.</div>` : ""}
|
${it.food ? `<div class="row small muted">Feeds ${it.food} townsfolk for two days in the city granary.</div>` : ""}
|
||||||
<div class="row"><div class="k">Donate to the city · +${it.value} favour each</div><div class="btns">${qtyBtns("data-donate")}</div></div>
|
${C.city_uses[id].length
|
||||||
|
? `<div class="row"><div class="k">Donate to the city · +${it.value} favour each</div><div class="btns">${qtyBtns("data-donate")}</div>
|
||||||
|
<div class="small muted">Used for ${esc(listing(C.city_uses[id]))}.</div></div>`
|
||||||
|
: `<div class="row small muted">The city has no use for this. Sell it, or give it to someone who can use it.</div>`}
|
||||||
${others.length ? `<div class="row"><div class="k">Give to <select data-keep="give-to">${others.map((p) => `<option value="${esc(p.id)}">${esc(p.name)}</option>`).join("")}</select></div><div class="btns">${qtyBtns("data-give")}</div></div>` : ""}
|
${others.length ? `<div class="row"><div class="k">Give to <select data-keep="give-to">${others.map((p) => `<option value="${esc(p.id)}">${esc(p.name)}</option>`).join("")}</select></div><div class="btns">${qtyBtns("data-give")}</div></div>` : ""}
|
||||||
<div class="row"><div class="k">Sell</div><div class="btns">${qtyBtns("data-sell")}</div></div>
|
<div class="row"><div class="k">Sell</div><div class="btns">${qtyBtns("data-sell")}</div></div>
|
||||||
<div class="row"><button class="btn sm" data-select="">Close</button></div>
|
<div class="row"><button class="btn sm" data-select="">Close</button></div>
|
||||||
</div>${goldPanel()}`;
|
</div>${goldPanel()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function listing(words) {
|
||||||
|
return words.length < 2 ? words.join("") : `${words.slice(0, -1).join(", ")} and ${words.at(-1)}`;
|
||||||
|
}
|
||||||
|
|
||||||
function equipRow(id, it) {
|
function equipRow(id, it) {
|
||||||
const [skill, level] = it.req || [];
|
const [skill, level] = it.req || [];
|
||||||
const short = skill && S.me.levels[skill] < level;
|
const short = skill && S.me.levels[skill] < level;
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,28 @@ class ActionTest(unittest.TestCase):
|
||||||
self.assertEqual(p["favour"], 1.5) # half favour for overflow
|
self.assertEqual(p["favour"], 1.5) # half favour for overflow
|
||||||
self.assertEqual(p["donated"], 3)
|
self.assertEqual(p["donated"], 3)
|
||||||
|
|
||||||
|
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}
|
||||||
|
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})
|
||||||
|
self.assertEqual(p["gold"], 3 * game.ITEMS["copper_ore"]["value"])
|
||||||
|
self.assertNotIn("copper_ore", world["city"]["stockpile"])
|
||||||
|
self.assertEqual(p["favour"], 0)
|
||||||
|
|
||||||
|
def test_only_useful_things_can_be_donated(self):
|
||||||
|
world = world_with(("fred", "warrior"))
|
||||||
|
p = world["players"]["fred"]
|
||||||
|
p["bank"] = {"copper_ore": 10, "bricks": 10, "bread": 5, "iron_helmet": 1, "steel_sword": 1}
|
||||||
|
self.assertEqual(game.donate(world, p, "copper_ore", 10, T0), "The city has no use for that")
|
||||||
|
self.assertEqual(game.donate(world, p, "iron_helmet", 1, T0), "The city has no use for that")
|
||||||
|
for item, qty in (("bricks", 10), ("bread", 5), ("steel_sword", 1)):
|
||||||
|
self.assertIsNone(game.donate(world, p, item, qty, T0))
|
||||||
|
self.assertIn("Walls", game.city_uses("bricks"))
|
||||||
|
self.assertEqual(game.city_uses("bread"), ["the granary"])
|
||||||
|
|
||||||
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