feat(currency): copper/silver/gold replace the coin

The three denominations are move tokens, not stored buckets — there is one
integer, so change-making never arises. The deserter is down to 47c: under a
silver, so he cannot cover a bed and the player sees it on turn one.

A parity test guards the code-side ratios against the content-side ids.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
This commit is contained in:
2026-07-12 18:36:08 -05:00
parent c106dbbd37
commit a9d9eedb7e
8 changed files with 27 additions and 6 deletions

View File

@@ -61,9 +61,10 @@ static func construct(origin: Dictionary, world: ContentDB, creation: Dictionary
var qd: Dictionary = world.quest(quest_id)
log.active_quests.append(Quest.new(qd.get("id", ""), qd.get("name", ""), "active", qd.get("objective", "")))
# inventory grants -> game state (NOT the log). Charter §2.
# inventory grants -> game state (NOT the log). Charter §2. grant() routes
# currency to the purse; everything else to the inventory dict.
for grant in origin.get("inventory_grants", []):
state.add_item(grant.get("item_id", ""), int(grant.get("qty", 0)))
state.grant(grant.get("item_id", ""), int(grant.get("qty", 0)))
return {"ok": true, "errors": [], "log": log, "state": state}

View File

@@ -18,7 +18,24 @@ func test_loads_world_ids():
assert_true(db.has_location("greywater_docks"))
assert_true(db.has_quest("find_the_ledger"))
assert_true(db.has_item("worn_shortsword"))
assert_true(db.has_item("coin"))
assert_true(db.has_item("copper"))
assert_true(db.has_item("silver"))
assert_true(db.has_item("gold"))
assert_false(db.has_item("coin"), "the undifferentiated coin is gone")
func test_currency_items_agree_with_the_code():
# The denomination ids exist twice — in code (Currency.VALUES, because the
# ratios are rules) and in content (slot: "currency"). Guard the two against
# drifting apart, since nothing at runtime reconciles them.
var content_ids: Array = []
for id in db.items:
if db.items[id].get("slot", "") == "currency":
content_ids.append(id)
content_ids.sort()
var code_ids: Array = Currency.VALUES.keys()
code_ids.sort()
assert_eq(content_ids, code_ids)
func test_companions_are_brannoc_and_cadwyn():

View File

@@ -40,8 +40,9 @@ func test_numeric_luck_absent_from_log():
func test_inventory_and_dispo_land_in_state_not_log():
var res := _build({"name": "Aldric", "class_id": "sellsword"})
assert_eq(res["state"].inventory.get("coin", 0), 3)
assert_eq(res["state"].inventory.get("worn_shortsword", 0), 1)
assert_eq(res["state"].purse_copper, 47, "the deserter is down to his last coin")
assert_false("copper" in res["state"].inventory, "money is never an inventory row")
assert_false("inventory" in res["log"].to_dict())

View File

@@ -15,7 +15,7 @@
"disposition_overrides": { "brannoc_thane": 40, "cadwyn_vell": 15 },
"inventory_grants": [
{ "item_id": "worn_shortsword", "qty": 1 },
{ "item_id": "coin", "qty": 3 }
{ "item_id": "copper", "qty": 47 }
],
"start_quest_id": "find_the_ledger",
"build_constraints": {

View File

@@ -1 +0,0 @@
{ "id": "coin", "name": "coin", "slot": "currency" }

View File

@@ -0,0 +1 @@
{ "id": "copper", "name": "copper coin", "slot": "currency" }

View File

@@ -0,0 +1 @@
{ "id": "gold", "name": "gold coin", "slot": "currency" }

View File

@@ -0,0 +1 @@
{ "id": "silver", "name": "silver coin", "slot": "currency" }