Goal
The one-card effect verbs are split into the reusable halves they are made of, so the next set
can reach for "exile the top N", "you may cast a card exiled this way", or "each player chooses
one of theirs" without a new enum variant and six new match arms.
Context and evidence
Parent: #819, §2.3.
Across the 299 catalog files there are 121 distinct kind values, 60 of which are used by
exactly one card. Some are genuinely singular primitives and are fine — win_the_game,
no_maximum_hand_size, play_lands_from_graveyard are one card's rule and nothing else's.
The problem is the ones that bundle several rules steps into one opaque verb instead of
composing verbs that already exist:
| Verb |
Card |
What it bundles |
sacrifice_chosen_per_player |
Vaevictis Asmadi |
per-seat targeting, a sacrifice, a reveal, and a conditional put-onto-battlefield — behind one reveal_top: bool |
may_cast_exiled_this_way |
Chaos Wand |
a may-cast and, silently, "put the rest on the bottom in a random order" |
exile_from_library_until |
Chaos Wand |
the dig half of the same sentence |
reveal_top_and_may_play |
Djinn of Wishes |
reveal + may-play-for-free + exile-if-declined |
remove_all_counters |
Suncleanser |
removal + a continuous prohibition (then_forbid) |
player_loses_all_counters |
Suncleanser |
the same, for a player — a second verb for one card's second mode |
exile_library_except_bottom |
Nicol Bolas |
— |
shuffle_self_into_library |
Inferno Hellion |
— |
exchange_control |
Switcheroo |
— |
ignore_hexproof |
Detection Tower |
— |
put_hand_onto_battlefield_face_down |
Tezzeret, Cruel Machinist |
— |
exile_self_and_return_transformed |
Nicol Bolas |
— |
sacrifice_chosen_per_player also owns the only special case in Effect::target_groups
(ability/effect/targets.rs:99) — the one effect whose slot count comes from the seat count
rather than from the card. That per-seat targeting is the genuinely reusable half, and nothing
else can reach it.
exile_top_for_play is the counterexample and the model: properly decomposed, used by two
cards (Apex of Power, Dark-Dweller Oracle), with cast_only as a field rather than a twin verb.
The engine guide states the tax plainly — "each new primitive is one enum variant plus every
exhaustive match that consumes it, across the resolver and the server's rules-text formatter —
both are wildcard-free, so the compiler names every site." Effect is at 82 variants
against 86 Effect:: arms in sage-server/src/rules_text/effects.rs alone. A second set
will not cost half as much per card; it will cost more, because the phrasings it needs are the
ones that were never worth a variant the first time.
exclusions.json names at least three limits that are limits of this bundling rather than of
the rules — "Shuffling anything but the effect's own source into a library", "Gaining
control of a permanent for longer than a turn", and "Playing a card from a zone other than
the hand, the command zone, or a permitted graveyard" — each of which is a bundled verb's
missing half.
Scope
Split each bundled verb along its own seams, keeping behaviour identical. At minimum:
- Per-seat targeting lifted out of
sacrifice_chosen_per_player into a target-group shape
any effect can declare, so the Vaevictis trigger becomes a sacrifice plus a reveal-and-put
rather than one verb.
- Chaos Wand's sentence as three composable effects — dig until a class, may-cast a card
exiled this way, bottom the rest in a random order — with the bottoming visible in the
authored JSON rather than hidden inside the cast verb.
- Djinn of Wishes as reveal + may-play + otherwise-exile, reusing whatever
exile_top_for_play and may_cast_exiled_this_way become.
- Suncleanser's two modes sharing one counter-removal verb and one prohibition, with the
subject (permanent or player) as a field.
- The remaining singles reviewed one at a time: each either keeps its variant with the reason
stated where it lives, or gains a field that makes it reusable. shuffle_self_into_library
and exchange_control in particular are one destination and one duration away from the
general cases exclusions.json currently names.
Behaviour-preserving throughout. No card's rules change.
Acceptance criteria
Test and validation evidence
- The existing behavioural tests for each affected card are the net; they should pass unchanged
in intent over re-authored JSON.
- A test proving one newly-general capability — e.g. a second card, or a scenario file, that
reaches a decomposed half the original card did not need.
- Rules-text tests: a decomposed sentence must still render as one readable sentence, not as
three clauses in a list. This is the visible risk and wants a snapshot.
make verify.
Dependencies and sequencing
Independent of #824–#827 in subject matter, but overlapping in files — sequence rather than
parallelise. Best done after them, since the collapsed selector and payload shapes are what
several of these verbs would decompose into. Must land before another set is authored, which
is the whole point.
Estimated scope
Large — broad change requiring a split assessment. It splits cleanly per verb if it needs to:
sacrifice_chosen_per_player alone is a coherent PR, and so is the Chaos-Wand/Djinn pair. Take
it verb by verb with the suite green at each step rather than as one landing.
Risks and constraints
- Rules text is the visible risk. A bundled verb renders one sentence; three composed
effects render three. The server's formatter must recompose them, or players read a card
differently than it is printed — and AGENTS.md makes playing the merge criterion.
- Decomposition can change when things happen within a resolution. Chaos Wand's bottoming
happens after the may-cast; making it a separate effect makes that ordering authored rather
than implied, which is an improvement only if the authored order is right.
- Some of these genuinely are one card's rule. Splitting a verb that nothing will ever reuse
trades one variant for three and is a net loss; the "stated reason for being singular"
criterion exists so that judgment is recorded rather than re-litigated.
Non-goals
Not the selector unification (#824, #825) or the verb matrix (#826, #827). Not adding
mechanics no bundled verb already contains. Not authoring a second set.
Documentation impact
docs/card-schema.md — "How far the vocabulary reaches" enumerates these verbs and is rewritten
around the decomposed ones. exclusions.json — entries dropped or narrowed as capabilities land.
crates/sage-engine/AGENTS.md — "The IR is the constraint" section may want a sentence on
preferring composition to a new verb, which is the lesson this issue encodes.
Goal
The one-card effect verbs are split into the reusable halves they are made of, so the next set
can reach for "exile the top N", "you may cast a card exiled this way", or "each player chooses
one of theirs" without a new enum variant and six new match arms.
Context and evidence
Parent: #819, §2.3.
Across the 299 catalog files there are 121 distinct
kindvalues, 60 of which are used byexactly one card. Some are genuinely singular primitives and are fine —
win_the_game,no_maximum_hand_size,play_lands_from_graveyardare one card's rule and nothing else's.The problem is the ones that bundle several rules steps into one opaque verb instead of
composing verbs that already exist:
sacrifice_chosen_per_playerreveal_top: boolmay_cast_exiled_this_wayexile_from_library_untilreveal_top_and_may_playremove_all_countersthen_forbid)player_loses_all_countersexile_library_except_bottomshuffle_self_into_libraryexchange_controlignore_hexproofput_hand_onto_battlefield_face_downexile_self_and_return_transformedsacrifice_chosen_per_playeralso owns the only special case inEffect::target_groups(
ability/effect/targets.rs:99) — the one effect whose slot count comes from the seat countrather than from the card. That per-seat targeting is the genuinely reusable half, and nothing
else can reach it.
exile_top_for_playis the counterexample and the model: properly decomposed, used by twocards (Apex of Power, Dark-Dweller Oracle), with
cast_onlyas a field rather than a twin verb.The engine guide states the tax plainly — "each new primitive is one enum variant plus every
exhaustive match that consumes it, across the resolver and the server's rules-text formatter —
both are wildcard-free, so the compiler names every site."
Effectis at 82 variantsagainst 86
Effect::arms insage-server/src/rules_text/effects.rsalone. A second setwill not cost half as much per card; it will cost more, because the phrasings it needs are the
ones that were never worth a variant the first time.
exclusions.jsonnames at least three limits that are limits of this bundling rather than ofthe rules — "Shuffling anything but the effect's own source into a library", "Gaining
control of a permanent for longer than a turn", and "Playing a card from a zone other than
the hand, the command zone, or a permitted graveyard" — each of which is a bundled verb's
missing half.
Scope
Split each bundled verb along its own seams, keeping behaviour identical. At minimum:
sacrifice_chosen_per_playerinto a target-group shapeany effect can declare, so the Vaevictis trigger becomes a sacrifice plus a reveal-and-put
rather than one verb.
exiled this way, bottom the rest in a random order — with the bottoming visible in the
authored JSON rather than hidden inside the cast verb.
exile_top_for_playandmay_cast_exiled_this_waybecome.subject (permanent or player) as a field.
stated where it lives, or gains a field that makes it reusable.
shuffle_self_into_libraryand
exchange_controlin particular are one destination and one duration away from thegeneral cases
exclusions.jsoncurrently names.Behaviour-preserving throughout. No card's rules change.
Acceptance criteria
per-seat target group is declarable by any effect.
Effect::target_groupshas no effect-specific special case; the seat-count arity is aproperty of a target group, not of one verb.
JSON.
kindeither has a stated reason for being singular, next toits definition, or has been given the field that makes it general.
Effectvariant count is lower than it is today, and the reduction is stated inthe PR.
exclusions.jsonentry naming a bundled verb's missing half is dropped ornarrowed, with a card or a test proving the capability.
make compatregenerated; supported and excluded stay disjoint.make verifygreen.Test and validation evidence
in intent over re-authored JSON.
reaches a decomposed half the original card did not need.
three clauses in a list. This is the visible risk and wants a snapshot.
make verify.Dependencies and sequencing
Independent of #824–#827 in subject matter, but overlapping in files — sequence rather than
parallelise. Best done after them, since the collapsed selector and payload shapes are what
several of these verbs would decompose into. Must land before another set is authored, which
is the whole point.
Estimated scope
Large — broad change requiring a split assessment. It splits cleanly per verb if it needs to:
sacrifice_chosen_per_playeralone is a coherent PR, and so is the Chaos-Wand/Djinn pair. Takeit verb by verb with the suite green at each step rather than as one landing.
Risks and constraints
effects render three. The server's formatter must recompose them, or players read a card
differently than it is printed — and
AGENTS.mdmakes playing the merge criterion.happens after the may-cast; making it a separate effect makes that ordering authored rather
than implied, which is an improvement only if the authored order is right.
trades one variant for three and is a net loss; the "stated reason for being singular"
criterion exists so that judgment is recorded rather than re-litigated.
Non-goals
Not the selector unification (#824, #825) or the verb matrix (#826, #827). Not adding
mechanics no bundled verb already contains. Not authoring a second set.
Documentation impact
docs/card-schema.md— "How far the vocabulary reaches" enumerates these verbs and is rewrittenaround the decomposed ones.
exclusions.json— entries dropped or narrowed as capabilities land.crates/sage-engine/AGENTS.md— "The IR is the constraint" section may want a sentence onpreferring composition to a new verb, which is the lesson this issue encodes.