Distribute recovered treasure across the party - #32
Merged
Conversation
Every treasure pickup landed entirely on the first living member: all three branches of _handle_take_treasure took `living[0]`. Since battle and surrender spoils funnel back through TakeTreasure, that covered essentially every haul in play. It is not only unfair. RAW puts maximum load at 1,600 coins of weight, states that a character carrying more cannot move, and sets the party's rate by its slowest member — so one haul on one character stops everyone. Measured: 150 gp plus a 600 sp / 900 cp pile put 1,650 coins on member zero, dropped them to a movement rate of 0, and had MoveParty reject with exploration.move.cannot_move for the whole party. A haul is now distributed: items first, then valuables, then coins. Items go to a character whose class can actually use them, checked through the existing equip policies and, for magic, usable_by_class — so the plate mail stops landing on the magic-user. Least-loaded wins among usable carriers, and an item nobody can use still finds a pack rather than vanishing. Gems and jewellery balance by worth rather than count, richest piece first to whoever holds the least value. A share means worth, and value is nearly free to move — a thousand-gold gem weighs one coin. Coins then divide evenly denomination by denomination, which is the one place fairness and mobility agree, since every coin weighs the same. Nothing is ever placed past a character's maximum load, so a take cannot immobilise anyone. What will not fit stays in the cell as a pile and emits exploration.item.left_behind; best-first packing means a capacity-starved party keeps the platinum and leaves the copper. TakeTreasure gains an optional recipient_id for callers that want one carrier, who is also the character who springs any trap on the cache. RAW is silent on who physically carries what — "Awarded XP is always divided evenly, irrespective of how the players decide to divide the treasure" — so this is registered as an engineering decision in the adaptations register, not as a rule. XP is unaffected either way; it sums the whole party. Distribution consumes no randomness. The phase-4 and phase-5 goldens are regenerated for the event stream only: every RNG stream state, clock reading, XP award, and level-up line is byte-identical. Also fixes an aliasing bug found while writing the back-out path. Pydantic models compare by value, so removing an item by equality could remove an equal item the carrier already owned and leave the offered one in two inventories at once. Removal is now by identity. Claude-Session: https://claude.ai/code/session_01PjCy2kE4R1Yj85rizkkXpV
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the case reported downstream as osr-web#8: "Party member in position 0 is bestowed all loot."
The bug
All three branches of
_handle_take_treasuretookliving[0]. Battle and surrender spoils land in the cell'sDropPileand are recovered throughTakeTreasure, so this covered essentially every haul in play.It can immobilise the party. RAW puts maximum load at 1,600 coins of weight, says a character carrying more cannot move, and sets the party's rate by its slowest member. Measured: 150 gp plus a 600 sp / 900 cp pile put 1,650 coins on member zero, dropped them to movement rate 0, and
MovePartythen rejected withexploration.move.cannot_movefor the whole party. One pickup, party stuck, unwindable only by hand throughGiveItems.Every write to a member's inventory or purse was swept; the other sites (
GiveItems, the referee grant commands, selling, purchasing, evasion, NPC equipping, starting gold) already name their recipient and are correct.The policy
Items → valuables → coins. Items are the lumpy, class-constrained part and go out while everyone still has room to honour the constraint; wealth follows as the fluid balancer.
usable_by_class— so plate mail stops landing on the magic-user. Least-loaded wins among usable carriers; an item nobody can use still finds a pack rather than vanishing.exploration.item.left_behind; best-first packing means a capacity-starved party keeps the platinum and leaves the copper.recipient_idis optional and additive — one named carrier, capped at their own load, and also the character who springs any trap on the cache.RAW
Silent on who physically carries, and explicitly so —
srd/Awarding_XP.md: "Awarded XP is always divided evenly, irrespective of how the players decide to divide the treasure." Registered as an engineering decision in the adaptations register, not as a rule. What RAW does pin is the consequence, inTime, Weight, Movement: the 1,600-coin ceiling and party-speed-is-slowest-member. XP is unaffected — it sums the whole party.Determinism
Distribution consumes no randomness (asserted against exported stream state). Goldens regenerated for the event stream only; every RNG stream state, clock reading, XP award, and level-up line is byte-identical, and the phase-4 seed search selected the same seed.
recipient_idis additive, so noschema_versionbump.An aliasing bug found in passing
Pydantic models compare by value, so removing an item by equality could remove an equal item the carrier already owned and leave the offered one aliased into two inventories at once. Removal is now by identity, with a regression test that fails
2 == 2 and 1 == 2if reverted.Verification
1606 passed, 58 skipped(baseline 1590, +16)ruff format --check,ruff check,pyright0 errors,mkdocs build --strictcleanplay-view.pngnow shows the party splitting a haul — the thief takes the silver dagger and holy water, everyone shares the coin — instead of one name hoovering the lot.https://claude.ai/code/session_01PjCy2kE4R1Yj85rizkkXpV