test(currency): pin both halves of the dedup key
The existing test varied the move name and the denomination together, so a name-only or denom-only key would have passed it too. Two assertions nail it: give_item(gold)+give_item(silver) kills a name-only key, and give_item(gold)+accept_item(gold) kills a denom-only key. The spec now records the per-reply ceiling as a decision rather than an accident, and flags the pre-existing non-currency duplicate-tag path as open. 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:
@@ -196,6 +196,25 @@ func take(item_id: String, qty: int) -> void # currency -> purse_copper; else
|
||||
`take` clamps the purse at `0`. The validator already gates affordability (§4.7); the
|
||||
clamp is defence in depth, not the mechanism.
|
||||
|
||||
**A currency move applies at most once per reply.** `TagExtractor` returns *every* tag
|
||||
occurrence, `MoveValidator` is a pure membership test, and `MoveApplier` loops — so
|
||||
without this, a model repeating `[MOVE: accept_item(copper)]` forty-seven times would
|
||||
drain a 47c purse, and ten `give_item(gold)` tags would mint 100,000c. That is the model
|
||||
choosing an amount **in unary** — the §2 breach that rejecting a quantity argument was
|
||||
meant to prevent. `MoveApplier` therefore keys currency moves on `name(denomination)`
|
||||
and drops repeats within a reply.
|
||||
|
||||
The residual ceiling is deliberate: one reply may still combine *distinct* denominations
|
||||
(`give_item(gold)` + `give_item(silver)` + `give_item(copper)` = +10,101c). That is a
|
||||
model selecting a subset of a closed, six-element, state-gated set — an NPC handing over
|
||||
a gold, a silver and a copper is a legal sentence — not inventing an integer. Unary
|
||||
repetition was what made it unbounded, and that is closed.
|
||||
|
||||
**The same duplicate-tag path exists for non-currency moves and predates this work**
|
||||
(three `give_item(amulet)` tags yield three amulets; a repeated `offer_quest` can
|
||||
double-add a quest). It is bounded *across* replies by `gifts_given` / `has_quest`, but
|
||||
not *within* one. Deliberately left alone here — it is not a currency bug. **Open.**
|
||||
|
||||
**`NpcContent.available_moves`:**
|
||||
|
||||
- `accept_item(<denom>)` is offered for each denomination the player can actually
|
||||
|
||||
Reference in New Issue
Block a user