Skip to content

engine: one subject and one modification payload for pump, grant, and restrict #827

Description

@ninthworld

Goal

One effect says who and one payload says what changes, so a printed sentence about one
permanent is one effect whether that permanent is the source, a target, or a class — and the
four empty cells in the matrix fill themselves.

Context and evidence

Parent: #819, §2.2. Second of two halves; #826 collapses the amount axis and should land
first.

The subject × payload matrix is one variant per cell, and it is unevenly filled:

power/toughness keyword restriction written-out ability
source PumpSelf AlterAbilitiesSelf.gain RestrictSelf
target Pump Pump.keywords / GrantKeyword Pump.restrictions / Restrict Pump.abilities
class PumpAll GrantKeywordAll RestrictAll

Nine variants for one idea, and three consequences visible in the shipped catalog:

The self row has no keyword grant of its own. The only way to add a keyword to the source
is AlterAbilitiesSelf.gain, whose sibling field is lose_all — reaching for the
lose-all-abilities verb to say "gains lifelink". resplendent_angel's third printed ability
({3}{W}{W}{W}: gets +2/+2 and gains lifelink) is missing from the catalog entirely, and
this is the most likely reason (#821).

One printed sentence becomes three effects. chromium_the_mutable's
Discard a card: … becomes a Human with base power and toughness 1/1, loses all abilities, and gains hexproof. It can't be blocked this turn. is animate_self + alter_abilities_self +
restrict_self. They share a CR 613.7 timestamp only by accident of being adjacent in a list.

Two effects naming the same target advertise two slots. Effect::target_groups is
collected with flat_map over the effect list (apply.rs:191, actions/targeting.rs:91) with
no dedup. docs/card-schema.md names the hazard —

One effect declares one target group, so two effects would advertise two independent slots
and let a player pump one creature while a different one gained flying.

— and prescribes folding the grant into the pump. That prescription only covers pump:
PutCounters has no keywords field, which is why skyrider_patrol is authored the way the
document warns against and asks for two targets (#821). A single Subject field makes the
bug unrepresentable rather than merely discouraged.

Scope

  • One Subject { This, Target(TargetSpec, TargetCount), Class(PermanentFilter) } — the axis
    that decides who, and the one place a target group is declared.
  • One modification payload — power/toughness, keywords, combat restrictions, written-out
    abilities, and (where it belongs) counters — the axis that decides what changes.
  • The nine variants above collapsed onto it, with the four empty cells filled by construction.
  • PutCounters reconciled: either it joins the payload, or the reason it stays separate is
    stated where the payload lives.
  • Durations stay explicit — until end of turn, until your next turn, for as long as the source
    remains — since Animate already models three and the collapse must not flatten them.
  • Catalog JSON re-authored, including chromium_the_mutable as one effect per printed sentence.
  • The validator keeps refusing a subject/payload pairing that means nothing, and gains one for
    the case that is now representable and still wrong.

Acceptance criteria

  • PumpSelf, PumpAll, GrantKeyword, GrantKeywordAll, Restrict, RestrictSelf, and
    RestrictAll no longer exist as separate variants.
  • A self-directed keyword grant is authorable without touching the lose-all-abilities verb,
    and resplendent_angel's {3}{W}{W}{W} ability is in the catalog and works.
  • skyrider_patrol's trigger declares one target group; a test asserts the slot count
    with two eligible creatures on the board.
  • No ability in the catalog can declare two target groups for one printed target — enforced
    by the type or by a validator with a named Violation, not by review.
  • Granting a written-out ability to the source and to a class is authorable, and one
    of the two is proven by a test.
  • chromium_the_mutable's activated ability is one effect and its halves share one
    CR 613.7 timestamp.
  • Keyword grants stay idempotent and written-out ability grants stay additive, per
    CR 613.1f — unchanged from today and tested.
  • alter_abilities_self's lose / lose_all half survives intact; this collapses the
    additive row only.
  • make compat regenerated; make verify green.

Test and validation evidence

  • Existing m19_* behavioural suites over the re-authored catalog — behaviour-preserving for
    every card except the two that were wrong.
  • New tests for resplendent_angel and skyrider_patrol (see engine: two M19 cards the ability IR cannot currently say #821 — if that lands first,
    these are its tests, unchanged).
  • A catalog-wide test that no ability declares duplicate target groups.
  • Layer-6 tests: a granted keyword indistinguishable from a printed one, a granted dies trigger
    still firing from the snapshot it was granted in.
  • Rules-text tests for each row of the matrix.
  • make verify.

Dependencies and sequencing

After #826 (one Amount first — the payload is much cleaner over verbs that already carry
one). After #824 if Subject::Class is to carry the shared filter rather than MassAffects.
Supersedes #821 if scheduled before it; otherwise it collapses the narrow affordances
#821 adds.

Estimated scope

Large — broad change requiring a split assessment. Split from #826 along the two independent
axes; splitting further would mean landing a half-collapsed matrix, which is harder to reason
about than either end state.

Risks and constraints

  • Layer 6 and CR 613.7 are the correctness surface. Merging three adjacent effects into one
    changes how many timestamps exist. That is the fix for chromium_the_mutable, and a
    regression anywhere it was load-bearing by accident. The layer tests are the net; run them
    card by card during re-authoring, not once at the end.
  • A granted dies trigger is the one grant that outlives the grant, read from the snapshot
    the permanent still carried it in (CR 603.6c/603.10a). The collapse must not disturb the
    accessor path — abilities_of_permanent stays the only road, per the engine guide.
  • Idempotence differs by payload: duplicate keyword grants are redundant, duplicate ability
    grants are additive. One payload carrying both must keep both rules.

Non-goals

The amount axis (#826). TargetSpec's internals (#825). AlterAbilitiesSelf's subtractive
half. The bundled one-card verbs (#828).

Documentation impact

docs/card-schema.md — "Granting keywords", "Granting a whole ability", and "Losing keywords
and losing all abilities" all describe the current per-row shape and are rewritten around the
matrix. "How far the vocabulary reaches" gains the four cells. An ADR is warranted here if
anywhere in this tranche: this changes how every future card is authored, and per AGENTS.md
it is written after the code proves the shape, not before.

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