Fires: everyone keeps their own pile

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-21 13:14:03 +01:00
parent a02a81dffd
commit b4ecacf96c
4 changed files with 91 additions and 54 deletions

56
game.py
View file

@ -2053,9 +2053,11 @@ def _use_charge(player: dict, skill: str, summary: dict) -> None:
# --- fires -----------------------------------------------------------------
# Each fire is a list of fuel: {tier, left (game seconds), by (who fed it)}. It burns on
# game time, hottest fuel first and the oldest of that first, so the flame's colour is
# the best fuel still in it.
# Each fire is a list of fuel: {tier, left (game seconds), by (who fed it)}. Everyone who
# feeds a fire keeps their own pile in it, up to their own bank, so a full fire never
# shuts a beginner out. The fire burns a game minute a minute, shared evenly between the
# piles, so every log adds to how long it lasts; each pile burns its hottest fuel first,
# and the fire is as hot as its hottest pile.
def new_fire(now: float) -> dict:
return {"fuel": [], "at": now, "saved": 0}
@ -2069,39 +2071,44 @@ def burn_fires(world: dict, now: float) -> None:
seconds = (now - fire["at"]) * speed(world)
fire["at"] = now
fuel = fire["fuel"]
while seconds > 0 and fuel:
chunk = burning(fire)
used = min(seconds, chunk["left"])
chunk["left"] -= used
seconds -= used
if chunk["left"] <= 1e-6:
fuel.remove(chunk)
while seconds > 1e-9 and fuel:
# Each pile burns its best fuel at 1/n of the pace, until the first of those runs out.
heads = [burning(fuel, by) for by in {chunk["by"] for chunk in fuel}]
step = min(seconds, min(chunk["left"] for chunk in heads) * len(heads))
for chunk in heads:
chunk["left"] -= step / len(heads)
if chunk["left"] <= 1e-6:
fuel.remove(chunk)
seconds -= step
def burning(fire: dict) -> dict | None:
"""The fuel burning right now: the hottest, and the oldest of those."""
return max(fire["fuel"], key=lambda chunk: chunk["tier"], default=None)
def burning(fuel: list, by: str | None = None) -> dict | None:
"""The fuel burning hottest (in one pile, with `by`): the hottest, and the oldest of those."""
return max((chunk for chunk in fuel if by is None or chunk["by"] == by), key=lambda chunk: chunk["tier"], default=None)
def heat(world: dict, fire: str) -> int:
"""The tier of a fire's flame, or -1 when it's out."""
chunk = burning(world["city"]["fires"][fire])
chunk = burning(world["city"]["fires"][fire]["fuel"])
return chunk["tier"] if chunk else -1
def _room(fire: dict, tier: int, level: int) -> float:
"""Game seconds of cheaper fuel a log of `tier` would push out, or -1 if it won't fit."""
short = sum(chunk["left"] for chunk in fire["fuel"]) + fire_minutes(level) * 60 - fire_bank(level) * 60
cheaper = sum(chunk["left"] for chunk in fire["fuel"] if chunk["tier"] < tier)
def _room(fire: dict, player: dict, tier: int) -> float:
"""Game seconds of the player's own cheaper fuel a log of `tier` would push out, or -1 if it won't fit."""
level = skill_level(player, "firekeeping")
mine = [chunk for chunk in fire["fuel"] if chunk["by"] == player["id"]]
short = sum(chunk["left"] for chunk in mine) + fire_minutes(level) * 60 - fire_bank(level) * 60
cheaper = sum(chunk["left"] for chunk in mine if chunk["tier"] < tier)
return -1 if short > cheaper + 1e-6 else max(0.0, short)
def feed_fire(world: dict, player: dict, fire_id: str, tier: int) -> float:
"""Put one log in a fire, pushing out the cheapest fuel if it's full. Returns the minutes added."""
"""Put one log on your pile in a fire, pushing out your cheapest fuel if it's full. Returns the minutes added."""
fire, level = world["city"]["fires"][fire_id], skill_level(player, "firekeeping")
short = _room(fire, tier, level)
short = _room(fire, player, tier)
while short > 1e-6:
cheapest = min((chunk for chunk in fire["fuel"] if chunk["tier"] < tier), key=lambda chunk: chunk["tier"])
cheapest = min((chunk for chunk in fire["fuel"] if chunk["by"] == player["id"] and chunk["tier"] < tier),
key=lambda chunk: chunk["tier"])
out = min(short, cheapest["left"])
cheapest["left"] -= out
short -= out
@ -2135,7 +2142,7 @@ def _warm(world: dict, crafter: dict, action: dict, xp: float, now: float) -> No
"""A fire paid for a craft: count it, and give its feeder a share of the XP."""
fire = world["city"]["fires"][action["fire"]]
fire["saved"] = fire.get("saved", 0) + 1
keeper = world["players"].get(burning(fire)["by"])
keeper = world["players"].get(burning(fire["fuel"])["by"])
if keeper and keeper is not crafter:
gain_xp(world, keeper, "firekeeping", xp * WARMTH, now, keeper.setdefault("away", empty_summary()))
@ -2184,9 +2191,8 @@ def can_start(world: dict, player: dict, action: dict) -> str | None:
return f"Out of {ITEMS[item]['name'].lower()}"
elif player["bank"].get(item, 0) < qty:
return f"Out of {ITEMS[item]['name'].lower()}"
if action.get("special") == "tend" and _room(world["city"]["fires"][action["tend"]], action["tier"],
skill_level(player, "firekeeping")) < 0:
return f"The {FIRES[action['tend']]['name'].lower()} is full"
if action.get("special") == "tend" and _room(world["city"]["fires"][action["tend"]], player, action["tier"]) < 0:
return f"Your pile in the {FIRES[action['tend']]['name'].lower()} is full"
if action.get("special") == "sabotage":
raid = upcoming_raid(world)
if not raid or intel_level(world, raid) < 3: