feat(currency): money moves through the bounded vocabulary (§6)
give_item(gold) is +10,000c and does NOT mark gifts_given — that gate is a global one-shot keyed by item id, so marking money would let an NPC pay you exactly once per campaign. accept_item(<denom>) is offered only for the denominations the purse can actually pay. The eight moves do not grow. MoveValidator is untouched. 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:
@@ -23,10 +23,11 @@ static func apply(moves: Array, game_state, canon_log, content_db, npc_id: Strin
|
||||
game_state.mark_revealed(str(args[0]))
|
||||
"give_item":
|
||||
var i: String = str(args[0])
|
||||
game_state.add_item(i, 1)
|
||||
game_state.mark_gift_given(i)
|
||||
game_state.grant(i, 1)
|
||||
if not Currency.is_currency(i):
|
||||
game_state.mark_gift_given(i) # the one-shot gate is for real items only
|
||||
"accept_item":
|
||||
game_state.remove_item(str(args[0]), 1)
|
||||
game_state.take(str(args[0]), 1)
|
||||
"adjust_disposition":
|
||||
var delta: int = clampi(int(str(args[0])), -MAX_DELTA, MAX_DELTA)
|
||||
var cur: int = int(game_state.npc_dispositions.get(npc_id, 0))
|
||||
|
||||
@@ -21,12 +21,20 @@ static func available_moves(npc_dict: Dictionary, game_state, canon_log) -> Arra
|
||||
moves.append("reveal(%s)" % t)
|
||||
|
||||
for i in caps.get("giveable_items", []):
|
||||
if not game_state.is_gift_given(i):
|
||||
# Currency is exempt from the one-shot gift gate — an NPC can pay you twice.
|
||||
if Currency.is_currency(i) or not game_state.is_gift_given(i):
|
||||
moves.append("give_item(%s)" % i)
|
||||
|
||||
for item_id in game_state.inventory.keys():
|
||||
if int(game_state.inventory[item_id]) > 0:
|
||||
moves.append("accept_item(%s)" % item_id)
|
||||
|
||||
# Money is not in the inventory dict, so it needs its own offer. Only the
|
||||
# denominations the purse can actually pay: an NPC cannot ask a party holding
|
||||
# 47c for a gold coin.
|
||||
for denom in Currency.VALUES:
|
||||
if game_state.purse_copper >= Currency.value(denom):
|
||||
moves.append("accept_item(%s)" % denom)
|
||||
|
||||
moves.append_array(UNIVERSAL)
|
||||
return moves
|
||||
|
||||
@@ -44,9 +44,40 @@ func test_become_hostile_sets_floor():
|
||||
|
||||
func test_accept_item_removes_from_inventory():
|
||||
var gs = GameState.new()
|
||||
gs.add_item("coin", 2)
|
||||
MoveApplier.apply([_m("accept_item", ["coin"])], gs, CanonLog.new(), _content(), "fenn")
|
||||
assert_eq(gs.inventory.get("coin", 0), 1)
|
||||
gs.add_item("worn_shortsword", 2)
|
||||
MoveApplier.apply([_m("accept_item", ["worn_shortsword"])], gs, CanonLog.new(), _content(), "fenn")
|
||||
assert_eq(gs.inventory.get("worn_shortsword", 0), 1)
|
||||
|
||||
|
||||
func test_give_item_currency_pays_the_purse():
|
||||
var gs = GameState.new()
|
||||
MoveApplier.apply([_m("give_item", ["gold"])], gs, CanonLog.new(), _content(), "fenn")
|
||||
assert_eq(gs.purse_copper, 10000)
|
||||
assert_eq(gs.inventory.size(), 0, "money is never an inventory row")
|
||||
|
||||
|
||||
func test_give_item_currency_is_not_a_one_shot_gift():
|
||||
# gifts_given is a GLOBAL one-shot keyed by item id. If money were marked, an
|
||||
# NPC could hand over coin exactly once, ever, for the whole campaign.
|
||||
var gs = GameState.new()
|
||||
MoveApplier.apply([_m("give_item", ["gold"])], gs, CanonLog.new(), _content(), "fenn")
|
||||
MoveApplier.apply([_m("give_item", ["gold"])], gs, CanonLog.new(), _content(), "fenn")
|
||||
assert_eq(gs.purse_copper, 20000)
|
||||
assert_false(gs.is_gift_given("gold"), "currency is exempt from the one-shot gate")
|
||||
|
||||
|
||||
func test_give_item_real_item_still_marks_the_one_shot_gate():
|
||||
var gs = GameState.new()
|
||||
MoveApplier.apply([_m("give_item", ["amulet"])], gs, CanonLog.new(), _content(), "fenn")
|
||||
assert_eq(gs.inventory.get("amulet", 0), 1)
|
||||
assert_true(gs.is_gift_given("amulet"))
|
||||
|
||||
|
||||
func test_accept_item_currency_spends_the_purse():
|
||||
var gs = GameState.new()
|
||||
gs.grant("gold", 1)
|
||||
MoveApplier.apply([_m("accept_item", ["silver"])], gs, CanonLog.new(), _content(), "fenn")
|
||||
assert_eq(gs.purse_copper, 9900)
|
||||
|
||||
|
||||
func test_refuse_and_end_are_noops():
|
||||
|
||||
@@ -39,6 +39,39 @@ func test_revealed_topic_drops_out():
|
||||
|
||||
func test_accept_item_offered_per_held_item():
|
||||
var gs = GameState.new()
|
||||
gs.add_item("coin", 2)
|
||||
gs.add_item("worn_shortsword", 2)
|
||||
var moves = NpcContent.available_moves(_fenn(), gs, CanonLog.new())
|
||||
assert_true("accept_item(coin)" in moves)
|
||||
assert_true("accept_item(worn_shortsword)" in moves)
|
||||
|
||||
|
||||
func test_a_broke_party_is_asked_for_nothing():
|
||||
var moves = NpcContent.available_moves(_fenn(), GameState.new(), CanonLog.new())
|
||||
for denom in ["copper", "silver", "gold"]:
|
||||
assert_false("accept_item(%s)" % denom in moves, "an empty purse cannot pay a %s" % denom)
|
||||
|
||||
|
||||
func test_accept_item_denomination_gated_on_affordability():
|
||||
var gs = GameState.new()
|
||||
gs.grant("copper", 347)
|
||||
var moves = NpcContent.available_moves(_fenn(), gs, CanonLog.new())
|
||||
assert_true("accept_item(copper)" in moves)
|
||||
assert_true("accept_item(silver)" in moves)
|
||||
assert_false("accept_item(gold)" in moves, "347c cannot pay a gold coin")
|
||||
|
||||
|
||||
func test_a_gold_coin_is_asked_for_once_affordable():
|
||||
var gs = GameState.new()
|
||||
gs.grant("gold", 1)
|
||||
var moves = NpcContent.available_moves(_fenn(), gs, CanonLog.new())
|
||||
assert_true("accept_item(gold)" in moves)
|
||||
|
||||
|
||||
func test_currency_gift_is_offered_repeatedly():
|
||||
var npc := _fenn()
|
||||
npc["capabilities"]["giveable_items"] = ["copper", "amulet"]
|
||||
var gs = GameState.new()
|
||||
gs.mark_gift_given("copper")
|
||||
gs.mark_gift_given("amulet")
|
||||
var moves = NpcContent.available_moves(npc, gs, CanonLog.new())
|
||||
assert_true("give_item(copper)" in moves, "money is not a one-shot gift")
|
||||
assert_false("give_item(amulet)" in moves, "a real item still is")
|
||||
|
||||
Reference in New Issue
Block a user