Work orders: anyone can pool reagents, paid by value

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-21 13:40:54 +01:00
parent f7daa4e44d
commit 9ff7a22a5d
3 changed files with 161 additions and 44 deletions

116
game.py
View file

@ -2189,10 +2189,8 @@ def can_start(world: dict, player: dict, action: dict) -> str | None:
if not order or order["status"] != "open" or order["claimed"] != player["id"]:
return "Work order done" if order and order["status"] == "done" else "Work order closed"
for item, qty in fire_inputs(world, action).items():
if order and item in order["provided"]:
if order["provided"][item] < qty:
return f"Out of {ITEMS[item]['name'].lower()}"
elif player["bank"].get(item, 0) < qty:
pooled = order["provided"].get(item, 0) if order else 0
if pooled + 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"]], player, action["tier"]) < 0:
return f"Your pile in the {FIRES[action['tend']]['name'].lower()} is full"
@ -2227,11 +2225,13 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
order = working_order(world, player, action)
inputs = fire_inputs(world, action)
warmed = inputs is not action["inputs"]
own = {item: qty for item, qty in inputs.items() if not order or item not in order["provided"]}
own, used = dict(inputs), {}
if order:
for item, qty in inputs.items():
if item in order["provided"]:
order["provided"][item] -= qty
used = _draw(order, inputs)
for share in used.values():
for item, qty in share.items():
own[item] -= qty
own = {item: qty for item, qty in own.items() if qty}
if own and rng.random() < brew.get("keep", 0):
summary["kept"] += 1
else:
@ -2362,7 +2362,7 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
sent = min(outputs.get(order["item"], 0), order["qty"] - order["delivered"])
if sent:
outputs[order["item"]] -= sent
_deliver(world, order, player, sent, now)
_deliver(world, order, player, sent, now, used, inputs)
summary["delivered"] = summary.get("delivered", 0) + sent
for item, qty in outputs.items():
if not qty:
@ -2513,16 +2513,18 @@ def take_summary(player: dict, summary: dict) -> dict:
# --- work orders -----------------------------------------------------------
# A player asks the guild for something. A crafting order names a recipe: the
# requester can put in some of its reagents, one crafter claims it and makes it
# with the normal action (their own materials for the rest), and each piece goes
# straight to the requester. A materials request is open to anyone who has the
# items. Reagents and the reward are held by the order, paid out as it's filled,
# and handed back if it's cancelled or runs out of time.
# A player asks the guild for something. A crafting order names a recipe: anyone
# can put its reagents into the order's pool, one crafter claims it and makes it
# with the normal action (pool first, their own materials for the rest), and each
# piece goes straight to the requester. A materials request is open to anyone who
# has the items. Reagents and the reward are held by the order, paid out as it's
# filled, and handed back to whoever put them in if it's cancelled, runs out of
# time, or finishes with some left over.
ORDER_LIMIT = 5 # open orders per player
ORDER_DAYS = 3 # before an order expires and is refunded
ORDER_MAX = {"craft": 500, "gather": 2000}
INPUT_SHARE = 0.5 # of each piece's reward, split by value among whoever's reagents went in
def _order_sources() -> tuple[dict, dict]:
@ -2581,37 +2583,70 @@ def post_order(world: dict, player: dict, kind: str, item: str, qty, provide, re
order = {"id": world["next_order_id"], "kind": kind, "by": player["id"], "at": now,
"expires": now + ORDER_DAYS * DAY / speed(world), "item": item, "qty": qty, "action": source,
"skill": action["skill"], "level": action["level"], "provided": provided, "reward": total,
"paid": 0, "delivered": 0, "held": 0, "claimed": None, "from": {}, "status": "open"}
"paid": 0, "delivered": 0, "held": 0, "claimed": None, "from": {}, "status": "open",
"gave": {player["id"]: dict(provided)} if provided else {}, "earned": {}}
world["next_order_id"] += 1
world["orders"].append(order)
return None
def _gave(order: dict) -> dict:
"""Who has reagents in the pool: {player: {item: qty}}, oldest first. Older orders only had the requester's."""
return order.setdefault("gave", {order["by"]: dict(order["provided"])} if order["provided"] else {})
def provide_reagent(world: dict, player: dict, order_id, item: str) -> str | None:
"""Put a reagent into your own order after posting it, enough for what's still to be made."""
"""Put a reagent into a crafting order's pool, up to what's still to be made."""
order = order_by_id(world, order_id)
if not order or order["by"] != player["id"] or order["status"] != "open" or order["kind"] != "craft":
if not order or order["status"] != "open" or order["kind"] != "craft":
return "No such order"
action = ACTION_BY_ID[order["action"]]
if item not in action["inputs"] or item in order["provided"]:
return "Already provided" if item in order["provided"] else "That isn't in the recipe"
if item not in action["inputs"]:
return "That isn't in the recipe"
need = action["inputs"][item] * _batches(action, order["item"], order["qty"] - order["delivered"])
if player["bank"].get(item, 0) < need:
return f"{ITEMS[item]['name']}: {player['bank'].get(item, 0)} / {need}"
bank_take(player, item, need)
order["provided"][item] = need
short = need - order["provided"].get(item, 0)
if short <= 0:
return "Already provided"
qty = min(player["bank"].get(item, 0), short)
if not qty:
return f"No {ITEMS[item]['name'].lower()} in your bank"
bank_take(player, item, qty)
order["provided"][item] = order["provided"].get(item, 0) + qty
mine = _gave(order).setdefault(player["id"], {})
mine[item] = mine.get(item, 0) + qty
return None
def _draw(order: dict, inputs: dict) -> dict:
"""Take one run's reagents from the pool, oldest first: {player: {item: qty}} of whose went in."""
used = {}
for item, qty in inputs.items():
take = min(order["provided"].get(item, 0), qty)
if not take:
continue
order["provided"][item] -= take
for uid, items in _gave(order).items():
part = min(items.get(item, 0), take)
if part:
items[item] -= part
used.setdefault(uid, {})[item] = part
take -= part
if not take:
break
return used
def _refund(world: dict, order: dict, now: float) -> None:
"""Hand back what's left in the pool to whoever put it in, and the unpaid reward to the requester."""
for uid, items in _gave(order).items():
player = world["players"].get(uid)
for item, qty in items.items():
if qty and player:
bank_add(world, player, item, qty, now, empty_summary())
order["provided"], order["gave"] = {}, {}
owner = world["players"].get(order["by"])
if not owner:
return
for item, qty in order["provided"].items():
if qty:
bank_add(world, owner, item, qty, now, empty_summary())
order["provided"] = {}
owner["gold"] += order["reward"] - order["paid"]
if owner:
owner["gold"] += order["reward"] - order["paid"]
order["paid"] = order["reward"]
@ -2692,12 +2727,26 @@ def deliver_order(world: dict, player: dict, order_id, now: float) -> str | None
return None
def _deliver(world: dict, order: dict, giver: dict, qty: int, now: float) -> None:
def _deliver(world: dict, order: dict, giver: dict, qty: int, now: float,
used: dict | None = None, inputs: dict | None = None) -> None:
"""Pay for `qty` pieces. Whoever's reagents went into them (`used` of the run's `inputs`) shares
INPUT_SHARE of the pay by value; the requester's own reagents leave theirs with the crafter."""
order["delivered"] += qty
order["from"][giver["id"]] = order["from"].get(giver["id"], 0) + qty
order["held"] += qty
pay = round(order["reward"] * order["delivered"] / order["qty"]) - order["paid"]
order["paid"] += pay
worth, base = sum(ITEMS[item]["value"] * n for item, n in (inputs or {}).items()), pay
for uid, items in (used or {}).items():
helper = world["players"].get(uid)
if uid == order["by"] or not helper or not worth:
continue
share = min(pay, round(base * INPUT_SHARE * sum(ITEMS[i]["value"] * n for i, n in items.items()) / worth))
if share:
helper["gold"] += share
earned = order.setdefault("earned", {})
earned[uid] = earned.get(uid, 0) + share
pay -= share
giver["gold"] += pay
owner = world["players"].get(order["by"])
if owner:
@ -2705,6 +2754,7 @@ def _deliver(world: dict, order: dict, giver: dict, qty: int, now: float) -> Non
if order["delivered"] >= order["qty"]:
order["status"] = "done"
order["done"] = now
_refund(world, order, now) # leftover reagents
log(world, now, "order", f"{giver['name']} filled {owner['name'] if owner else 'someone'}'s order: "
f"{amount(order['item'], order['qty'])}")
@ -4419,7 +4469,7 @@ def content() -> dict:
"city_uses": {item: city_uses(item) for item in ITEMS},
"prayers": [{**prayer, "text": describe_prayer(prayer)} for prayer in PRAYERS], "max_prayers": MAX_PRAYERS,
"potion_charges": POTION_CHARGES, "order_recipes": ORDER_RECIPES, "order_raw": ORDER_RAW,
"order_limit": ORDER_LIMIT, "order_max": ORDER_MAX, "problems": PROBLEMS, "fittings": list(FITTING_BY_ID.values()),
"order_limit": ORDER_LIMIT, "order_max": ORDER_MAX, "input_share": INPUT_SHARE, "problems": PROBLEMS, "fittings": list(FITTING_BY_ID.values()),
"equip_slots": EQUIP_SLOTS, "eat_below": EAT_BELOW, "class_speed": CLASS_SPEED,
"styles": STYLES, "damage_types": DAMAGE_TYPES, "weights": WEIGHTS, "vulnerable": VULNERABLE,
"resistant": RESISTANT, "jewel_stats": JEWEL_STATS, "stations": list(STATIONS), "class_style": CLASS_STYLE, "block_chance": BLOCK_CHANCE,