Skip to content

engine: one Amount, not a ByCount and a ByAmount twin per verb #826

Description

@ninthworld

Goal

One Amount says how much, so a verb stops being duplicated once per way of counting and
Creatures you control get +1/+1 for each … becomes authorable without two more variants.

Context and evidence

Parent: #819, §2.2. First of two halves; #827 collapses the subject axis.

Effect carries the same verb three times over, once per way its number is derived:

Verb Printed number Count of permanents Derived amount
pump a target Pump PumpByCount PumpByAmount
deal damage DealDamage DealDamageByCount DealDamageByAmount
gain life GainLife GainLifeByCount
lose life LoseLife LoseLifeByAmount
draw DrawCard DrawCardsByAmount
discard Discard DiscardByAmount
pump a class PumpAll

Eleven variants for six verbs, and the table's holes are the point: LoseLifeByCount and
GainLifeByAmount do not exist because no M19 card needed them, so the next card that does
adds a variant plus an arm in each of ~6 exhaustive matches — including 86 arms in
sage-server/src/rules_text/words.rs and 86 Effect:: arms in
sage-server/src/rules_text/effects.rs. Effect is at 82 variants.

The codebase already solved this once, and its own reasoning is the argument.
CreateToken takes count_of as a field, not a create_token_by_count twin:

A field rather than a create_token_by_count twin, which is where this departs from
Effect::PumpByCount and its two siblings: a second variant would duplicate the four other
fields here … The field says where that number comes from; it does not add a second verb.

The counter-argument stated in PumpByAmount's docs is that two optional fields would make
"neither" and "both" authorable shapes that mean nothing. That is true of two Option fields
and false of a sum type:

enum Amount { Fixed(i32), PerPermanent { per: i32, count_of: PermanentCount },
              Derived { per: i32, source: DerivedAmount } }

Exactly one is present, by construction. CreateToken's field already proves the ergonomics.

The CR 608.2 semantics are identical across all three today — the amount is read once, as the
effect applies — which is what makes this a mechanical collapse rather than a behaviour change.

exclusions.json names one limit that is a limit of this shape: "Effects whose amount is
derived from a source outside the ones the IR names."
Narrowing DerivedAmount is orthogonal
to this issue, but the entry should be re-read against the collapsed shape.

Scope

  • One Amount sum type covering the three derivations already modelled.
  • The eleven variants above collapsed to six verbs carrying it.
  • Resolution reads the amount at the one CR 608.2 seam, once, exactly as each variant does now.
  • The server's rules-text formatter renders the amount from one place instead of three arms per
    verb.
  • Catalog JSON re-authored. No card's rules change.
  • The validator keeps refusing an amount a given verb cannot mean.

Acceptance criteria

  • PumpByCount, PumpByAmount, DealDamageByCount, DealDamageByAmount,
    GainLifeByCount, LoseLifeByAmount, DrawCardsByAmount, and DiscardByAmount no
    longer exist as separate variants.
  • The four combinations the table is missing today — a life gain per derived amount, a life
    loss per counted permanent, and the two on the class row — are authorable with no Rust
    change, and at least one is proven by a test.
  • Every affected card behaves identically; the existing behavioural suites pass with the
    re-authored catalog.
  • The amount is still read once, on resolution, and a "this way" amount still reads the
    same resolution's earlier work (patient_rebuilding, scapeshift,
    liliana_untouched_by_death).
  • nightmare_s_thirst still reads life gained this turn and not life gained after it
    resolves.
  • make compat regenerated; make verify green.

Test and validation evidence

  • Existing m19_* suites over re-authored JSON — this is behaviour-preserving.
  • A new test for one of the previously impossible combinations, card-shaped.
  • Rules-text tests for each verb's three renderings.
  • make verify.

Dependencies and sequencing

After #820 and #822 so the catalog is correct first. Sequence before #827: the subject
collapse is much cleaner over verbs that already carry one Amount. Overlaps #824/#825 in
files — pick an order.

Estimated scope

Medium — focused cross-layer change: one type, eleven variants to six, and the formatter.

Risks and constraints

The three derivations have subtly different reading rules per verb — LoseLifeByAmount reads
of each named player ("half their life" is each seat's own half), while
GainLifeByCount counts relative to the effect's controller. Collapsing the carrier must
not collapse that distinction; it belongs to the PlayerRef/CountScope already present, and
a test per verb should pin it.

Non-goals

The subject axis (#827). Widening DerivedAmount to new sources. CreateToken, which already
has the right shape.

Documentation impact

docs/card-schema.md — the amount vocabulary. exclusions.json — re-read the derived-amount
entry. No ADR: this is the shape ADR 0003 already chose, applied consistently.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions