DSL Primitive Reference
TurnZero card definitions
Engine docs

Engine Primitives And DSL Guide

This is the card-definition DSL reference for TurnZero. It is meant to be useful both as human documentation and as context for LLM-assisted card imports.

The engine owns legality. Card definitions describe costs, effects, triggers, abilities, roles, and intentionally unsupported text. The pilot chooses from legal actions exposed by the engine.

effects triggeredAbilities activatedAbilities manaAbilities turn hooks

Goldfish Scope And Unsupported Behavior

TurnZero models the goldfish player's game plan, not a complete multiplayer game. Card definitions should preserve behavior that changes the player's available actions, resources, permanents, triggers, or useful metrics. They should not add speculative opponent state merely to reproduce every Oracle clause.

Interaction

  • Pure reactive interaction, such as a counterspell with no independently useful goldfish effect, is normally held in hand as an unsupported Interaction card. Do not cast it just to spend mana.
  • Targeted removal may use an abstract opponent permanent when the existing target and interaction machinery supports that approximation. The engine gives that virtual permanent the card type required by the declared target filter, including artifact, battle, creature, enchantment, land, and planeswalker. It does not become a persistent battlefield object unless an effect needs to materialize it for a supported consequence.
  • Board wipes and other global interaction affect only permanents represented on TurnZero's tracked battlefield. This commonly means they remove the goldfish player's permanents while the untracked opponent board remains unsupported.
  • A spell with independently useful self-side behavior should model that behavior and record only the opponent-dependent remainder as unsupported.

Opponent State And Observable Events

TurnZero does not persist complete opponent life totals, hands, libraries, graveyards, or battlefield states. Use documented assumptions or estimates only where the goldfish decision needs them; do not invent concrete opponent objects or update nonexistent zones.

Lack of persistent opponent state does not mean every opponent action is invisible. When one of the goldfish player's cards can react to an action or outcome, prefer an existing abstract event with the relevant player, opponent, card, and amount information. Opponent draws, casts, searches, land entries, attacks, damage, life gain, and life loss are useful only to the extent that they can affect the tracked player's cards or metrics.

If an opponent-only consequence has no supported downstream goldfish effect, record it precisely in unsupported and stop. Do not propose a new engine or public DSL API solely to mutate untracked opponent state. A generic engine event or estimate is warranted when the omitted fact can trigger or modify supported self-side behavior; that shared capability must be implemented in the engine rather than hidden in one card definition.

Opponent Model

game.opponentModel holds every rate TurnZero assumes about how opponents play: extra land drops, extra draws, abstract discard categories, spells per turn and their type mix, the estimated spell's mana value, creature deaths, library searches, tax payment, attacking creatures, tapped-land share, and revealed-card mana value. The default is defaultOpponentModel in src/engine/opponentModel.ts. A run can pass its own opponentModel to createGame, simulateGame, or runSimulations.

Cards never carry these rates. If two cards could need to agree on a fact, it belongs in the opponent model; a card that watches opponent casts declares only its trigger. Card assumptions hold answers to a question only that card asks, such as how opponents vote on its vote.

pilotSeat sets turn order. The default, LAST, has every opponent take turn N before the pilot's turn N: catch-up ramp only works from behind, and going first rarely helps a goldfish. The opponent turns between the pilot's turns N and N+1 therefore read the model's schedules at turn N+1, and each opponent starts the game with one land from their first turn.

game.opponentBoards records what those turns put onto each opponent's battlefield: lands from land entries, and artifacts, creatures, enchantments, planeswalkers and battles from resolved opponent spells. Opponent land counts, OPPONENT_CONTROLS_MORE_LANDS_THAN_YOU, the tapped-land estimate, and estimatedOpponentPermanents destruction read these boards rather than a formula, so every card sees the same opponents. The ambient opponent creature death takes a creature from the first board that has one, and does not happen while no opponent controls a creature.

Some effects promote an always-available typed virtual opponent permanent to a tracked battlefield object. The object has an opponent owner and controller, but it is not added to game.opponentBoards; doing both would count the same assumed permanent twice. Targeted destruction and exile materialize the type that satisfied the declared target filter, so downstream card-movement and leaves-graveyard behavior sees the correct kind of card. An Aura that resolves on the virtual permanent uses a fresh tracked object and retains its attachment to that exact object. The Aura remains controlled by the goldfish player, so controlled-enchantment counts and cast or entry triggers work normally. The generic virtual permanent remains available for later targets.

Opponent rolls use game.opponentRng, a stream separate from the pilot's game.rng. A seeded game derives it from <seed>:opponents. Different lines of play still shuffle the library differently, but they no longer change what opponents do. Opponent steps roll every turn whether or not a card listens.

Abstract opponent discards sample one of three mutually exclusive categories from opponentModel.discards.categoryWeights: creature, land, or noncreature/nonland. The default weights are 0.27, 0.36, and 0.37. These are the rounded aggregate card shares in TurnZero's ten production 100-card Commander simulation decks, excluding toy and generic shell decks. A run can replace all three weights through its opponent-model override. Each abstract discard consumes one roll from game.opponentRng and exposes the sampled type to ordinary card filters.

Planning And Escalation

An unsupported Oracle clause is not itself a reason for strong-model review. A definition remains routine when its useful goldfish behavior uses existing DSL and its remaining opponent-dependent behavior is documented honestly. Escalate only when supported self-side behavior needs a missing shared primitive, current engine semantics are incorrect, or the omission could change a goldfish decision or trigger.

For example, Swords to Plowshares uses EXILE_PERMANENT against an abstract opponent creature. Its controller's life total is not tracked. The current definition records that life gain as unsupported rather than introducing an opponent-life-total API.

Core Shape

Most behavior starts in a CardDefinition.

{
  name: "Harmonize",
  types: ["Sorcery"],
  manaCost: { generic: 2, green: 2 },
  roles: ["Draw"],
  effects: [
    { type: "DRAW_CARDS", amount: 3 }
  ]
}

Harmonize

Important fields:

  • types, subtypes, keywords, power, and toughness describe the modeled card face. Parameterless keywords are strings; parameterized keywords are objects in the same array. Keywords can also carry modeled rules, such as Backup, Buyback, Convoke, Cycling, Dash, Enlist, Gravestorm, Landcycling, Madness, Myriad, Plot, Warp, and Firebending.
  • Card color is normally derived from manaCost. Use colors only for a printed color indicator or another characteristic that overrides that derivation. colorIdentity is commander-deck metadata and does not make a card colored; a land can have a colored identity while remaining colorless.
  • power and toughness accept either a fixed number or an EffectAmount for a characteristic-defining value that is recalculated in every zone. Dynamic printed stats resolve before base-stat settings, counters, and continuous modifiers.
  • manaCost, alternateManaCosts, and manaCostReduction describe casting costs. Ordinary mana payment is implicit.
  • additionalCosts accepts ordinary Cost entries plus chooseOne clauses. Ordinary entries are all mandatory; a chooseOne clause requires exactly one named cast-time payment option.
  • manaAbilities is the canonical API for non-stack mana abilities.
  • asEnters is the canonical API for choices and copy modifications described by Oracle text as a permanent enters.
  • asTurnedFaceUp contains immediate transition effects performed while a permanent is turning face up. These effects are not triggered abilities and do not use the stack.
  • entersWithCounters describes counters the permanent's own rules give it as it enters, including X-derived amounts.
  • manaProduction is deprecated syntactic sugar for simple tap mana abilities.
  • effects is for spell resolution.
  • pregameAbilities contains effects a card may apply from the kept opening hand before the game begins. Each effect resolves once with that card as SOURCE; choosing BEGIN_GAME declines any remaining pregame abilities.
  • activatedAbilities is for abilities with costs.
  • triggeredAbilities is for event-driven abilities.
  • staticAbilities is for continuous battlefield state.
  • roles is used by the pilot and metrics.
  • assumptions makes any goldfish simulation policy explicit and separate from the card's rules effects.
  • unsupported records text intentionally not modeled yet.

As This Enters

Use the card-level asEnters array for Oracle text beginning "As this enters" or "This enters as/with" when the instruction modifies how the permanent enters. These instructions are neither spell-resolution effects nor ENTERS triggered abilities: they happen before the card is on the battlefield and do not use the stack.

The entry sequence is:

  1. Resolve every asEnters choice for every permanent in the entering batch.
  2. Apply intrinsic entry state such as entersWithCounters, loyalty, tapped state, and summoning sickness.
  3. Put the complete batch onto the battlefield simultaneously.
  4. Emit counter-placement events, then ENTERS events.

Because all asEnters choices finish before step 3, permanents in the same entering batch cannot see or copy one another. An as-enters copy can add more asEnters instructions from the copied definition; those instructions are also completed before entry. entersWithCounters remains convenient card-level syntax for intrinsic counters, but it belongs to this same entry timing rather than being an ENTERS trigger.

asEnters also accepts ordinary Effect values. They resolve in array order against a persistent context whose SOURCE is the pending entry. If an ordinary effect creates a normal card or other effect choice, that choice pauses the same pending-entry queue and resumes it with the same context after the choice settles. This keeps choice legality in the usual primitive while letting later as-enters effects inspect refs recorded by earlier ones.

Choose a creature type and reuse it from later abilities:

{
  asEnters: [
    {
      type: "CHOOSE_CREATURE_TYPE",
      id: "kindred-discovery-creature-type"
    }
  ],
  triggeredAbilities: [
    {
      trigger: {
        type: "ENTERS",
        to: "battlefield",
        filter: {
          controller: "SELF",
          types: ["Creature"],
          subtypes: [{ ref: "kindred-discovery-creature-type" }]
        }
      },
      effects: [{ type: "DRAW_CARDS", amount: 1 }]
    }
  ]
}

The id stores the choice on that card instance. A CardFilter.subtypes entry of { ref: id } resolves it against the ability source, so each copy or re-entry has its own choice. Choice references are cleared when the card leaves the battlefield.

An as-enters creature-type choice can apply immediate characteristic grants after the choice but before the permanent enters. Subtype references resolve against the entering source's stored choice:

asEnters: [
  {
    type: "CHOOSE_CREATURE_TYPE",
    id: "chosen-creature-type",
    afterChoiceEffects: [ /* New API */
      {
        type: "GRANT",
        kind: "characteristics",
        target: "SOURCE",
        operation: "ADD",
        subtypes: [{ ref: "chosen-creature-type" }] /* Widened API */
      }
    ]
  }
]

These follow-up effects are limited to characteristic grants. Use ADD for "in addition to its other types"; SET replaces the complete subtype field.

Use the same API for permanents that copy something as they enter:

asEnters: [
  {
    type: "COPY",
    mode: "BECOME_COPY",
    applyTo: "SOURCE",
    choice: {
      id: "permanent-to-copy",
      zone: "battlefield",
      controller: "self",
      excludeSource: true,
      optional: true,
      filter: {
        anyOf: [
          { types: ["Creature"] },
          { types: ["Planeswalker"] }
        ]
      }
    },
    overrides: {
      effects: [
        {
          type: "PUT_COUNTER",
          target: "SOURCE",
          counter: { type: "+1/+1", amount: 1 }
        }
      ]
    }
  }
]

choice uses the normal battlefield target shape to expose legal options to the pilot. overrides.effects run after the copy is applied but before the permanent enters, allowing the copied characteristics to be modified in the same timing window.

Dynamic Mana-Cost Reductions

manaCostReduction.generic accepts a count and multiplier when each counted value reduces the generic cost by the same amount:

manaCostReduction: {
  generic: {
    count: {
      type: "UNIQUE",
      zone: "graveyard",
      filter: {
        anyTypes: ["Instant", "Sorcery"]
      },
      attribute: "MANA_VALUE"
    },
    multiplier: 2
  }
}

This example counts the different mana values among instant and sorcery cards in the graveyard, then reduces the spell's generic cost by twice that count. The count wrapper keeps the aggregate operation distinct from the amount applied per counted value.

A flat amount reduces the generic cost while every present gate holds. condition.filter matches any permanent on the battlefield, when (/* New API */) is a CountCondition evaluated with the spell as its source, and turnContext (/* New API */) is "OWN_TURN" or "OPPONENT_TURN". Any of the three may be omitted; when several are present all of them must hold:

manaCostReduction: {
  generic: {
    amount: 2,
    when: {
      count: { source: "SPELLS_CAST_THIS_TURN", player: "SELF" },
      comparison: "AT_LEAST",
      value: 1
    },
    turnContext: "OWN_TURN"
  }
}

Cost modifiers

COST_MODIFIER changes the generic portion of matching spell or activated ability costs. appliesTo.action selects CAST_SPELL or ACTIVATE_ABILITY. The latter covers both ordinary activated abilities and mana abilities. The filter matches the spell card for a cast or the ability's source permanent for an activation.

Omitting appliesTo.player, or setting it to "SELF", applies the modifier only when the cost subject and modifier source have the same controller. Use player: "ANY" for global effects that apply to every player's matching costs:

staticAbilities: [{
  type: "COST_MODIFIER",
  appliesTo: {
    action: "CAST_SPELL",
    player: "ANY",
    filter: { not: { types: ["Creature"] } }
  },
  increase: { generic: 1 }
}]

Set appliesTo.sourceZones to limit where the spell or ability source may be. For example, a graveyard-only spell reduction uses sourceZones: ["graveyard"]; the same card cast from hand does not receive it. An activated-ability modifier can use sourceZones: ["battlefield"] to exclude abilities on cards in hand or graveyards.

A modifier specifies exactly one of increase or reduction. The tracked cost is assembled as its selected main or alternate cost plus mandatory additional mana, then generic increases are applied before generic reductions. The final generic amount cannot be negative. A reduction may set minimumTotalMana; that specific reduction stops when the whole mana cost reaches the stated amount. Other unrestricted reductions can continue.

reduction.generic accepts any EffectValue (/* Widened API */), resolved with the modifier's permanent as its source each time a cost is computed, so a discount can scale with counters on the source or with permanents you control. A value that resolves below zero reduces by nothing:

reduction: {
  generic: { target: "SELF", counters: "+1/+1" }
}

These modifiers do not change a spell's mana value, and mana-spent records contain only mana actually paid. The filter is evaluated with the originating permanent as its source, so source-owned choiceRefs can be referenced with { ref: "choice-id" }.

A modifier may carry a condition (/* New API */), a CountCondition evaluated with the modifier's permanent as its source, and a turnContext (/* New API */) of "OWN_TURN" or "OPPONENT_TURN". Every present member must hold for the modifier to apply. The spell being paid for is not yet in SPELLS_CAST_THIS_TURN when its cost is computed, so an ordinal discount such as "the first artifact spell you cast each turn costs {1} less" composes from the existing history without a separate per-turn budget:

staticAbilities: [{
  type: "COST_MODIFIER",
  condition: { /* New API */
    count: {
      source: "SPELLS_CAST_THIS_TURN",
      player: "SELF",
      filter: { types: ["Artifact"] }
    },
    comparison: "EQUAL",
    value: 0
  },
  appliesTo: {
    action: "CAST_SPELL",
    player: "SELF",
    filter: { types: ["Artifact"] }
  },
  reduction: { generic: 1 }
}]

A count of exactly one selects the second matching spell each turn instead.

This ordering also applies to granted and prepared casts. Casting without paying the mana cost starts with no main mana cost, but still owes mandatory additional mana and matching increases. A modifier stops applying as soon as its source leaves the battlefield. Estimated opponent casts can trigger abilities, but the goldfish opponent abstraction does not model their mana pools, cost payment, or casting legality.

Alternate Mana Costs

alternateManaCosts adds a different way to cast a card. Existing shorthand mana-cost entries remain valid. Use the wrapper form when the alternate cost has a condition, distinct sourceZones, resolution destination, resolution effects, or explicitly lets the player cast without paying the mana cost.

Source-zone availability and observation

sourceZones is plural and appears at the top level of an action-producing definition, such as an activated ability, alternate-cost option, or named spell. It is prospective structural metadata: the engine compares the source card's current zone with this list while generating and validating legal actions. An explicitly listed nonstandard cast zone grants the intrinsic permission represented by that cast option.

condition.sourceZone is singular and appears on a resolving effect or post-resolution effect. It is retrospective runtime context: the engine records the zone from which a spell was actually cast, then the condition observes that fact after the action has happened. It never grants permission or causes an action to be offered.

The names intentionally share sourceZone because both refer to the action's source zone. The plural top-level field describes the set of allowed origins; the singular condition compares the one origin recorded for a particular action.

First-class casting mechanics compile to these generic routes internally. For example, author Flashback as a structured keyword:

keywords: [
  {
    type: "Flashback",
    cost: { mana: { generic: 5, red: 1 } }
  }
]

The engine supplies the graveyard permission, route identity, and exile destination. Use alternateManaCosts directly for casting routes that do not have a first-class mechanic. sourceZones limits where such a generic cost can be selected and supplies its intrinsic cast permission. A separate condition can be used at the same time for a dynamic count predicate. resolutionDestination changes only casts using that entry. An optional effects array replaces the card's usual resolution effects for that cast. Target generation and cast validation use the replacement effects too. A route whose replacement effects have no target is targetless even when the card's usual effects target, while a replacement effect that introduces a target must choose that target while casting. An optional entersWithCounters array belongs to that cast route. When its permanent spell resolves, those counters enter through the ordinary intrinsic entry pipeline, including counter replacement and modification effects before ENTERS triggers. Normal casts and spell copies do not inherit the route's entry counters because copies do not retain alternateCostId.

Use cost: "FREE" for "without paying its mana cost." This skips only the spell's main mana cost: mandatory additional costs and matching cost increases are still paid, the cast records only mana actually paid, and the card retains its printed mana value. A cast already instructed to skip its mana cost cannot also select an intrinsic alternate mana cost.

Alternate costs can use the shared count-condition shape. Flawless Maneuver checks for at least one self-controlled commander on the battlefield. Commander status is an instance designation represented by isCommander, not a keyword:

alternateManaCosts: [
  {
    id: "commander-free",
    cost: "FREE",
    condition: {
      count: {
        findCards: {
          zone: "battlefield",
          filter: {
            controller: "SELF",
            isCommander: true
          }
        }
      },
      minimum: 1
    }
  }
]

The condition is checked while legal cast actions are generated and rechecked when the spell is cast. The printed manaCost remains a separate available route whenever it can be paid.

Colourless and Generic Mana

Generic costs and colourless mana are different concepts. Use generic only for a numeric cost such as {2}. Any kind of mana may pay that requirement. Use colourless for each {C} symbol in a spell or ability cost; only colourless mana can pay it:

manaCost: {
  generic: 5,
  colourless: 1
}

Mana production and floating mana never use generic, because generic is not a type of mana. A source that adds {C}{C} uses colourless:

manaAbilities: [{
  id: "tap-for-two-colourless",
  cost: { tap: true },
  mana: { colourless: 2 }
}]

Colourless mana may pay generic costs. Coloured mana, any_colour, grouped any_one_colour, and creature payments such as Convoke cannot pay a colourless requirement.

Hybrid Mana

Use hybrid for a mana symbol that can be paid with either of two colors. Each group names the two distinct colors printed on the symbol and the number of copies of that symbol:

manaCost: {
  hybrid: [
    { colours: ["green", "blue"], count: 2 }
  ]
}

The same grouped shape is available inside the shared Cost.mana API used by activated abilities, additional costs, granted alternate costs, and PAY_COST:

cost: {
  mana: {
    hybrid: [
      { colours: ["white", "black"], count: 1 } /* New API */
    ]
  }
}

Each hybrid symbol costs one mana of either listed color. Legal-action generation checks the complete cost, and the payment engine chooses a concrete payable color from available mana sources. Generic, colorless, and unlisted colored mana cannot pay a hybrid symbol.

Phyrexian Mana

Use one phyrexian count per color in a spell or effect mana cost. The count is the number of Phyrexian symbols, not a preselected payment route:

manaCost: {
  phyrexian: { red: 1 } /* New API */
}

cost: {
  mana: {
    generic: 1,
    phyrexian: { white: 2 } /* New API */
  }
}

Each symbol contributes one to mana value and contributes its color to the card's colors. Legal-action generation derives every payable allocation from the single authored cost. A cast or activation action records the selected allocation as phyrexianLifePayments, where each selected symbol costs two life and every unselected symbol requires one mana of its color:

{
  type: "ACTIVATE_ABILITY",
  sourceId: "mondrak",
  abilityId: "put-indestructible-counter",
  phyrexianLifePayments: { white: 1 } /* New API */
}

For a hybrid Phyrexian symbol such as {R/W/P}, put phyrexian: true on the hybrid group. Each symbol may be paid with either listed color or two life:

manaCost: {
  hybrid: [{
    colours: ["red", "white"],
    count: 1,
    phyrexian: true /* New API */
  }]
}

Hybrid life allocations are recorded in phyrexianLifePayments.hybrid with the authored colors and number of symbols paid with life. A cast permanent with keywords: ["Compleated"] enters with two fewer starting loyalty counters when at least one of those hybrid symbols was paid with life. The payment snapshot belongs to the original spell; free casts, direct battlefield entry, and spell copies do not inherit it. If Compleated and another entry counter replacement produce different totals, the engine exposes both legal orders and the default pilot chooses the greater loyalty result.

For two white Phyrexian symbols, the engine can therefore expose { white: 0 }, { white: 1 }, and { white: 2 } when all three allocations are legal. Life and mana legality remain engine-owned. Paying exactly the remaining life is rules-legal; the default pilot avoids voluntarily choosing a payment that would leave it at zero.

Cost Payment Options

Use a GRANT with kind: "cost payment option" when a permanent changes how an ordinary mana symbol may be paid. The grant is reusable across spell, activated-ability, and mana-ability costs:

staticAbilities: [{
  type: "GRANT",
  kind: "cost payment option",
  appliesTo: {
    payer: "SELF",
    costKinds: ["SPELL", "ACTIVATED_ABILITY", "MANA_ABILITY"]
  },
  match: { manaSymbol: "black" },
  alternative: { loseLife: { amount: 2 } },
  repeat: "EACH_MATCH"
}]

An optional appliesTo.filter narrows the card or permanent whose cost is being paid. For example, a Defiler-style permanent-spell rule can use costKinds: ["SPELL"], filter: { types: ["Artifact"] }, and repeat: "ONCE" to offer one substitution for each matching artifact spell.

The engine finalizes a mana cost in this order: choose its normal or alternate base cost, add additional costs, apply increases, apply reductions, enforce a minimum when one applies, then create payment options. EACH_MATCH creates an independent ordinary-mana-or-two-life option for every matching ordinary symbol. ONCE creates one option per granting source. A converted symbol is no longer available to another identical grant, so duplicate grants do not duplicate the same option.

The converted options use the existing phyrexianLifePayments action field. Printed Phyrexian symbols, mana value, card colors, and color identity remain unchanged. Whole-cost alternatives expressed with Cost.loseLife remain a separate cost stage and can coexist with these per-symbol options. The grant works only while its source is on the battlefield.

Changeling

Record Changeling in the ordinary keyword list:

keywords: ["Changeling"] /* New API */

A card with Changeling matches every creature subtype supported by the card catalog. It does not gain noncreature subtypes such as Aura, Equipment, Forest, Saga, or Treasure. Ordinary continuous characteristic changes still apply after this base characteristic, so a later subtype-setting effect can replace the Changeling-derived list.

Resolution-Time Creature-Type Choices

Use a CHOOSE_CREATURE_TYPE effect when a resolving spell or ability instructs its controller to choose a creature type. Give the choice an id, then refer to that value from later effects in the same resolution:

effects: [
  {
    type: "CHOOSE_CREATURE_TYPE",
    id: "chosen-creature-type"
  },
  {
    type: "DRAW_CARDS",
    count: {
      findCards: {
        zone: "battlefield",
        filter: {
          controller: "SELF",
          subtypes: [{ ref: "chosen-creature-type" }]
        }
      }
    }
  }
]

This is distinct from asEnters. A spell or ability choice is stored only in its current resolution context, while an asEnters choice is stored on the entering permanent for later abilities of that permanent.

Effect Values

EffectValue is the shared numeric result used by effect quantities. EffectAmount is the authoring wrapper used by effects whose quantity is an amount. It separates four concepts:

  • Literal numbers such as 1.
  • EffectCount queries that count or aggregate game objects and history.
  • ContextValue expressions that read one recorded or derived scalar.
  • Arithmetic expressions that transform another EffectValue.
  • Assumption-backed values that sample an explicit documented simulation policy.

Context values use structured categories rather than count-shaped sentinels:

{ event: { attribute: "DAMAGE_AMOUNT" } }
{ event: { attribute: "COUNTER_AMOUNT" } } /* New API */
{ event: { attribute: "LIFE_GAINED_AMOUNT" } }
{ event: { attribute: "LIFE_LOST_AMOUNT" } }
{ event: { attribute: "MANA_SPENT" } }
{ event: { attribute: "MANA_VALUE" } } /* New API */
{ source: { attribute: "POWER" } }
{ source: { attribute: "TOUGHNESS" } } /* Widened API */
{ source: { attribute: "MANA_VALUE" } } /* New API */
{ target: { attribute: "POWER" } } /* New API */
{ result: { ref: "damage-result", attribute: "DAMAGE_DEALT" } }
{ variable: "X" }
{ variable: "X", offset: 1 }
{ game: { attribute: "SPEED" } }
{ estimate: { attribute: "OPPONENT_REVEALED_CARD_MANA_VALUE" } }
{ assumption: { opponentCount: "opponents-not-sacrificing" } } /* New API */

An opponent-count assumption samples its weighted outcome from the opponent stream when the resolving ability first reads it, caches that value for the rest of that resolution, and caps it at game.simulatedOpponentCount. Pilot scoring uses the weighted expected value without consuming RNG, so the same card model can remain deterministic while the engine resolves an explicit simulation sample.

The old EVENT_DAMAGE_AMOUNT, EVENT_LIFE_GAINED_AMOUNT, SOURCE_POWER, X, SPEED, opponent-revealed-mana-value, and turn-based-estimate source sentinels are deprecated compatibility shapes. New definitions must use ContextValue.

Arithmetic numeric results

FLOOR_DIVIDE divides a nested EffectValue by a positive integer and rounds the result down. MULTIPLY multiplies a nested value by a numeric factor, including a negative factor for effects such as -X/-X. Arithmetic values are recursive, so the nested value may be a context value, count, conditional result, or another arithmetic result:

amount: {
  value: { /* New API */
    operation: "FLOOR_DIVIDE",
    value: { variable: "X" },
    divisor: 2
  }
}

power: {
  value: { /* New API */
    operation: "MULTIPLY",
    value: { variable: "X" },
    multiplier: -1
  }
}

The divisor must be a positive integer. Invalid authored divisors fail resolution rather than producing an infinite or non-numeric effect quantity.

For a spell-cast trigger, { event: { attribute: "MANA_VALUE" } } reads the triggering spell's mana value from its printed mana cost and includes the chosen value of X. It does not use the amount of mana actually paid, so cost reductions, alternate costs, and free casts do not change the result.

For a leave-or-dies trigger, { source: { attribute: "POWER" } } uses the source permanent's last-known battlefield power, including counters and continuous or temporary modifiers that applied immediately before it left. { source: { attribute: "TOUGHNESS" } } reads current toughness the same way and uses last-known toughness after the source leaves. { source: { attribute: "MANA_VALUE" } } reads the source card's current mana value. When a static alternate-cost grant is attached to another card, that recipient card is the cost source. { source: { attribute: "IS_ATTACHED" } } returns one while the contextual source permanent is attached to another permanent and zero otherwise. It is a numeric source-state value so continuous permissions can compose it with the normal count comparison API.

{ target: { attribute: "POWER" } } reads the current power of the selected battlefield target. Temporary MODIFY_STATS effects evaluate this value before adding their own modifier, so using the value as a positive power modifier doubles that target's power at that moment.

Where values are used

Effects use them for quantities. For example, draw a card for each matching creature:

{
  type: "DRAW_CARDS",
  amount: {
    count: {
      findCards: {
        zone: "battlefield",
        filter: { types: ["Creature"], controller: "SELF" }
      }
    }
  }
}

DRAW_CARDS may be optional. The engine exposes accept and decline actions to the pilot and continues with later effects after either choice:

{
  type: "DRAW_CARDS",
  amount: 1,
  optional: true
}

An optional draw may instead limit accepted executions for the current turn. Set matchingCountThisTurn on the draw effect itself, together with optional: true and an id:

{
  type: "DRAW_CARDS",
  id: "once-per-turn-draw",
  amount: 1,
  optional: true,
  matchingCountThisTurn: 1
}

This count belongs to the current source zone object and this exact effect ID. Accepting consumes one use before drawing. Declining does not consume a use. Once the limit is reached, later resolutions skip the effect without opening a draw choice. Other draw effects, another source permanent, a returned source object, and the next turn have independent availability.

The same API supplies amount fields such as counters, life, and damage:

{
  type: "PUT_COUNTER",
  target: "SOURCE",
  counter: {
    type: "+1/+1",
    amount: { event: { attribute: "DAMAGE_AMOUNT" } }
  }
}

Mana abilities distinguish a genuine count from a contextual scalar:

mana: {
  amount: {
    count: { target: "SELF", counters: "+1/+1" }
  },
  colour: ["green"]
}
mana: {
  amount: {
    value: { source: { attribute: "POWER" } }
  },
  colour: ["ANY_ONE_COLOUR"]
}

Counts also drive numeric conditions:

condition: {
  count: { target: "SELF", counters: "+1/+1" },
  comparison: "EQUAL",
  value: 0
}

And they can be the numeric right operand of an event-card comparison:

condition: {
  match: [{
    left: "EVENT_CARD",
    comparison: "GREATER_THAN",
    right: {
      count: {
        type: "MAX",
        attribute: "POWER",
        zone: "battlefield",
        exclude: "EVENT_CARD"
      }
    },
    attribute: "POWER"
  }]
}

For an amount field, use a literal directly, wrap a genuine count under { count }, and wrap a contextual scalar under { value }. Other numeric consumers such as search limits and conditions continue to use their declared EffectValue or EffectCount fields.

Count API

EffectCount is reserved for genuine counts and aggregations: matching cards, counter totals, distinct values, sums, maxima, devotion, spell history, target counts, and similar queries. A future historical query such as total damage dealt belongs here; the amount recorded by one damage event is a ContextValue.

A named effect can expose a collection of numeric results. MAX and SUM reduce the complete collection without separately restating which players the effect affected:

{
  type: "MAX",
  values: {
    result: {
      ref: "discard-hands",
      attribute: "MOVED_CARD_COUNT"
    }
  }
}

MOVED_CARD_COUNT returns one value for every player affected by the named player-scoped MOVE_CARD. LIFE_LOST returns one value for every player affected by the named LOSE_LIFE effect. An absent result or an empty collection resolves to zero.

Literal and filtered counts

A literal number is used directly:

count: 3

An object count explains how to calculate the number. For simple card/permanent counts, use findCards with a zone and filter:

{
  findCards: {
    zone: "battlefield",
    filter: {
      types: ["Creature"],
      tapped: true,
      controller: "SELF"
    }
  }
}

PARTY_SIZE counts the controller's current party across creatures on the battlefield:

{ source: "PARTY_SIZE" } /* New API */

The four party roles are Cleric, Rogue, Warrior, and Wizard. Each creature can fill at most one role, and the engine chooses the assignment that produces the largest party. Effective subtypes apply, including granted subtypes and Changeling. The result is capped at four.

That object means "count matching battlefield permanents." This is the shape for Harvest Season's "number of tapped creatures you control":

count: {
  findCards: {
    zone: "battlefield",
    filter: {
      types: ["Creature"],
      tapped: true,
      controller: "SELF"
    }
  }
}

Contextual counts

Some counts come from existing game or turn history:

count: { source: "ATTACKING_CREATURES" }
count: {
  source: "ATTACKING_CREATURES", /* Widened API */
  defender: "EVENT_OPPONENT", /* New API */
  filter: { minPower: 4 }
}
count: { source: "PLAYERS_BEING_ATTACKED" } /* New API */
count: { source: "ATTACKED_CREATURES_THIS_TURN" } /* New API */
count: { source: "OPPONENT_COUNT" } /* New API */
count: { source: "SPELLS_CAST_THIS_TURN", player: "SELF" }
count: { source: "SPELLS_CAST_THIS_TURN", before: "SOURCE" } /* Widened API */
count: {
  source: "SPELLS_CAST_THIS_TURN",
  before: "TRIGGERING_SPELL"
} /* Widened API */
count: { source: "CARDS_DISCARDED_THIS_TURN", player: "SELF" } /* New API */
count: { source: "CARDS_DRAWN_THIS_TURN", player: "SELF" } /* New API */
count: { source: "LIFE_GAINED_THIS_TURN" } /* New API */
count: {
  source: "ZONE_CHANGES_THIS_TURN", /* New API */
  from: "battlefield",
  to: "graveyard",
  filter: { types: ["Artifact"] }
}
count: { source: "TARGET_CARD_COUNT" }
count: { source: "LIFE_TOTAL" } /* New API */
count: {
  source: "PLAYER_STATUS", /* New API */
  status: "ENDURING_STORY"
}
count: { source: "MANA_COLOURS_SPENT" }
count: {
  source: "ADDITIONAL_COST_PAID", /* New API */
  id: "bonus"
}
count: { source: "KICKER_COSTS_PAID" }

ADDITIONAL_COST_PAID returns 0 when the named choice is absent, 1 for an optional cost recorded as true, and the exact positive integer recorded for a repeatable cost. This lets a resolving spell or a source-self cast trigger use the same payment count without a separate payment event or count source.

LIFE_TOTAL reads the goldfish player's current life total. Compose it with a count-backed condition rather than introducing a threshold-specific condition:

condition: {
  count: { source: "LIFE_TOTAL" },
  comparison: "AT_LEAST",
  value: 30
}

PLAYER_STATUS returns one when the goldfish player has the named status and zero otherwise. INITIATIVE reads current ownership from game.hasInitiative, without storing a second copy in playerStatuses. CITYS_BLESSING, COMPLETED_DUNGEON, and ENDURING_STORY read persistent statuses. For example, an enduring-story payoff composes the status count with an ordinary comparison:

condition: {
  count: {
    source: "PLAYER_STATUS", /* New API */
    status: "ENDURING_STORY"
  },
  comparison: "AT_LEAST",
  value: 1
}

Attachment-dependent permissions use the same composition:

condition: {
  count: { source: { attribute: "IS_ATTACHED" } }, /* New API */
  comparison: "EQUAL",
  value: 1
}

OPPONENT_COUNT reads the number of opponents configured for the current game. It is useful for instructions that scale with “each opponent”:

amount: { source: "OPPONENT_COUNT" } /* New API */

An optional where comparison counts qualifying opponents individually. OPPONENT_LAND_COUNT reads the opponent being evaluated and requires this scope. LAND_COUNT continues to count our controlled lands within that scope. Each opponent's land count is the number of lands the simulated opponent turns have put onto that opponent's board (see Opponent Model), so it stays outside the card DSL.

count: {
  source: "OPPONENT_COUNT",
  where: { /* New API */
    count: { source: "OPPONENT_LAND_COUNT" }, /* New API */
    comparison: "AT_LEAST",
    value: { /* Widened API: comparison values accept EffectValue */
      operation: "ADD", /* New API */
      value: { source: "LAND_COUNT" }, /* New API */
      amount: 2
    }
  }
}

ADD sums two effect values. Expression-valued comparisons use the same resolver for effects, casting conditions, triggers, and player rules queries. Search counts are evaluated once when the search resolves; finding a land does not reduce the already established search maximum.

Other count sources include SOURCE_TRIGGER_COUNT, VOTE_COUNT, opponent hand or tapped-land estimates, hand size at resolution, and commander color-identity size.

PLAYERS_BEING_ATTACKED counts the distinct simulated opponents assigned as defenders in the current combat. Multiple creatures attacking the same player still count that player once. It returns zero outside a represented combat.

ATTACKING_CREATURES accepts an optional battlefield card filter. On an opponent-specific attack trigger, defender: "EVENT_OPPONENT" restricts the count to attackers assigned to that event's opponent; without that event context, the defender-filtered count is zero.

ATTACKED_CREATURES_THIS_TURN counts the distinct creatures declared as attackers during the current turn. It remains available after combat and does not count creatures that entered the battlefield already attacking.

ZONE_CHANGES_THIS_TURN counts recorded zone-change events matching optional source-zone, destination-zone, and card filters. It includes abstract opponent objects emitted by simulation assumptions, so effects can compose a real rules count from tracked and estimated movement without embedding a threshold or card-specific calculation.

Historical event counts use the engine's event vocabulary directly. An ungrouped count returns the number of matching events from the current turn:

count: {
  event: { /* New API */
    type: "ATTACK", /* New API */
    filter: { player: "SELF" }
  },
  scope: "THIS_TURN" /* New API */
}

ATTACK records the player-level event emitted once when one or more attackers are declared. It does not count individual ATTACKS events or creatures put onto the battlefield attacking. BEGIN_END_STEP, CAST_SPELL, DRAW_CARD, ENTERS, and PERMANENT_SACRIFICED also support direct ungrouped counts.

BEGIN_END_STEP is recorded before the engine queues triggers for that end step. Its optional filter.player distinguishes self and opponent end steps. The history resets at the start of each individual player's turn, so additional end steps on the same turn increase the count while the next player's first end step starts again at one.

PERMANENT_SACRIFICED counts permanents sacrificed this turn:

count: {
  event: {
    type: "PERMANENT_SACRIFICED", /* New API */
    filter: { player: "SELF", card: { subtypes: ["Food"] } }
  },
  scope: "THIS_TURN"
}

The record keeps the permanent's last-known characteristics, so a sacrificed token still matches its card filter after it ceases to exist. player is the player who sacrificed it: the permanent's controller when it left the battlefield. A permanent that was destroyed, exiled, or otherwise moved without being sacrificed is not counted.

CAST_SPELL counts spells cast this turn. It is the composed form of the SPELLS_CAST_THIS_TURN count source and reads the same history, so the two spellings always agree. Prefer the composed form:

count: {
  event: {
    type: "CAST_SPELL", /* New API */
    filter: { player: "SELF", card: { types: ["Creature"] } }
  },
  scope: "THIS_TURN"
}

filter.player is the caster and filter.card is an ordinary card filter applied to the cast card; a recorded cast without a card never matches a card filter. The spell being paid for is not yet recorded when its cost is computed, so "the first creature spell you cast each turn" is this count compared EQUAL to 0 rather than a separate per-turn budget.

An optional before narrows the count to casts recorded strictly before a boundary spell, which is how "each other spell you've cast before it this turn" excludes the spell itself:

count: {
  event: {
    type: "CAST_SPELL",
    before: "TRIGGERING_SPELL", /* New API */
    filter: { player: "SELF", card: { anyTypes: ["Instant", "Sorcery"] } }
  },
  scope: "THIS_TURN"
}

"TRIGGERING_SPELL" uses the spell that triggered the ability and "SOURCE" uses the ability's own source card. When the boundary spell is not in this turn's history the count is 0, not the whole turn.

An attack filter can select a remembered player and a player defender:

count: {
  event: {
    type: "ATTACK",
    filter: {
      player: { ref: "chosen-opponent" }, /* New API */
      defender: "SELF" /* New API */
    }
  },
  scope: "THIS_TURN"
}

player accepts SELF, OPPONENT, or a captured player ref. defender accepts SELF or a captured player ref. SELF is relative to the resolving ability's controller. The engine records exact player defenders on ATTACK events in defendingPlayers; attacking a planeswalker or battle does not add its controller or protector. Damage is irrelevant. An absent defender list is unspecified and cannot satisfy a defender filter. The goldfish opponent-turn driver supplies an empty list under its explicit assumption that opponents do not attack the pilot. Generic opponent attack listeners continue to fire.

Grouped historical counts group matching events by exact player identity, apply an ordinary count comparison to each group, and return the number of qualifying players:

count: {
  event: { /* New API */
    type: "DRAW_CARD",
    filter: { player: "OPPONENT" } /* New API */
  },
  scope: "THIS_TURN", /* New API */
  groupBy: "PLAYER", /* New API */
  condition: { /* New API */
    count: { source: "GROUP_SIZE" }, /* New API */
    comparison: "AT_LEAST",
    value: 2
  }
}

filter.player selects the player category while groupBy: "PLAYER" keeps the exact self or opponent identity. For ENTERS, the player is the entering card's controller and filter.card accepts a normal card filter. Historical counts include events emitted before the source card entered the battlefield. In a trigger condition, filter.player: "EVENT_PLAYER" matches self or the exact opponent identified by the current event rather than aggregating all opponents.

MANA_COLOURS_SPENT counts the distinct colors of mana recorded on the originating spell. Colorless mana does not contribute. Main and mandatory additional mana payments are combined, while mana paid to activate mana abilities and creatures tapped for Convoke are excluded. A free cast or spell copy has a count of zero. When a spell consumes this count, automatic payment maximizes distinct colors without overpaying.

Counter totals use a target and counter type:

count: {
  target: "SELF",
  counters: "+1/+1"
}

Use target: "PLAYER_SELF" to count counters on the goldfish player rather than a permanent. Named counters and the "any" wildcard use the same shape:

count: {
  target: "PLAYER_SELF", /* Widened API */
  counters: "experience"
}

Use target: "EVENT_CARD" inside a triggered ability to inspect the card that caused the current event. The event card is retained as last-known information when it has left the battlefield. Use counters: "any" to total every counter type rather than selecting one kind:

condition: {
  count: { target: "EVENT_CARD", counters: "any" },
  comparison: "AT_LEAST",
  value: 1
}

Without an event card in the current context, an event-card counter count is zero.

TARGET_CARD_COUNT reads the number of card targets declared for the resolving spell. Use it when an effect acts on every selected card, such as Seasons Past:

count: { source: "TARGET_CARD_COUNT" }

Spell-history counts are available for cards that care how many matching spells were cast this turn:

count: {
  source: "SPELLS_CAST_THIS_TURN",
  player: "SELF",
  filter: { anyTypes: ["Instant", "Sorcery"] }
}

The count reads the current turn's spell-cast history and can be used with an activated ability's minimum condition.

Set before: "SOURCE" when the count must stop immediately before the spell or ability source's own cast-history record. The source cast and any later casts are excluded. Omitting player counts both players' casts, which is the composable count used by storm-style cast triggers.

Set before: "TRIGGERING_SPELL" inside a cast-triggered ability when the ability's permanent source is not the spell that defines the history boundary. This excludes the triggering spell and every spell cast in response after it, even if those later spells exist in history before the ability resolves.

CARDS_DISCARDED_THIS_TURN reads emitted discard-event history. Each card in a multi-card discard contributes one, a card discarded more than once contributes each time, and a replacement that sends the discarded card to exile still counts. Later zone changes do not alter the history. The count is evaluated when the effect resolves, resets at the beginning of every player's turn, and accepts an optional filter parallel to spell history:

count: {
  source: "CARDS_DISCARDED_THIS_TURN",
  player: "SELF",
  filter: { types: ["Creature"] }
}

CARDS_DRAWN_THIS_TURN reads the number of cards the player has actually drawn during the current turn. It includes the normal draw for the turn and cards drawn earlier during the current effect sequence, but it does not count cards put into hand without being drawn:

count: {
  source: "CARDS_DRAWN_THIS_TURN",
  player: "SELF"
}

LIFE_GAINED_THIS_TURN (/* New API */) reads the total life you have gained during the current turn, after life-gain replacement effects. It is the same fact the LIFE_GAINED_THIS_TURN trigger condition checks, exposed as a count so cost reductions and effect conditions can compare it. It counts on your own turn and on opponents' turns, and the end-turn cleanup resets it at the end of every turn:

count: { source: "LIFE_GAINED_THIS_TURN" }

CONTROLLED_PERMANENTS still exists for current definitions, but new simple counts should use findCards.

VOTE_COUNT reads the result of a collected vote:

count: {
  source: "VOTE_COUNT",
  voteId: "council-vote",
  optionId: "past"
}

Aggregates

Aggregate counts calculate a value across matching cards. They use current characteristics, so counters and static modifiers are included.

MAX returns the greatest current attribute and returns zero for an empty set. Contextual exclusion can remove the event card before aggregation, which is useful for “greater than each other creature”:

count: {
  type: "MAX",
  attribute: "POWER",
  zone: "battlefield",
  filter: { types: ["Creature"] },
  exclude: "EVENT_CARD"
}

UNIQUE counts distinct values after applying an optional filter:

count: {
  type: "UNIQUE",
  zone: "battlefield",
  filter: { types: ["Creature"] },
  attribute: "POWER"
}

That shape means “count the number of different current powers among battlefield creatures.”

SUM adds matching numeric attributes together:

count: {
  type: "SUM",
  zone: "battlefield",
  filter: { types: ["Creature"] },
  attribute: "POWER"
}

That shape means "sum the current power of your battlefield creatures."

SUM can instead aggregate a group created by an earlier effect. Its source is a card reference, not a zone, so unrelated cards in the same zone are not included:

amount: {
  type: "SUM",
  attribute: "MANA_VALUE",
  source: { ref: "milled-cards" }
}

Referenced sums support POWER, TOUGHNESS, and MANA_VALUE. A reference may contain one card or several cards; SUM uses the same shape for both. Power and toughness use the referenced permanent's last-known battlefield values, including counters and continuous or temporary modifiers, when it has left the battlefield:

count: {
  type: "SUM",
  attribute: "TOUGHNESS",
  source: { ref: "sacrificed-creature" }
}

COUNT returns the number of cards in a group stored by an earlier effect. This is useful when a later effect needs the size of a chosen group:

amount: {
  count: {
    type: "COUNT",
    source: { ref: "discarded-cards" }
  }
}

An effect id also names that effect's occurrence on its source. Add scope: "THIS_TURN" to count how many times that named effect has been reached for the same source object during the current turn:

{
  id: "opponent-draw",
  type: "DRAW_CARDS",
  amount: 1,
  player: "OPPONENT"
},
{
  type: "DRAW_CARDS",
  amount: 2,
  condition: {
    count: {
      type: "COUNT",
      source: { ref: "opponent-draw" },
      scope: "THIS_TURN" /* New API */
    },
    comparison: "EQUAL",
    value: 2
  }
}

The occurrence is recorded once after the effect's condition and optionality permit it to resolve, before the effect executes. Its amount does not multiply the occurrence: one DRAW_CARDS effect that draws three cards still counts as one. Declined optional effects and effects whose conditions do not match do not count. Occurrences are scoped to the current source object, clear when that object changes zones, and reset at the beginning of each turn. Omitting scope preserves the resolution-local card-group reference behavior.

Useful UNIQUE attributes include:

  • CARD_TYPE for the different card types among matching cards. Multi-type cards contribute each type. TurnZero's in-scope set is Artifact, Battle, Creature, Enchantment, Instant, Kindred, Land, Planeswalker, and Sorcery; supertypes and subtypes do not contribute.
  • COLOR for the different Magic colors among matching cards. Colorless does not contribute a value, and a multicolored card contributes each of its colors.
  • POWER for current power among matching cards or permanents.
  • MANA_VALUE for different mana values among matching cards.
  • COUNTERS for different counter types across matching cards or permanents.

Examples:

count: {
  type: "UNIQUE",
  zone: "battlefield",
  attribute: "MANA_VALUE"
}

count: {
  type: "UNIQUE",
  zone: "battlefield",
  attribute: "COUNTERS"
}

Conditional numeric results

A count can select between two numeric results by matching another count-backed condition. This is still an EffectValue, so matched and default may themselves be literals or calculated counts:

count: {
  match: {
    count: { target: "SELF", counters: "+1/+1" },
    comparison: "AT_LEAST",
    value: 1
  },
  matched: 3,
  default: 1
}

Count-backed conditions

Count-backed conditions compare a resolved numeric expression against a minimum. The expression may be an aggregate count or a contextual value such as the mana spent on the triggering spell:

condition: {
  count: {
    type: "UNIQUE",
    zone: "battlefield",
    filter: { types: ["Creature"] },
    attribute: "POWER"
  },
  minimum: 3
}

That condition is met when you control creatures with three or more different powers.

condition: {
  count: {
    event: {
      attribute: "MANA_SPENT"
    }
  },
  comparison: "AT_LEAST",
  value: 5
}

Use comparison and value for explicit numeric comparisons, including zero:

condition: {
  count: { target: "SELF", counters: "+1/+1" },
  comparison: "EQUAL",
  value: 0
}

Supported comparisons are EQUAL, AT_LEAST, and LESS_THAN. The existing minimum form remains supported.

Filters

CardFilter restricts cards or permanents.

{
  anyTypes: ["Artifact", "Enchantment"]
}

Common fields include types, anyTypes, subtypes, anySubtypes, keywords, colorIdentity, colorsExactly, controller, owner, cardKinds, landKinds, name, names, isToken, isAttacking, isSource, drawnThisTurn, enteredBattlefieldThisTurn, legendary, modified, ringBearer, tapped, basePower, minManaValue, maxManaValue, hasVariableCost, sharesCreatureTypeWith, and match.

Use hasVariableCost: true (/* New API */) to match a card whose mana cost contains {X}, such as "spells you cast with {X} in their mana costs". It reads the card instance's current mana cost, so a copy carries the copied cost and a face-down card never matches.

Use isSource: true (/* New API */) to match the exact source object in the current filter context. It compares zone-object identity, not a card name or physical-card ID, so a card that leaves and returns no longer matches its previous existence. isSource: false matches every other object. It composes with outer filter fields, anyOf, and not.

Use sharesCreatureTypeWith: "SOURCE" to require at least one shared effective creature subtype between the filtered card and the resolving source. Use sharesCreatureTypeWith: "AFFECTED_CARD" (/* Widened API */) inside a dynamic continuous modifier to compare with the card currently receiving that modifier. Changeling and granted characteristic changes apply. Land, Equipment, Background, and other noncreature subtypes do not count.

colorsExactly (/* New API */) matches the card's current color set. An empty array matches a colorless card, including a card with a colored mana cost whose current characteristics make it colorless. This field does not inspect Commander color identity.

Use ringBearer: true (/* New API */) to match the current Ring-bearer. The bearer also matches legendary: true while it remains on the battlefield, even if its printed card definition is not legendary.

subtypes requires every listed subtype. Use anySubtypes when any one listed subtype is sufficient. For example, Sram's cast trigger matches any Aura, Equipment, or Vehicle spell:

{
  anySubtypes: ["Aura", "Equipment", "Vehicle" /* New API */]
}

Use manaValue for exact matching. Mana-value bounds are inclusive and accept either a literal or a contextual value such as X or spell MANA_SPENT:

{ manaValue: 2 } /* New API */
{ types: ["Creature"], maxManaValue: 3 }
{ types: ["Creature"], minManaValue: 4 } /* New API */

Use basePower for exact base-power matching. It includes characteristic- defining abilities and effects that set base power, but excludes counters, temporary additive modifiers, and static bonuses:

{ types: ["Creature"], basePower: 1 } /* New API */

Counter filters require a minimum total of one named counter type:

{
  types: ["Creature"],
  counters: { type: "+1/+1", minimum: 1 }
}

Counters of other types do not contribute. A card without the named counter has a total of zero. Counter filters compose with ordinary fields and recursive not in the same way as every other filter field.

Use type: "any" when the counter's identity does not matter. The minimum is then checked against the total of every counter kind on the card:

{
  counters: { type: "any", minimum: 1 }
}

Use modified: true (/* New API */) for rule 700.9's modified permanent: one that has at least one counter of any kind on it, or that has an Equipment or Aura attached to it that its own controller controls. An opponent's Aura attached to your creature does not make that creature modified for you, and an unattached Equipment modifies nothing. modified: false matches every other permanent. It composes with ordinary filter fields, anyOf, and not:

{ controller: "SELF", types: ["Creature"], modified: true } /* New API */

Use recursive not to negate the complete result of another filter. It composes with every filter field, including token status and subtypes:

{ types: ["Creature"], not: { subtypes: ["Human"] } }
{ types: ["Creature"], not: { isToken: true } }

Use controller: "SELF" for text like "you control." Use tapped: true or tapped: false when the tapped state is part of the condition.

Use owner: "SELF" for cards owned by the tracked player, including a card currently controlled by an opponent. Cards without an explicit owner are self-owned; abstract opponent cards carry an opponent owner.

Use enteredBattlefieldThisTurn: true for a card that must have entered the battlefield during the current turn. The filter matches by card instance and includes token and copy entries. It composes with ordinary characteristics, so Novijen-style queries can require both types: ["Creature"] and current-turn entry:

{
  zone: "battlefield",
  filter: {
    controller: "SELF",
    types: ["Creature"],
    enteredBattlefieldThisTurn: true
  }
}

Use drawnThisTurn: true to match a card currently in the queried zone that the tracked player actually drew during the current turn. The history uses exact zone-object identity, not name or physical-card ID: duplicate names do not match each other, and a card that leaves and later returns is a new object. It includes upkeep draws, the normal turn-based draw, and additional actual draws; a card no longer in the queried zone is naturally ineligible.

Use createdBy: "SOURCE" to match tokens created by the exact source object in the current effect context. The engine records that source identity on every token in an authored CREATE_TOKEN group, including tokens added by replacement effects. If the source leaves and returns, its new battlefield object does not match tokens created by its previous existence.

Use isAttacking: true for a permanent that must be in the current declared attacker set. Attribute comparisons reuse the same any-match vocabulary as trigger conditions. FILTER_CARD means the card currently being tested and may appear on either side of an attribute comparison:

{
  types: ["Creature"],
  isAttacking: true,
  match: [
    {
      left: "FILTER_CARD",
      comparison: "LESS_THAN",
      right: "SOURCE",
      attribute: "POWER"
    }
  ]
}

When the two card operands use different numeric attributes, use attributes instead of the same-attribute attribute field. Inside an event trigger filter, EVENT_CARD is the card carried by that event and SOURCE is the permanent whose ability is listening. For example, this matches a cast spell whose mana value is less than the source permanent's power:

{
  match: [{
    left: "EVENT_CARD",
    comparison: "LESS_THAN",
    right: "SOURCE",
    attributes: {
      left: "MANA_VALUE",
      right: "POWER"
    }
  }]
}

Same-attribute comparisons may use POWER or TOUGHNESS; mixed comparisons also support BASE_POWER and MANA_VALUE. Use POWER against BASE_POWER on the same FILTER_CARD to match a creature whose current power is greater than its base power:

{
  match: [{
    left: "FILTER_CARD",
    comparison: "GREATER_THAN",
    right: "FILTER_CARD",
    attributes: {
      left: "POWER",
      right: "BASE_POWER"
    }
  }]
}

Either side may use a direct card operand, a named { ref: string } card, { count: EffectCount }, or { value: ContextValue }; SOURCE remains available on the right for source-relative comparisons. A named targeted-card selection uses its target id as the ref and points to the exact object that was legal when resolution began. Target ids share the same cardRefs namespace as cost and effect result ids. A collision is an invalid authored definition and throws instead of overwriting either ref.

LESS_THAN_OR_EQUAL includes equal values. This is useful when a resolving effect compares a legal card target with another named object:

condition: {
  match: [{
    left: { ref: "graveyard-target" }, /* Widened API */
    comparison: "LESS_THAN_OR_EQUAL", /* Widened API */
    right: { ref: "sacrificed-creature" }, /* Widened API */
    attribute: "POWER"
  }]
}

attribute selects one statistic for both card operands, while attributes selects them independently and numeric operands resolve directly. A match array succeeds when any entry matches, then combines with the filter's other fields using normal AND semantics. Missing operands do not match. Current characteristics include counters and continuous modifiers. A trigger filter is checked only while matching its event and is not retained on the resulting trigger stack. In an intervening-if triggered-ability condition, an EVENT_CARD that has left the battlefield uses its last-known power or toughness snapshot. That snapshot is retained for the resolution-time condition check.

An activated-ability card target may compare against the chosen X through the same numeric operand shape. Use an offset with LESS_THAN to express "X or less" without adding an inclusive comparison:

match: [{
  left: "FILTER_CARD",
  comparison: "LESS_THAN",
  right: { value: { variable: "X", offset: 1 } },
  attribute: "POWER"
}]

Finding Cards

CardFilter is a predicate for one card. CardQuery adds the zone to search, and the engine's findCards function returns every matching CardInstance in zone order:

type CardQuery = {
  exclude?: "SOURCE" | "AFFECTED_CARD" | { ref: string }
  id?: string
  player?: "ANY" /* New API; graveyard only */
  zone: "battlefield" | "exile" | "graveyard" | "hand" | "library"
  filter?: CardFilter
}

For a graveyard query, omitted player keeps the existing player-graveyard behavior. player: "ANY" searches the player's graveyard followed by every tracked opponent graveyard. Counts and untargeted queries include only tracked cards. A target choice may also append the existing assumed opponent graveyard card when it matches the filter.

exclude: "SOURCE" removes the contextual source card by instance ID after selecting the zone. It is useful for Oracle text such as “other creatures.” If there is no source in the current effect context, there is nothing to exclude. exclude: "AFFECTED_CARD" (/* Widened API */) removes the card currently receiving a dynamic continuous modifier. If there is no affected-card context, there is nothing to exclude. exclude: { ref: "..." } removes the card or cards stored under that resolution reference. This lets a later query exclude an object selected by an earlier query-backed choice.

Effects consume a query through the shared count API. The count is the length of the returned array:

count: {
  findCards: {
    zone: "battlefield",
    filter: { types: ["Creature"], controller: "SELF" }
  }
}

An optional query id saves the exact returned card array in the current spell or ability's resolution context. Later effects in that resolution can reuse it through a normal reference:

count: {
  findCards: {
    id: "countered-creatures",
    zone: "battlefield",
    filter: {
      types: ["Creature"],
      counters: { type: "+1/+1", minimum: 1 }
    }
  }
}

target: { ref: "countered-creatures" }

Queries without an id are consumed without saving their result. Queries with an id are always saved; the engine does not inspect later effects to decide whether a reference will be used. Card definitions remain immutable—the runtime array lives only in effect context.

Some effect primitives wrap a query as an explicit single-card target choice:

{
  id: "chosen-permanent",
  findCards: {
    zone: "battlefield",
    filter: { controller: "SELF" }
  },
  choice: true
}

For Oracle text that targets any number of cards from a filtered set, use choice: "ANY_NUMBER" instead. The engine offers every subset of the matching query, including the empty set, and stores the selected card ids as one named effect-target selection. choice expresses who chooses from the query; Count remains reserved for evaluating a numeric value.

{
  id: "chosen-creatures",
  findCards: {
    zone: "battlefield",
    filter: {
      controller: "SELF",
      types: ["Creature"]
    }
  },
  choice: "ANY_NUMBER" /* New API */
}

The wrapper id stores the selected card, rather than the query's complete result, for later reference exclusion and resolution. Battlefield choice pools also offer one persistent assumed opponent creature whenever the query filter allows an opponent-controlled creature. The assumed object is not added to ordinary findCards results or counts. It can receive and retain counters and emits normal counter-placement events, but it does not represent a specific printed opposing permanent.

When an effect actually moves that assumed battlefield target, the engine materializes one concrete opponent-owned zone object. That exact object can be exiled, returned, copied by a spell, or invalidated by a later zone change. This legacy materialization path suppresses its generic candidate while the concrete object remains tracked. Aura targets use a fresh promotion instead: the concrete enchanted permanent and the separate virtual candidate are both available to later targeted effects.

Graveyard target choices similarly offer one assumed opponent-owned permanent card when it matches the query. A concrete player: "ANY" target stores both its card ID and exact zoneObjectId; leaving and returning makes that target illegal. The assumed choice uses targetOpponentCard: true. The assumed card is not added to ordinary findCards results or counts.

Targets And References

For new spell definitions and modes, a target-bearing effect should put its target declaration on the effect itself. Do not lift that target to a sibling of effects on the card or mode merely because legacy container-level target fields and examples still exist.

effects: [{
  type: "DESTROY_PERMANENT",
  target: {
    id: "creature-to-destroy",
    zone: "battlefield",
    filter: { types: ["Creature"] }
  }
}]

Do not use the legacy sibling arrangement for an effect that owns target:

target: {
  zone: "battlefield",
  filter: { types: ["Creature"] }
},
effects: [{
  type: "DESTROY_PERMANENT"
}]

Container-level targets remain appropriate only when the documented container semantics require target selection there. For example, a triggered ability target is selected when the trigger is created and revalidated when it resolves. An effect without its own target field may also consume a documented container target reference. Do not infer a container-level exception solely from an older card definition.

Target controller eligibility comes from filter.controller. Use controller: "SELF" for “you control” and controller: "OPPONENT" for “an opponent controls.” Omitting controller, or using "ANY", offers both tracked permanents and the assumed opponent-permanent choice.

A tracked permanent with effective Shroud cannot be selected as a spell or ability target. Target generation, supplied-target validation, and resolution revalidation all enforce Shroud. Non-targeting permanent sets and other query-backed choices do not.

When selected spell modes declare more than one effect-owned target, the engine stages those targets one at a time after the modes are chosen. Each stage exposes actions proportional to that target's candidate pool rather than enumerating the Cartesian product of every target combination. The completed cast stores every selection by target id; resolution revalidates them together and follows the normal rule that the spell resolves while at least one target remains legal.

Later effects can reuse selected targets with:

target: { ref: "creature-to-destroy" }

An EffectPlayer can instead read that target's controller:

player: { controllerOf: { ref: "creature-to-destroy" } } /* New API */

The selector uses the target's controller, not its owner. It reads the current controller when the legal target remains on the battlefield and last-known information when an earlier effect moved that target. An opponent result does not change the goldfish player's life total. A target that is illegal at resolution resolves to no player.

Triggered effects can instead read the controller of the permanent that caused the event:

player: { controllerOf: "EVENT_CARD" } /* Widened API */

For an UNTAP_PERMANENT event, this selector reads the current controller if that exact zone object remains on the battlefield. Otherwise it uses the event permanent's last-known controller. A permanent that leaves and returns is a new zone object, so it does not replace the event permanent.

EVENT_OPPONENT resolves to the specific opponent identified by the event that created the current ability or effect context:

player: "EVENT_OPPONENT" /* New API */

For an ATTACKS trigger, this is the opponent that creature attacked. If the event has no opponent identity, the effect resolves against no player. It does not fall back to opponent 1.

ACTIVE_PLAYER resolves to the player whose turn it is. During a simulated opponent turn, it preserves that opponent's exact ID. During a draw step, the active draw-step player remains available until every queued trigger and choice from that step has resolved.

SELF is the preferred spelling for the current source permanent. SOURCE is still supported in many places for older definitions.

Effects that move or create specific cards can expose those cards to later effects by setting an id. Later effects can refer to that card with { ref: "that-id" }.

A spell Aura that initially enchants a graveyard card declares that relationship on its container-level card target:

cardTarget: {
  zone: "graveyard",
  player: "ANY", /* New API: include modeled opponent graveyards */
  count: 1,
  filter: { types: ["Creature"] },
  attach: true /* New API */
}

A spell that targets a bounded number of cards uses the shared { min, max } count. Casting stages one select or deselect action per candidate and an explicit finish action. The engine never expands the candidate pool into every subset.

cardTarget: {
  zone: "graveyard",
  count: { min: 0, max: 2 }, /* Widened API */
  filter: { types: ["Creature"] }
}

The spell records the target's zone-object identity, so the target is illegal at resolution if that card leaves and returns to the graveyard. While the Aura is entering, { attachment: "SOURCE" } refers to that exact graveyard card.

A reference may hold one card or an array. A permanent effect using an array reference applies to every referenced card that is still on the battlefield.

Effects that affect every matching permanent use a battlefield-only permanent set instead of target:

permanents: {
  findCards: {
    zone: "battlefield",
    filter: { types: ["Creature"], controller: "SELF" }
  }
}

The query is evaluated when the effect resolves and applies to every returned permanent without generating target-selection actions. Keep controller restrictions inside CardFilter. These similar-looking query positions have different meanings:

  • target declares a rules target or consumes an explicit reference.
  • permanents.findCards selects a non-targeted permanent set for an effect.
  • count.findCards returns a number for an EffectValue.
  • to.findCards selects possible destinations for an effect such as distributed counter movement.

Assumptions

assumptions records explicit facts the goldfish simulation needs but cannot derive from a real opposing board. They are card metadata, not card rules: the card's printed condition or effect still appears in its ordinary DSL. Facts several cards could share, such as how often opponents cast spells, belong in the opponent model instead.

assumptions: {
  triggeredAbilityOccurrences?: [ /* New API */
    {
      abilityId: string,
      timing: "OPPONENT_COMBAT",
      occurrencesPerTurnCycle: [
        { count: number, chance: number }
      ]
    }
  ],
  opponent: {
    graveyardCard?: CardName,
    handSize?: number,
    weightedCounts?: [ /* New API */
      {
        id: string,
        outcomes: [{ count: number, chance: number }]
      }
    ],
    choices?: [
      { choiceId: string, optionId: string } | {
        choiceId: string,
        condition: {
          sacrificeOutletFor: CardFilter
        },
        matched: Array<{
          optionId: string,
          count: number | "ALL_OPPONENTS" | "ALL_BUT_ONE_OPPONENT"
        }>,
        rest: Array<{
          optionId: string,
          count: number | "ALL_OPPONENTS" | "ALL_BUT_ONE_OPPONENT"
        }>
      }
    ],
    boardState?: {
      permanentGroups?: [
        {
          id: string,
          candidates: [
            { card: CardName, chance: number, xValue: number }
          ]
        }
      ]
    }
  }
}
  • opponent.choices supplies the response each simulated opponent makes to a named choice, such as a vote. optionId: "DECLINE" fixes a named opponent payment at decline unless an injected opponent policy overrides it. Conditional vote assumptions may instead distribute opponent votes between matched and rest. sacrificeOutletFor matches actual activated costs on permanents you control, including activated mana abilities, against a hypothetical stolen permanent. The symbolic counts scale with the configured simulated opponent count.
  • opponent.graveyardCard supplies the assumed identity of a card targeted in an opponent's untracked graveyard. The normal target filter still applies, and a successful targeted movement stores this assumed card under the effect's id so later effects can inspect it.
  • opponent.handSize supplies the hand size of an opponent a card refers to, such as Borrowed Knowledge's target opponent.
  • opponent.weightedCounts supplies a named weighted count for a documented opponent-facing approximation. { assumption: { opponentCount: id } } resolves one sampled, capped count per ability resolution; pilot scoring evaluates its expected value without sampling.
  • opponent.boardState.permanentGroups describes possible opposing permanents for an effect such as GAIN_CONTROL. The effect names its group with assumptionId; the engine rolls and caches one candidate per source instance and group ID.
  • triggeredAbilityOccurrences samples one weighted occurrence count for the named ability per full turn cycle. OPPONENT_COMBAT queues one occurrence in each available opponent combat in turn order; any excess is queued during the final simulated opponent combat. Each occurrence uses the ability's normal targeting and resolution. The named ability must not require values from the event that would ordinarily trigger it.

Example, Braids, Arisen Nightmare records how many opponents decline its sacrifice. Only Braids asks that question, so the answer lives on the card:

"Braids, Arisen Nightmare": {
  assumptions: {
    opponent: {
      weightedCounts: [{
        id: "braids-opponents-not-sacrificing",
        outcomes: [
          { count: 1, chance: 0.50 },
          { count: 2, chance: 0.35 },
          { count: 3, chance: 0.15 }
        ]
      }]
    }
  }
}

Land Tax, by contrast, carries no assumption. Its OPPONENT_CONTROLS_MORE_LANDS_THAN_YOU condition reads the simulated opponent boards, which every catch-up card shares.

Effects

Primitive effects model game actions. A resolving spell or ability uses an uppercase effect object, so “draw two cards” is { type: "DRAW_CARDS", count: 2 }. The same action, when it is a cost that must be paid, uses its lower-camel-case cost form inside cost, such as moveCard, discardCard, or sacrificePermanent.

The supported shared cost primitives are moveCard, discardCard, sacrificePermanent, tapPermanent, loseLife, removeCounter, and putCounter. mana, waterbend, tap, and x remain intrinsic cost fields. The same atomic Cost shape is used by activated abilities, spell additionalCosts, alternate-cost additionalCosts, and PAY_COST.

Ordinary entries in the outer spell additionalCosts array are conjunctive: every one must be paid. A named { optional: true, costs } clause instead offers one cast action that declines the cost and, when payable, another whose additionalCostChoices records that the clause was paid. Use a chooseOne entry when Oracle requires exactly one of multiple additional costs. Each option has a stable id and a conjunctive costs payload. The finalized cast action records the selected option by choice-group id, so alternatives with no card selections remain distinct and are revalidated before payment:

additionalCosts: [
  {
    id: "payment",
    chooseOne: [ /* New API */
      {
        id: "discard-card",
        costs: [
          {
            discardCard: {
              id: "discard-cost",
              count: 1
            }
          }
        ]
      },
      {
        id: "pay-life",
        costs: [{ loseLife: { amount: 3 } }]
      }
    ]
  }
]

Choice-group ids must be unique across the spell-wide, selected alternate-cost, and selected mode-specific additional costs. Every group needs at least two uniquely identified options. Existing ordinary Cost entries remain source-compatible and need no choice metadata.

Use a named { id, repeatable: true, costs } clause when an additional cost may be paid any number of times. Omitting its id from additionalCostChoices means zero payments. A positive integer records the exact number of payments. Legal actions expose the ordinary zero-payment cast plus one choice for every count in the finite 1..maximum range. The engine derives that maximum from payable resources and cost modifiers, then revalidates the completed cast. Invalid, fractional, non-finite, zero, negative, and unknown selections are rejected. A clause with no consumptive payment never offers a positive count, so it cannot create an unbounded action space.

Repeatable permanent-sacrifice costs derive their maximum from distinct legal candidates. After the player chooses a count, the existing spell-cost selection actions collect that exact number of permanents one at a time. The engine never enumerates candidate subsets: n candidates produce n + 1 count choices and at most n selection actions at each step. Selection does not move a permanent. Cancellation leaves the card, mana, permanents, zones, and cast state unchanged. Once selection completes, the engine revalidates the whole cost and sacrifices the selected permanents as one simultaneous batch through the normal zone, death, and sacrifice event paths. A permanent cannot pay two repetitions, and selected permanents remain excluded from mana production while legality and payment are checked.

An additional cost may declare x, including a dynamic max, and reference the chosen value from its other primitives. The cast action records that value in xValue; every applicable spell cost uses the same X. If the printed mana cost also contains X, the legal values are the intersection of both ranges. Casting without paying the mana cost fixes printed mana-cost X at zero, but a spell whose X appears only in an additional cost still chooses and pays that X:

additionalCosts: [{
  x: { /* Widened API */
    min: 0,
    max: { source: "LIFE_TOTAL" }
  },
  loseLife: {
    amount: { variable: "X" }
  }
}]

Selectable moveCard costs can contribute toward a spell's final mana payment. Use selection.minimum: 0 for an optional choice and maximum: "PAYMENT_REMAINDER" to cap the selected cards at the compatible mana still owed after cost increases, reductions, and additional mana costs. Each selected card contributes payFor.amount; generic: true means that contribution pays only generic mana. The engine validates the complete payment before moving any selected cards:

additionalCosts: [
  {
    moveCard: {
      id: "delve-cards",
      from: "graveyard",
      to: "exile",
      selection: { /* New API */
        minimum: 0,
        maximum: "PAYMENT_REMAINDER"
      },
      payFor: { /* New API */
        amount: 1,
        generic: true
      }
    }
  }
]

Use another: true when the selected cards must be different objects from the cost's source. The engine excludes the source while generating actions and revalidates exact, distinct selected objects before changing mana, zones, or the stack. A successful spell cast moves its source to the stack before its selected movement costs are paid, and emits one movement group for cards paid together.

sacrificePermanent accepts either one sacrifice clause or an array of clauses. A named clause selects count ?? 1 matching permanents. count accepts any EffectValue, including { variable: "X" }, and resolves in the same cost context used for legality and payment. Array entries and counted entries use distinct permanents, so one permanent cannot pay more than one selection. Counted activated-ability costs are selected through staged legal actions; the engine does not enumerate every combination. Nothing is sacrificed until the player explicitly finishes a complete selection, and cancellation pays no part of the cost:

cost: {
  sacrificePermanent: [
    {
      id: "swamp",
      count: 1, /* New API */
      filter: { types: ["Land"], subtypes: ["Swamp"] }
    },
    {
      id: "forest",
      filter: { types: ["Land"], subtypes: ["Forest"] }
    }
  ]
}

The source permanent is eligible unless the clause says another: true or its filter excludes it. A completed multi-permanent sacrifice is paid as one battlefield batch. The engine owns candidate legality; the pilot chooses among the staged legal candidates. A permanent selected for sacrifice is excluded from mana sources used to pay another part of the same cost.

tapPermanent selects count ?? 1 distinct untapped matching permanents. It models costs that say “tap an untapped permanent,” not the {T} symbol: the source may be selected unless another: true, and summoning sickness does not make a permanent ineligible. All clauses are revalidated atomically before payment, and selected permanents cannot also supply mana for the same cost:

cost: {
  tapPermanent: { /* New API */
    id: "tapped-creatures",
    count: 2,
    filter: {
      controller: "SELF",
      types: ["Creature"]
    }
  }
}

totalPower replaces count with a combined power threshold: the payer taps any number of matching untapped permanents whose summed power is at least the threshold, and the cost is unpayable when no such set exists. Legal-action enumeration offers only minimal sets, so surplus power stays untapped:

cost: {
  tapPermanent: {
    id: "crew",
    another: true,
    filter: { controller: "SELF", types: ["Creature"] },
    totalPower: 3 /* Widened API */
  }
}

TAP_PERMANENT remains an effect primitive. Unlike tapPermanent, it does not select and validate untapped objects as payment.

waterbend represents the complete printed Waterbend mana component. Each selected untapped artifact or creature the payer controls pays for {1} of its generic amount:

cost: {
  waterbend: { generic: 3 } /* New API */
}

Summoning sickness does not prevent a creature from paying Waterbend. An artifact creature is one permanent and contributes only {1}. A selected permanent is excluded from mana-source activation during the same payment. Activated abilities, spell additional costs, and PAY_COST effects use staged select, deselect, finish, and cancel actions, so n candidates produce at most n selection actions rather than every candidate subset. Mandatory and optional payments share the same engine-owned legality; optional spell costs also retain their unpaid cast choice.

The contribution cap is the authored Waterbend generic value after resolving X. Cost increases raise the mana still owed but do not raise that cap. Cost reductions may lower both the outstanding generic payment and the number of useful Waterbend selections. The engine validates the completed mixed payment before tapping permanents or spending mana. The pilot prefers expendable and summoning-sick permanents, then finishes with mana rather than tapping mana sources, commanders, engines, or attack-ready creatures when those resources are more valuable than the saved mana.

Activated abilities may set maxActivationsPerTurn for a real Oracle restriction. The count belongs to that permanent object, follows it through control changes, resets at the beginning of every player's turn, and is cleared when the object changes zones. This is distinct from simulatedActivationsPerTurnCycle, which remains a pilot simulation throttle reset on the player's untap.

Life payments use the game's cumulative lifeTotal, which starts at 40 for a Commander game. A payment is legal when its amount is no greater than the current life total, including a payment of exactly all remaining life. The default pilot declines a voluntary payment that would leave it at zero.

DRAW_CARDS is intentionally distinct from moving a card from library to hand: it emits a draw event, while MOVE_CARD does not. This lets draw-trigger rules distinguish “draw a card” from “put a card into your hand.”

Composite game instructions do not automatically deserve their own primitive. Use the first-class SCRY and SURVEIL mechanics for instructions that specifically name those mechanics, so their Magic identity is retained. Other instructions should be assembled from the smallest existing actions whenever that retains their rules meaning.

REPLACEMENT_EFFECT

REPLACEMENT_EFFECT is a continuous static effect that rewrites a matching logical effect before that effect resolves. It does not trigger, use the stack, or copy the matched effect. Replacement sources normally remain on the battlefield. A replacement with sourceZones is active only while its source is in one of those explicitly declared zones.

The public shape can modify one token group's count, replace that group with named output groups, or append groups to the logical creation event:

type TokenReplacementGroup = {
  name: CardName,
  count: EffectValue | "EVENT_COUNT"
}

{
  type: "REPLACEMENT_EFFECT", /* New API */
  match: {
    effect: {
      type: "CREATE_TOKEN"
    },
    controller: "SELF" | "ANY",
    tokenFilter?: CardFilter,
    count?: {
      comparison: "AT_LEAST" | "LESS_THAN",
      value: EffectValue
    }
  },
  replace:
    | {
      count: {
        operation: "MULTIPLY",
        value: EffectValue
      }
    }
    | {
      tokens: TokenReplacementGroup[]
    }
    | {
      additionalTokens: TokenReplacementGroup[]
    }
}

match.controller is the controller under whose control the tokens would be created, not the controller of the spell or ability creating them. tokenFilter examines the characteristics the would-be token will have. When the filter is omitted, creature tokens, noncreature tokens, and token copies all match.

match.count examines the current proposed count for the matching token group. Replacement effects already applied to that group may have changed this value. replace.count changes only that count. Its resolved multiplier must be a positive integer. replace.tokens replaces one matching group with one output group per listed token name. For these group replacements, EVENT_COUNT resolves to the matched group's current count.

replace.additionalTokens preserves every current group and appends its listed groups once for the logical creation event. It applies once per exact replacement-source zone object. EVENT_COUNT resolves to the sum of the current groups that match the replacement. A fixed count therefore stays fixed even when an earlier replacement expanded the event into several groups. Every existing and appended group records that source as applied, so the replacement cannot apply again to its own addition. Other sources remain eligible in the engine's deterministic battlefield order. The creating effect and later effects still resolve once. Zero-token operations remain zero.

Anointed Procession doubles every positive token-creation count under the goldfish player's control:

staticAbilities: [
  {
    type: "REPLACEMENT_EFFECT",
    match: {
      effect: {
        type: "CREATE_TOKEN"
      },
      controller: "SELF",
      count: {
        comparison: "AT_LEAST",
        value: 1
      }
    },
    replace: {
      count: {
        operation: "MULTIPLY",
        value: 2
      }
    }
  }
]

Every engine path that creates a token is normalized through this logical CREATE_TOKEN operation before the token enters. This includes authored CREATE_TOKEN, INVESTIGATE, token-copy COPY, Afterlife, Offspring, and legacy permanent-token copies.

The logical operation begins as one group and retains its complete token specification, including copy characteristics, tapped state, power and toughness overrides, recipient, creating source, and linked result references. After a replacement creates named outputs, each output is its own group. A replacement source records itself on the group it changed and cannot apply to that group's descendants. Other sources may apply once to every eligible descendant. The engine uses battlefield order for this deterministic process; it does not expose a replacement-order choice.

After all replacements finish, all final groups enter as one simultaneous battlefield batch. A CREATE_TOKEN id or token-copy resultId records every final created token. Count multipliers apply once per final group and continue to compound with named-output replacements.

Academy Manufactor turns a matched group of N Clues, Foods, or Treasures into N Clues, N Foods, and N Treasures:

staticAbilities: [{
  type: "REPLACEMENT_EFFECT",
  match: {
    effect: { type: "CREATE_TOKEN" },
    controller: "SELF",
    tokenFilter: { names: ["Clue", "Food", "Treasure"] }
  },
  replace: {
    tokens: [
      { name: "Clue", count: "EVENT_COUNT" },
      { name: "Food", count: "EVENT_COUNT" },
      { name: "Treasure", count: "EVENT_COUNT" }
    ]
  }
}]

A spell-copy replacement can add to or multiply a positive COPY_SPELL count:

{
  type: "REPLACEMENT_EFFECT",
  match: {
    effect: { type: "COPY_SPELL" }, /* New API */
    controller: "SELF",
    count: {
      comparison: "AT_LEAST",
      value: 1
    }
  },
  replace: {
    count: {
      operation: "ADD",
      value: 1
    },
    additionalCopiesMayChooseNewTargets: true
  }
}

match.controller is the controller of the copy instruction. SELF excludes copy instructions controlled by a simulated opponent. match.count examines the current proposed count after earlier applicable replacements.

The engine keeps the original copies and each replacement's added copies in groups. The original group retains the COPY_SPELL effect's mayChooseNewTargets value. Each added group uses additionalCopiesMayChooseNewTargets, which defaults to false. This lets a replacement grant new-target permission to its added copy without granting it to the original copies. Groups with the same permission may be combined before the engine creates the individual stack objects.

Each replacement source applies once per exact battlefield zone object, in battlefield order. Additions and multipliers must resolve to positive integers. A zero-copy instruction remains zero and does not invoke these replacements.

A discard replacement can send the discarded card to exile instead of the graveyard:

{
  type: "REPLACEMENT_EFFECT",
  sourceZones: ["hand"], /* New API */
  match: {
    effect: {
      type: "DISCARD_CARDS"
    },
    source: "SELF" /* New API */
  },
  replace: {
    to: "exile" /* New API */
  }
}

This replacement applies to the proposed discard movement before the card leaves the hand. The completed MOVE_CARD event reports the actual hand-to- exile movement. The engine separately emits DISCARDED, because the rules action is still a discard. A direct effect that moves the same card from hand to exile does not match this replacement and does not emit DISCARDED.

A graveyard-entry replacement diverts a card before it enters its owner's graveyard from anywhere. owner names whose graveyard is replaced:

staticAbilities: [{
  type: "REPLACEMENT_EFFECT",
  match: {
    effect: { type: "PUT_INTO_GRAVEYARD", owner: "SELF" }, /* New API */
    controller: "ANY"
  },
  replace: { to: "exile" }
}]

PUT_INTO_OPPONENT_GRAVEYARD is the earlier spelling of owner: "OPPONENT" and only considers cards owned by a simulated opponent:

staticAbilities: [{
  type: "REPLACEMENT_EFFECT",
  match: {
    effect: { type: "PUT_INTO_OPPONENT_GRAVEYARD" },
    controller: "OPPONENT"
  },
  replace: { to: "exile" }
}]

controller reads the card's controller immediately before the zone change. Use "OPPONENT" for “a card you didn't control,” "ANY" for effects such as Dauthi Voidwalker or Forgotten Cellar, or "SELF" when the card text requires it. An optional filter tests the pre-move card; a nonempty filter cannot match an opaque opponent card.

A matching opponent-owned card is kept in game.opponentExiles[owner], separate from the player's game.exile zone. A matching card the player owns goes to game.exile exactly as an ordinary exile would, including discards, mills, dying permanents, and resolving instants and sorceries. The engine applies this replacement to ordinary card moves, battlefield exits, estimated opponent spells, and assumed opponent discards. It records the actual movement to exile wherever that movement path normally records zone movement. The replacement happens before graveyard-entry and death events, so it emits neither ENTERS { to: "graveyard" } nor CREATURE_DIED. Tokens are not cards and do not match. Simultaneous battlefield exits use the pre-move battlefield snapshot when determining whether the replacement applies.

This capability does not itself link exiled cards to a source or grant a permission to cast them. Add that behavior with a source-specific follow-up ability when a card requires it.

Life-gain replacement effects use the same multiplication model:

staticAbilities: [
  {
    type: "REPLACEMENT_EFFECT",
    condition: { /* New API */
      count: { source: "LIFE_TOTAL" },
      comparison: "LESS_THAN",
      value: 6
    },
    match: {
      effect: {
        type: "GAIN_LIFE" /* New API */
      },
      player: "SELF" /* New API */
    },
    replace: {
      amount: { /* New API */
        operation: "MULTIPLY",
        value: 2
      }
    }
  }
]

An optional top-level condition uses CountComparisonCondition. A false condition makes the replacement inapplicable; it is not an applied multiplier of one. The condition is checked before the multiplier is resolved and before the life total changes.

The finalized amount updates the player's life total and per-turn life-gain tracking, then becomes the amount of the emitted LIFE_GAINED event. Multiple multiplicative life-gain replacements compound. A replacement source must be on the battlefield when the life-gain effect resolves.

Card-draw replacement effects also use the multiplication model:

staticAbilities: [
  {
    type: "REPLACEMENT_EFFECT",
    condition: { /* New API */
      count: {
        findCards: { zone: "hand" }
      },
      comparison: "EQUAL",
      value: 0
    },
    match: {
      effect: {
        type: "DRAW_CARDS" /* New API */
      },
      player: "SELF", /* New API */
      firstInDrawStep: false /* New API */
    },
    replace: {
      amount: { /* New API */
        operation: "MULTIPLY",
        value: 2
      }
    }
  }
]

The optional top-level condition has the same applicability semantics as the life-gain variant. Draw replacements check it once for each original proposed draw before any card for that draw moves from the library. Replacement-created draws belong to that same proposal, so drawing two original cards from an empty hand with the example above draws three cards total, not four.

Draw replacements apply to each individual card draw before that card moves from the library. Every resulting draw updates draw tracking and emits its own DRAW_CARD event. firstInDrawStep: true matches only the first actual draw by the active player in that draw step. firstInDrawStep: false matches every other draw, including draws outside a draw step and draws by another player during the active player's draw step. Omitting it matches any draw. Separate and extra draw steps each start a new first-draw count; replacement-generated draws are counted only when they actually occur. Multiple multiplicative draw replacements compound.

CONDITIONAL

Evaluates one reusable effect condition and resolves exactly one of two effect branches. The condition is checked once when CONDITIONAL resolves, before either branch begins. This preserves Oracle instructions where an action in the matched branch could change the fact that selected the branch.

{
  type: "CONDITIONAL",
  if: {
    match: [{
      left: "EVENT_CARD",
      comparison: "GREATER_THAN",
      right: { count: 2 },
      attribute: "POWER"
    }]
  },
  matched: {
    effects: [{ type: "DRAW_CARDS", amount: 1 }]
  },
  rest: {
    effects: [{
      type: "PUT_COUNTER",
      target: "EVENT_CARD",
      counter: { type: "+1/+1", amount: 2 }
    }]
  }
}

if accepts the same reusable EffectCondition shape as an individual effect's optional condition. Card-attribute matches use current battlefield characteristics at resolution and last-known information when the referenced event card or source has left the battlefield. A match array succeeds when any comparison matches. After the selected branch finishes, later effects in the containing spell or ability continue in their original order.

MOVE_CARD

Moves cards between zones. Use this for "put into hand", "return from graveyard", "put a land onto the battlefield", and similar generic zone movement. Do not use it for an instruction or cost that says "discard" or "mill"; use DISCARD_CARDS, discardCard, or MILL so the engine preserves that rules meaning. Movement still goes through engine zone helpers, so battlefield entry and graveyard events happen normally. A completed MOVE_CARD operation also emits one grouped MOVE_CARD trigger event, even when it moves several cards.

When target is a CardQueryChoice, the effect owns the target declaration. Set optional: true on that choice for "up to one target card." The player selects a legal target or declines when the triggered ability is put on the stack. Declining queues the ability with an empty selection for the target id. The MOVE_CARD effect then leaves its result ref absent, so dependent effects do not treat the decline as a successful move.

The engine revalidates a selected card and its zone-object identity when the ability resolves. If the selected object left the queried zone, including when a card with the same id returned as a new zone object, the whole targeted ability fails to resolve. No later effect in that ability resolves.

Current shape:

{
  type: "MOVE_CARD",
  from?: MoveCardZone | [MoveCardZone, ...MoveCardZone[]], /* Widened API */
  to: "hand" | "battlefield" | "commander" | "exile" | "graveyard" |
      "library" | "library_top" | "library_bottom" | "opponent_control",
  count?: EffectValue,
  card?: "SOURCE" | "TARGET_CARD" | "REVEALED_CARD" |
    "SELECTED_REVEALED_CARD" | { ref: string } |
    { attachment: "SOURCE" }, /* New API */
  cards?: { ref: string }, /* New API */
  target?: PermanentEffectTarget | CardQueryChoice, /* Widened API */
  playableAs?: "NON_ADVENTURE",
  source?: { ref: string },
  filter?: CardFilter,
  choice?: boolean | {
    constraint?: {
      type: "MATCH_DISTINCT_VALUES",
      attribute: "COLOR",
      values: { findCards: CardQuery }
    },
    minimum?: number,
    maximum?: EffectValue,
    order?: true
  },
  all?: true,
  order?: true,
  randomSelection?: true, /* New API */
  randomOrder?: true, /* New API */
  reveal?: true, /* New API */
  choicePool?: {
    linkedTo: { id: string, source: "SOURCE" }
  },
  exclude?: { ref: string }, /* New API */
  optional?: boolean,
  chooser?: "OPPONENT", /* New API; requires choice */
  declineEffects?: Effect[], /* Widened API */
  exileLink?: {
    id: string,
    source: "SOURCE",
    returnWhenSourceLeaves?: true
  },
  face?: CardName,
  faceDown?: true, /* New API */
  tapped?: boolean,
  controller?: "OWNER" | "SELF", /* Widened API; battlefield only */
  entersWithCounters?: CounterEffect[], /* New API; battlefield only */
  id?: string
}

An effect can move cards from the graveyard of a selected player. The target belongs to the effect, so modal spells can give different modes independent targets without widening the spell-level target shape:

{
  type: "MOVE_CARD",
  from: "graveyard", /* Widened API */
  to: "exile", /* Widened API */
  all: true, /* Widened API */
  target: {
    id: "graveyard-player",
    type: "player",
    player: "any"
  }
}

Choosing self moves every actual card in the tracked graveyard. The abstract opponent target maps to opponent 1, TurnZero's existing generic opponent, and moves only cards already tracked in that opponent's graveyard. An empty graveyard remains a legal player target. This form does not create assumed graveyard cards and does not exile every simulated opponent's graveyard.

The nonempty array form combines candidates from the listed source zones, in zone-list order, for one movement operation and one shared choice count. Each selected card is moved from the zone it actually occupies. Battlefield entries from multiple source zones are placed as one simultaneous batch, while grouped MOVE_CARD events are emitted separately for each actual source zone because the event shape records one from zone.

The targeted stack-to-hand form is intentionally narrow:

{
  type: "MOVE_CARD",
  card: "TARGET_CARD",
  from: "stack", /* New API */
  to: "hand",
  count: EffectValue
}

It removes only matching spell objects from the stack. Activated abilities, triggered abilities, and pending choice objects are not cards and are never eligible. A copied spell ceases to exist when removed from the stack instead of becoming a physical card in hand.

An as-enters queue can instead move its own pending source from the stack to a nonbattlefield zone. This is intentionally narrow and is only valid while an ordinary as-enters effect is resolving:

{
  type: "MOVE_CARD",
  card: "SOURCE",
  from: "stack", /* New API */
  to: "graveyard",
  count: 1,
  condition: { refMissing: "mox-diamond-discard" } /* New API */
}

The move prevents that entry from reaching the battlefield, so it emits no battlefield ENTERS event. It reuses the same neutral stack-card zone movement as normal spell resolution.

Moving a card from hand to graveyard with MOVE_CARD is only zone movement. It does not emit DISCARDED, increment CARDS_DISCARDED_THIS_TURN, or satisfy discard-dependent effects. This boundary is important for effects that put a card into a graveyard without instructing its owner to discard it.

DISCARD_CARDS

Models the semantic Magic action "discard". It moves tracked cards out of hand, applies destination replacements such as Madness, records each discarded card, and emits one DISCARDED event per card after the replacement is applied. For a multi-card discard, every selection is staged first. The engine then moves the complete selected batch before emitting any of those events.

{
  type: "DISCARD_CARDS",
  count: EffectValue | "HAND",
  choice?: boolean,
  filter?: CardFilter, /* New API */
  optional?: boolean,
  randomSelection?: boolean,
  player?: EffectPlayer | "EACH_OPPONENT",
  id?: string,
  afterChoiceEffects?: Effect[]
}

Use count: "HAND" for "discard your hand". choice: true exposes CHOOSE_DISCARD_CARDS selection actions one card at a time without moving the selected cards. An empty selection finishes an allowed partial choice; reaching the required count finishes automatically. Only then does the engine commit the batch. optional: true normally allows an empty selection to stop; with count: "HAND" and no choice, it is instead all or none, so the player may decline before the first selection but must finish selecting the hand after accepting. A named id captures the cards actually discarded for later { ref: id } and moved-card count references. randomSelection uses the game RNG instead of a pilot choice.

When filter is present, only matching cards in hand are legal selection actions and direct selections of nonmatching cards are rejected by the engine.

Discard triggers are created by the committed batch but wait with all other triggers until the enclosing spell or ability has finished resolving. Effects after the discard therefore resolve before any discard trigger can resolve.

For "you may discard ... if you do" or a dependent "you may discard ... then ..." instruction, prefer the discard primitive's own choice plus a stored result. Give DISCARD_CARDS an id, make it optional or a choice as the instruction requires, and put condition: { refExists: id } on the dependent effect. Do not model acceptance and decline as CHOOSE_ONE options. A declined discard leaves the ref absent. An accepted discard stores the discarded cards; when an empty hand is discarded, the stored ref is an empty array that still satisfies refExists.

The player-scoped form records the completed discard count separately for every affected player:

{
  type: "DISCARD_CARDS",
  id: "discard-hands",
  player: "EACH_PLAYER", /* New API */
  count: "HAND"
}

The tracked player's cards move through the normal zone machinery. Opponent hands remain abstract, so their completed counts use the source card's assumptions.opponent.handSize value. Each assumed opponent discard samples its configured category and emits its own semantic DISCARDED event with the exact opponent ID. The materialized abstract card exposes only enough identity for ordinary type filters. It does not enter a tracked opponent graveyard.

player: "OPPONENT" affects one opponent. When resolution already carries an exact event opponent, the discard keeps that opponent ID; otherwise it uses the first simulated opponent. player: "EACH_OPPONENT" affects every simulated opponent and does not discard from the tracked hand. Both forms cap count at the source card's assumptions.opponent.handSize, emit one distinct typed DISCARDED event per assumed card for each affected opponent, and leave both the tracked hand and opponent graveyards unchanged. The hand-size assumption applies afresh to every resolution rather than tracking depletion between resolutions. Named discard results preserve the completed count for each exact opponent ID.

The cost form is discardCard. It supports either discarding the source itself or selecting cards from hand:

cost: {
  discardCard: { source: "SELF" }
}

additionalCosts: [{
  discardCard: {
    id: "discard-cost",
    count: 1,
    filter: { types: ["Land"] }
  }
}]

The cost has no from or to: those zones are intrinsic to discard. Use a generic moveCard cost when a cost physically moves a card but does not say "discard".

cards: { ref: "..." } moves the complete captured card group still present in from; it does not take a count. Exact object identity is retained, so a card that leaves that zone and later returns is not reconnected even if it keeps the same physical-card id. Moving a group to the battlefield places all of its permanents before emitting any of their enters-battlefield events. For mixed-owner return effects, controller: "OWNER" assigns each entering permanent to its own owner without splitting the simultaneous entry batch.

source: { ref: "..." } also retains exact zone-object identity. For a graveyard source, the engine follows each referenced card to its owner's actual graveyard, including a tracked opponent graveyard. A card that leaves and returns is a new object and no longer matches the reference. When the destination is the battlefield, controller: "SELF" preserves the card's owner but puts it under the resolving object's controller.

For a battlefield destination, entersWithCounters places its resolved counters during battlefield-entry preparation. Normal counter-placement modifiers apply. The counters exist before counter-placement records and ENTERS listeners observe the permanent.

A permanent with one or more finality counters replaces any battlefield-to- graveyard move with a move to exile. The permanent does not enter a graveyard, does not die, and emits no graveyard-entry or death event. Sacrificing it still emits the normal sacrifice event. Once its last finality counter is removed, later battlefield-to-graveyard movement and death proceed normally.

Use all: true to move every matching candidate. count may be omitted in that form; if an older definition supplies both, all wins and count is ignored.

An exact count moves as many of those cards as remain available if the source zone contains fewer cards than requested. optional: true adds accept and decline actions. When choice is omitted, accepting preserves the same deterministic cards as the non-optional effect, such as the top count cards of a library. Add choice: true only when the player chooses which matching cards move.

For an optional move with top-level declineEffects, the optional decision is made before any card choice. Accepting proceeds to the normal choice: true card-selection action; declining skips that selection, resolves declineEffects, and then resumes later sequential effects. No optional decision is opened when there are no eligible cards. Existing optional moves without declineEffects retain their direct move-or-decline actions.

Use randomSelection: true on a mandatory counted move when the instruction randomly determines which eligible cards move. The engine samples the cards uniformly without replacement using the game RNG, preserves their source-zone order during movement, and does not offer a pilot choice. It cannot be combined with choice, optional, all, order, card, cards, or randomOrder.

Use randomOrder: true only with to: "library_bottom". The engine randomizes the moved group using the game RNG while leaving cards outside that group in their existing relative order ahead of it.

Use reveal: true when the movement instruction reveals the chosen card. Opponent-facing public information is not separately displayed in the goldfish UI.

face is available only when to is "battlefield". It selects the named face before battlefield-entry preparation and events, so the permanent enters with that face's characteristics. Outside the battlefield, transforming double-faced cards continue to normalize to their front face.

faceDown: true is available only when to is "battlefield". The card enters as a colorless, nameless 2/2 Creature with no mana cost, subtypes, keywords, or printed abilities. The engine retains its underlying identity for effects that can later turn it face up.

A self-controlled face-down permanent whose underlying card is a creature card offers a TURN_FACE_UP special action when its printed mana cost can be paid. The action pays that cost and restores the card's normal characteristics without using the stack or causing it to enter the battlefield again.

A permanent cast using Morph instead pays the structured Morph keyword's Cost. The engine records how the card became face down, so this route remains distinct from the printed-mana-cost turn-up action above.

A spell, activated ability, or triggered ability can instead move one card chosen as a target when it is created. This query-backed form supports battlefield permanent and graveyard card targets:

{
  type: "MOVE_CARD",
  target: {
    id: "graveyard-card",
    choice: true,
    findCards: {
      zone: "graveyard",
      player: "ANY", /* New API */
      filter?: CardFilter
    }
  },
  to: "battlefield",
  controller: "SELF" /* New API */
}

The engine creates one legal cast, activation, or triggered-target action per matching tracked card and also offers the matching assumed opponent permanent or graveyard card. The named selection is retained on the stack and revalidated at resolution. A concrete nonbattlefield target stores both its card id and its exact zone-object id. If that card leaves the zone and returns before resolution, it is a new object and the target is illegal even when the returned card has the same card id. Legacy actions that contain only targetCardId remain accepted and use the current matching object. If a concrete target is no longer legal, none of the spell or ability's effects resolve. For to: "battlefield", controller: "SELF" puts the card onto the battlefield under the resolving ability's captured self controller while preserving its owner. The zone change assigns a fresh zoneObjectId.

An assumed opponent graveyard target normally records and emits only its abstract movement. Targeted movement to the battlefield with controller: "SELF" is different because it changes the player's permanents and combat output. The engine materializes a unique opponent-owned battlefield object from the source card's assumptions.opponent.graveyardCard, stores that exact object under the effect id, and emits normal movement and entry events.

When one spell declares more than one effect-local target, the engine stages the declarations in authored effect order. Card-query targets and battlefield permanent or damage targets use the same staged cast flow. Each legal action chooses one candidate for the current target, then the engine advances to the next declaration. It does not enumerate card/permanent target pairs, so action generation is proportional to the current candidate pool rather than the product of every target pool. The completed cast stores all named selections in effectTargets. A selected spell mode with more than one effect-local target hands off to the same staged flow after its mode and cost are chosen.

Set optional: true when moving the already-selected target is optional. The target itself remains mandatory when the spell or ability is created; the accept-or-decline choice happens only after the target is revalidated at resolution.

Set chooser: "OPPONENT" on an untargeted move whose printed choice belongs to an opponent, such as "an opponent chooses two of those cards. Put the chosen cards into your graveyard." Legality does not change: the engine stages the same MOVE_CARD_CHOICE, with the same counts and candidates, and rejects the field without choice. The pilot reads chooser off the pending choice and picks against itself, so the simulated opponent takes the best cards:

{
  type: "MOVE_CARD",
  source: { ref: "found-lands" },
  from: "library",
  to: "graveyard",
  count: 2,
  choice: true,
  chooser: "OPPONENT" /* New API */
}

Add id to a targeted MOVE_CARD when later effects need the exact object that successfully reached the requested destination. The ref is empty when the target is illegal or a movement replacement sends it elsewhere:

{
  type: "MOVE_CARD",
  id: "exiled-card", /* Widened API */
  target: {
    id: "graveyard-target",
    choice: true,
    findCards: { zone: "graveyard" }
  },
  to: "exile"
}

For targeted battlefield movement, put the permanent target directly on the MOVE_CARD effect. The target's zone supplies the source zone, so this form does not also use from:

{
  type: "MOVE_CARD",
  target: {
    id: "permanent-to-return",
    zone: "battlefield",
    filter: { not: { types: ["Land"] } }
  },
  to: "hand"
}

Counted permanent targets can require every selected permanent to have a different controller:

{
  type: "MOVE_CARD",
  target: {
    id: "creatures-with-different-controllers",
    zone: "battlefield",
    count: 2,
    filter: { types: ["Creature"] },
    constraint: { /* New API */
      type: "MATCH_DISTINCT_VALUES",
      attribute: "CONTROLLER"
    }
  },
  to: "hand"
}

MATCH_DISTINCT_VALUES compares exact controller identities, not merely whether a permanent is controlled by self or an opponent. A tracked permanent without an explicit controller is controlled by self; the assumed opponent permanent uses its configured opponent controller. The constraint is enforced while enumerating, staging, validating, and automatically selecting targets. It does not change the declared target count or the target's normal filter.

When a counted permanent target group resolves, each target is revalidated. The effect acts on the legal members when at least one remains legal and does nothing when every target is illegal. A relationship constraint is checked against the complete declared group first, using current controller information or last-known information for a permanent that left the battlefield. An assumed opponent permanent contributes its controller to the constraint and records its abstract movement while tracked legal targets still move normally.

Each effect may declare its own target id. A later effect can consume the same selection with target: { ref: "permanent-to-return" }; a different id creates an independent target choice. Tracked permanents move through the normal battlefield-leaving machinery. An abstract opponent permanent remains a legal goldfish target, but moving it is a state no-op because opponent battlefield objects are not tracked.

When card: "SOURCE" is used during spell resolution, from may be omitted. The engine then moves the resolving source card. playableAs: "NON_ADVENTURE" marks an Adventure card exiled this way as castable later as its non-Adventure face.

When id is present, the moved card or chosen moved cards are stored as card refs for later effects:

{
  type: "MOVE_CARD",
  id: "returned-card",
  from: "graveyard",
  to: "hand",
  count: 1,
  choice: true
}

id can also be placed on a MOVE_CARD trigger. The trigger receives the actual moved card group through that ref, letting its effects act on the card that caused it. Multi-card movements remain grouped by their actual destination. Use a DISCARDED trigger to receive one event and one captured card per discard, regardless of its post-replacement destination.

choice.constraint.type: "MATCH_DISTINCT_VALUES" makes a multi-card choice legal only when every chosen card can be assigned a different matching value found by the declared query. For attribute: "COLOR", each selected card must have at least one color among the queried cards, and no color can be assigned twice. A multicolored card is assigned exactly one of its colors for this choice. The engine performs the assignment check; the pilot chooses only among legal card groups.

exileLink records that a card moved to exile is associated with the resolving source's exact zone object. Physical card ids remain stable, while every real zone change assigns a fresh zoneObjectId; phasing retains the same object. A source that leaves and returns therefore cannot access links created by its old object. A later choice: true movement can use choicePool.linkedTo to offer only cards associated with that same source. The association is cleared when the linked card changes zones. Ordinary links survive source movement for pending old-source abilities. Set returnWhenSourceLeaves: true only for effects that should immediately return linked cards to their original zone when the source leaves the battlefield. If the exact source object has already left before such a move resolves, the card does not enter exile. A returned physical card is a new zone object and cannot revive the pending link.

exclude: { ref } removes exact referenced zone objects after normal candidate and linked-pool filtering. A destination-aware effect condition such as condition: { card: { ref: "new-imprint" }, zone: "exile" } succeeds only when that exact captured object remains in the declared zone. These compose the "if you do" pattern without confusing an attempted move with a successful one. For a zone-change trigger, condition: { movedCard: { ref: "moved" }, zone: "exile" } checks the captured destination object rather than the event's last-known source object. Use that form when an intervening zone change must make the pending effect fail. Represented opponent-owned deaths are retained in owner-specific graveyards; explicit refs locate the exact referenced zone object across those graveyards, while an unqualified graveyard query still means the tracked player's graveyard. A stale ref fails if that object left the graveyard, even when a card with the same physical id later returns.

For a battlefield destination, controller: "OWNER" returns each card under its owner's control. controller: "SELF" returns every referenced card under the resolving player's control while preserving its owner. The controller choice does not widen the candidate pool or make unrelated opponent graveyard cards selectable.

Currency Converter uses both halves of the link. Its discard trigger captures the discarded card, then optionally stores that exact card in exile:

{
  trigger: {
    type: "DISCARDED",
    id: "discarded-card",
    source: "ANY"
  },
  effects: [
    {
      type: "MOVE_CARD",
      source: { ref: "discarded-card" },
      from: "graveyard",
      to: "exile",
      count: 1,
      optional: true,
      exileLink: { id: "currency-converter", source: "SOURCE" }
    }
  ]
}

Its conversion ability later chooses only cards linked to that particular Converter instance, stores the returned card under a ref, then uses that ref in the token conditions:

{
  type: "MOVE_CARD",
  id: "currency-converter-returned",
  from: "exile",
  to: "graveyard",
  count: 1,
  choice: true,
  choicePool: {
    linkedTo: { id: "currency-converter", source: "SOURCE" }
  }
}

MILL

Models the semantic Magic action "mill". It moves the top cards of the tracked player's library to their graveyard through the normal zone machinery and emits one grouped MILLED event when at least one card moves.

{
  type: "MILL",
  count: EffectValue,
  id?: string,
  player?: EffectPlayer
}

The default player is self. player: "EACH_PLAYER" mills the tracked player and treats opponent libraries as abstract. An effect can instead declare an effect-local player target:

{
  type: "MILL",
  target: {
    id: "player-to-mill",
    type: "player",
    player: "any"
  },
  count: 3
}

Choosing self moves tracked cards normally. Choosing an opponent is a goldfish-only no-op because opponent libraries and graveyards are not tracked. A named id captures exactly the tracked cards that were milled for later { ref: id } and moved-card count references.

Do not substitute MILL when cards enter a graveyard without an instruction saying "mill", such as while resolving Explore, Surveil, or the unchosen remainder of looked-at cards. Those actions keep their own mechanic or generic movement identity. Generic library-to-graveyard movement does not emit MILLED or satisfy a MILLED trigger.

Millikin mills as part of its mana ability cost:

Millikin

{
  type: "MILL",
  count: 1
}

TRANSFORM

Transforms an existing permanent in place without changing zones. The named face is applied before devotion and static grants are reconciled. Transforming does not emit leave-battlefield or enters-battlefield events.

{
  type: "TRANSFORM", /* New API */
  target: "SOURCE" | "GRANT_SOURCE" | { ref: string },
  face: CardName
}

Use GRANT_SOURCE when a permanent grants the resolving ability to another permanent but the granting permanent transforms, as with Dowsing Dagger.

SEARCH_LIBRARY

Searches the library and moves cards to a zone. Omit filter for an unrestricted search; provide filter only when the search states a card quality. For filtered searches, the engine owns Magic's hidden-zone fail-to-find rule and lets the pilot finish without finding a card even if a match exists. Add choice for other printed search ranges that the pilot must decide, such as “up to three.”

{
  type: "SEARCH_LIBRARY",
  count: EffectValue | { source: "ANY_NUMBER" }, /* Widened API */
  optional?: boolean, /* New API */
  player?: { /* New API */
    controllerOf: { ref: string }
  },
  shuffle?: boolean, /* New API */
  maxManaValue?: EffectValue,
  choice?: {
    minimum?: number
    constraint?: { /* New API */
      type: "MATCH_SHARED_VALUES",
      attribute: "SUBTYPE",
      values: CardSubtype[]
    } | {
      /* New API */
      type: "TOTAL_MANA_VALUE",
      maximum: EffectValue
    } | {
      /* New API */
      type: "DISTINCT_VALUES",
      attribute: "NAME"
    }
  },
  filter?: CardFilter,
  destination: "battlefield" | "graveyard" | "hand" | "library" | "library_top", /* New API */
  id?: string, /* New API; required by and only with destination: "library" */
  reveal?: true, /* New API */
}

An unrestricted search omits filter:

{
  type: "SEARCH_LIBRARY",
  count: 1,
  destination: "hand",
  shuffle: true
}

A filtered search retains it:

{
  type: "SEARCH_LIBRARY",
  count: 1,
  filter: { types: ["Creature"] },
  destination: "hand",
  shuffle: true
}

count is the maximum, or { source: "ANY_NUMBER" } for no fixed maximum. During a chosen search, the pilot selects named cards one at a time, or uses FINISH_LIBRARY_SEARCH after selecting at least minimum cards. A filtered library search always permits finishing with fewer cards under the hidden-zone fail-to-find rule. An unrestricted search to the graveyard requires selecting a card when the library is nonempty. Selected cards move immediately, including direct library-to-graveyard moves; ordered effects after the search, such as SHUFFLE_LIBRARY, resolve only after the pilot finishes.

choice.constraint.type: "MATCH_SHARED_VALUES" restricts each selection after the first to cards that share at least one listed subtype with every card already selected. A single selected card satisfies the relationship even when it has none of the listed subtypes. The engine exposes one action per legal next card and never enumerates candidate subsets.

count: { source: "ANY_NUMBER" } searches for any number of matching cards instead of a fixed maximum, such as "search your library for any number of creature cards ... and put them onto the battlefield." It always routes through the same chosen-search legality as a numeric count, so pair it with destination: "battlefield" | "graveyard" | "hand"; combine it with choice: { minimum: 0 } when the printed text allows finding nothing. The pilot repeats CHOOSE_LIBRARY_CARD for as many rounds as it wants and then uses FINISH_LIBRARY_SEARCH; nothing caps the selected count except choice.constraint, if present, or an empty set of remaining legal cards.

choice.constraint.type: "TOTAL_MANA_VALUE" caps the summed mana value of the whole selected set, rather than any single card, and is the shape "any number of creature cards with total mana value 6 or less" needs. Each offered card is checked against the constraint before it is chosen: a card is legal only when adding its mana value to the running total of cards already selected in this search would not exceed maximum. The maximum resolves once when the search begins, the same as choice.minimum.

choice.constraint.type: "DISTINCT_VALUES" with attribute: "NAME" is the shape "up to four land cards with different names" needs. The pending choice records each chosen card id, and a candidate is legal only when no card chosen so far in this search shares its name. The same check filters the offered CHOOSE_LIBRARY_CARD actions and rejects a duplicate-name request, so the engine still exposes one action per legal next card.

destination: "library" holds the found cards instead of moving them. Searching and revealing does not change zones (rule 701.19), so the cards stay in the library and the search records the selected instances under the required id, the same way LOOK_AT_LIBRARY records its viewed cards. Following effects read the group with source: { ref } and perform the only zone changes; a search that selects nothing records an empty group, so those effects move nothing. A held search always stages its choice, and an already-selected card is neither offered again nor accepted twice:

{
  type: "SEARCH_LIBRARY",
  id: "found-lands",
  count: 4,
  choice: {
    minimum: 0,
    constraint: { type: "DISTINCT_VALUES", attribute: "NAME" }
  },
  filter: { types: ["Land"] },
  destination: "library",
  reveal: true
}

maxManaValue adds a mana-value ceiling that may be computed from the resolving effect context, such as a devotion count.

optional: true exposes accept and decline actions before the search. When accepted, shuffle: true shuffles after the search finishes, including when no matching card is found. Declining performs neither the search nor the shuffle.

player.controllerOf reads the controller of a card stored by an earlier effect. A missing reference does nothing. An opponent-controlled reference also does nothing in the current goldfish model because opponent libraries are not tracked.

destination: "library_top" supports single-card top-of-library tutors. The chosen card is held outside the library while shuffle: true randomizes the remaining cards, then that exact card is put on top without emitting a zone change. The chosen card becomes the known top library card. Use reveal: true when the search instruction reveals that card; opponent-facing public information is not separately displayed in the goldfish UI.

INVESTIGATE

Creates Clue tokens. It is equivalent to "create a Clue token" but keeps Investigate card text readable in the DSL.

{
  type: "INVESTIGATE",
  count?: EffectValue
}

ADDITIONAL_COMBAT_PHASE

Queues one or more additional combat phases after the current combat. The queued combat begins directly after end of combat without an intervening main phase or untap step.

{
  type: "ADDITIONAL_COMBAT_PHASE",
  count?: EffectValue
}

The omitted count defaults to one. Multiple resolutions accumulate. At the end of combat, MOVE_TO_NEXT_COMBAT consumes one queued combat, clears mana that expires at end of combat, resets combat-local attackers and assignments, and emits a fresh BEGIN_COMBAT event. Any remaining queue is offered again after that combat; only after the queue is empty can play proceed to second main. The queue is discarded at turn end.

For a spell that adds combat only when it resolves during its controller's combat phase, use condition: { timing: { turnStep: "COMBAT" } }. The effect does nothing during either main phase or during an opponent's combat.

Fear of Missing Out combines source-specific first-attack tracking, a CARD_TYPE graveyard threshold, a resolution-time-validated untap target, and the additional-combat queue:

{
  trigger: {
    type: "ATTACKS",
    source: "SELF",
    matchingCountThisTurn: 1
  },
  condition: {
    count: {
      type: "UNIQUE",
      attribute: "CARD_TYPE",
      zone: "graveyard"
    },
    comparison: "AT_LEAST",
    value: 4
  },
  effects: [
    {
      type: "UNTAP_PERMANENT",
      target: {
        id: "fear-of-missing-out-untap-target",
        zone: "battlefield",
        filter: { types: ["Creature"] }
      }
    },
    { type: "ADDITIONAL_COMBAT_PHASE" }
  ]
}

ADDITIONAL_END_STEP

Queues one or more additional end steps after the current end step.

{
  type: "ADDITIONAL_END_STEP", /* New API */
  count?: EffectValue
}

The omitted count defaults to one, and multiple effects accumulate. The engine consumes one queued step only after the current end-step stack and all resolution choices finish, then records and emits a fresh BEGIN_END_STEP. Each additional end step gets its own triggers and stack resolution. Cleanup, maximum-hand-size discards, and expiration of until-end-of-turn effects wait until the queue is empty.

The queue and its in-progress transition reset at the start of each individual player's turn. The engine advances one end step at a time through stack settlement, so a required target or other choice pauses the turn before the next end step begins.

ADDITIONAL_LAND_PLAY

As an effect, grants additional land plays for the current turn.

{
  type: "ADDITIONAL_LAND_PLAY",
  amount: EffectAmount /* Widened API */
}

The effect resolves dynamic amounts in the current spell or ability context. For example, an X spell can grant one additional land play per chosen X:

{
  type: "ADDITIONAL_LAND_PLAY",
  amount: { value: { variable: "X" } }
}

Resolved amounts are rounded down and clamped to zero before being added to the current turn's land-play allowance.

LANDS_ENTER_TAPPED

Makes lands controlled by the resolving player enter tapped for the rest of the current turn.

{
  type: "LANDS_ENTER_TAPPED", /* New API */
  controller: "SELF",
  until: "end of turn"
}

The modifier applies through the shared battlefield-entry path, including lands played normally and lands moved onto the battlefield by effects. It is cleared during end-of-turn cleanup. A modeled LANDS_ENTER_UNTAPPED static ability is applied afterward, representing the controlled player's favorable ordering when both entry modifiers apply.

Example, Broken Bond lets the pilot choose whether to put a land from hand onto the battlefield:

Broken Bond

{
  type: "MOVE_CARD",
  from: "hand",
  to: "battlefield",
  count: 1,
  choice: true,
  optional: true,
  filter: { types: ["Land"] }
}

Notes:

  • library_top and library_bottom are destination aliases, not long-lived zones.
  • choice: true chooses exactly count cards. The object form chooses from minimum through maximum; omit maximum to make every current candidate eligible. Set order: true when the pilot also orders the selected cards for their destination. When more than one card may be selected, the engine automatically stages the choice as individual select and deselect decisions followed by an explicit finish decision. Destination ordering is then staged one card at a time. This execution detail does not require a distinct DSL shape and keeps legal-action generation proportional to the candidate pool.
  • all: true moves every matching candidate. With order: true, it presents a staged destination-order choice when two or more cards remain.
  • optional: true exposes a decline action when a choice is pending.

ADD_MANA

Adds mana to the player's floating mana pool. The amount may use normal effect values. An optional card filter restricts that mana to paying for matching spells. Use colourless, not generic, when the effect adds {C}:

{
  type: "ADD_MANA",
  mana: {
    any_one_colour: 2 /* Widened API */
  },
  constraint: { /* New API */
    anyTypes: ["Instant", "Sorcery"]
  }
}

Constrained mana is included only when the engine evaluates or pays for a matching spell. It may combine with unrestricted floating mana and mana abilities, is consumed during payment, and empties at normal phase boundaries. Omit constraint for unrestricted mana.

any_one_colour adds the stated amount as one same-color group. For example, any_one_colour: 2 can pay {U}{U} or {R}{R}, but not {U}{R}. A constrained same-color group keeps both its spell filter and its grouping until it is spent or the pool empties.

RETAIN_MANA

RETAIN_MANA preserves mana as steps and phases end. Its effect form refers to a batch created by an earlier unrestricted ADD_MANA effect and gives that batch an explicit duration:

[
  {
    type: "ADD_MANA",
    id: "cast-trigger-mana", /* New API */
    mana: { red: 1 }
  },
  {
    type: "RETAIN_MANA", /* New API */
    mana: { ref: "cast-trigger-mana" },
    until: "end of turn"
  }
]

Use until: "end of combat" for mana that survives combat step boundaries but empties when the game leaves combat. Use until: "end of turn" for mana that survives every step and phase boundary during the turn.

Only the referenced mana is retained. Spending from a pool containing both ordinary and retained mana spends ordinary mana first, then mana retained until end of combat, then mana retained until end of turn. The retained mana survives intervening step and phase boundaries but empties when its duration ends.

The static-ability form continuously preserves matching mana while its source remains on the battlefield:

staticAbilities: [
  {
    type: "RETAIN_MANA",
    mana: {
      colors: ["red"]
    }
  }
]

Omit mana to preserve all unspent mana. Static retention has no until field because the source being active determines its duration. At turn end, mana retained by a resolving effect expires, while mana covered by an active static ability remains in the pool.

EARTHBEND

EARTHBEND is the first-class keyword action for “Earthbend N.” It targets a land you control and resolves its count through the normal effect-value API:

{
  type: "EARTHBEND", /* New API */
  id?: "earthbent-land",
  count: 2,
  target: "TARGET_PERMANENT"
}

On resolution, the target must still be a land controlled by the player. It remains a land, becomes a 0/0 creature with haste, and receives the resolved number of +1/+1 counters through normal counter-placement modifiers and events. A successful action may store the land under id, and then emits the EARTHBEND event.

The animated characteristics last only for that battlefield object. If it dies or is exiled, a triggered ability returns that exact card tapped if it is still in the expected zone when the trigger resolves. Moving it to hand or library does not return it. The returned object is an ordinary tapped land; its counters, animation, haste, and return abilities have cleared. Repeating Earthbend on the same object adds more counters and another pair of return triggers.

PUT_COUNTER

Places counters on a battlefield permanent or the player. Use the same counter shape for a resolving effect and its lower-camel-case cost form:

{
  type: "PUT_COUNTER",
  target?: "SOURCE" | "EVENT_CARD" | "PLAYER_SELF" |
    PermanentEffectTarget,
  permanents?: PermanentSet,
  counter:
    | { type: "lore", amount: 1 }
    | { from: "EVENT_CARD" },
  optional?: boolean
}

cost: {
  putCounter: {
    source: "SELF",
    counter: { type: "loyalty", amount: 1 }
  }
}

target and permanents are mutually exclusive. When target is omitted, the effect can consume its containing spell or ability's target context as before. permanents instead applies the placement to every permanent returned by its battlefield query without targeting them.

PLAYER_SELF stores the resolved counters in game.playerCounters after applying active player-targeted replacement modifiers. PUT_COUNTER emits the matching post-placement event. Sagas use permanent events to check their new lore total and trigger the appropriate chapter; player events do not match permanent-only listeners.

A permanent with one or more positive supported keyword counters gains the corresponding effective keyword. The current registry supports indestructible and lifelink counters. Destroy instructions may still legally target a permanent with an indestructible counter, but do not destroy it while a counter remains. Damage dealt by a source with a lifelink counter gains life normally. Removing the last counter of that type removes the effective keyword.

{ from: "EVENT_CARD" } is placement-only. It reads every positive counter kind from the event card's last-known snapshot and copies each kind onto every referenced battlefield target. It does not remove counters from that snapshot. Each placement applies active modifiers and emits its normal final PUT_COUNTER event. Missing event context or battlefield targets does nothing.

optional: true exposes accept and decline actions before placing the counters. Accepting resolves the normal placement, including replacement modifiers and its PUT_COUNTER event; declining places nothing. Effects later in the same spell or ability continue after either choice.

For a non-targeting instruction that chooses one permanent as the effect resolves, put a query-backed choice directly on PUT_COUNTER. The selected permanent is stored under the choice id, so following effects can reuse it by reference:

{
  type: "PUT_COUNTER",
  choice: {
    id: "chosen-creature",
    findCards: {
      zone: "battlefield",
      filter: { controller: "SELF", types: ["Creature"] }
    }
  },
  counter: { type: "+1/+1", amount: 1 }
}

This is a resolution-time choice, not a Magic target, and is therefore not part of cast-time target selection. Each occurrence clears and replaces the stored reference. With no legal candidate, the effect does nothing and later effects continue.

REMOVE_COUNTER

Removes up to the resolved amount of the requested counter type from each matching battlefield permanent. It shares PUT_COUNTER's effect targets; its cost form is source-only because costs do not target.

{
  type: "REMOVE_COUNTER",
  target?: "SOURCE" | "EVENT_CARD" | PermanentEffectTarget,
  permanents?: PermanentSet,
  counter: { type: "lore", amount: 1 }
}

cost: {
  removeCounter: {
    source: "SELF",
    counter: { type: "loyalty", amount: 4 }
  }
}

target and permanents are mutually exclusive. When target is omitted, the effect can consume its containing spell or ability's target context. The permanent-set form removes the resolved amount from every permanent returned by the query.

Successful removals emit REMOVE_COUNTER with the actual type and amount removed. Removing more counters than exist reports only the amount that was present; removing zero emits nothing. See the event section for last-counter checks.

At stack-settlement and cleanup boundaries, the state-based-action pass removes opposing power/toughness counters in pairs. If a permanent has N +1/+1 counters and M -1/-1 counters, the engine removes min(N, M) of each as one atomic mutation. Both removal events observe the same final permanent state. Creatures already doomed by nonpositive toughness leave first and retain both counter kinds in their last-known battlefield state.

MOVE_COUNTER

Moves a chosen distribution of counters from the source permanent to cards returned by findCards:

{
  type: "MOVE_COUNTER",
  from: "SOURCE",
  to: {
    findCards: {
      zone: "battlefield",
      exclude: "SOURCE",
      filter: { types: ["Creature"] }
    }
  },
  counter: {
    type: "+1/+1",
    amount: {
      target: "SELF",
      counters: "+1/+1"
    }
  },
  distribution: "ANY"
}

counter.amount is the maximum available to move. distribution: "ANY" allows zero through that maximum to be divided among any number of returned cards. The engine snapshots the query result, then asks for one recipient and one amount at a time. Each recipient is chosen at most once, and the pilot may finish while counters remain on the source.

Allocation is atomic: counters do not change while choices are pending. On completion, the engine removes the allocated raw total from the source and places one allocation on each still-valid recipient before resulting triggers resolve. Counter-placement modifiers apply at each destination, so they may increase the amount received without increasing the amount removed.

The query-backed targeted variant chooses one source and one destination when the spell or ability is created, then chooses the counter kind as it resolves:

{
  type: "MOVE_COUNTER",
  from: {
    id: "counter-source",
    findCards: {
      zone: "battlefield",
      filter: {
        controller: "SELF",
        counters: { type: "any", minimum: 1 }
      }
    },
    choice: true
  },
  to: {
    id: "counter-destination",
    findCards: {
      zone: "battlefield",
      exclude: { ref: "counter-source" }
    },
    choice: true
  },
  counter: { type: "any", amount: 1, choice: true }
}

Each endpoint is a single-card choice from its CardQuery. A destination query without a self-controller restriction also offers the assumed opponent creature. Both chosen objects are revalidated on resolution. The engine then offers every positive counter kind still on the source, removes exactly one of the selected kind, and places one on the destination through normal placement modifiers and events. If either target is no longer legal, nothing is removed or placed.

The targeted all-counter variant moves the complete settled counter bundle to one permanent:

{
  type: "MOVE_COUNTER",
  from: "SOURCE",
  to: {
    id: "counter-recipient",
    zone: "battlefield",
    filter: { types: ["Creature"] }
  },
  counter: "ALL",
  optional: true
}

The target declaration belongs to MOVE_COUNTER; do not put a sibling target on the containing triggered or activated ability. A named to target is chosen when the spell or ability is created and revalidated on resolution. The engine snapshots every positive counter amount by kind, removes those raw amounts from the source, and places them on the target through normal counter placement. Destination modifiers therefore apply without changing the amount removed. Missing or departed objects, an illegal target, and moving to the source itself are no-ops that leave the source counters untouched.

The query-backed many-to-one variant moves any chosen subset of counters from any number of matching permanents to one destination:

{
  type: "MOVE_COUNTER",
  from: {
    findCards: {
      zone: "battlefield",
      filter: {
        controller: "SELF",
        types: ["Creature"],
        counters: { type: "any", minimum: 1 }
      }
    }
  },
  to: "TARGET_PERMANENT",
  counter: "ANY",
  choice: "ANY"
}

The engine snapshots the source query as the effect resolves and excludes the destination from that source set. The player may finish immediately to move zero counters, or repeatedly choose a source permanent, one counter kind on that source, and an amount. Each source-and-kind pair is selected at most once. Counters do not change while choices are pending.

On completion, the engine removes the chosen raw amounts from every still-valid source, aggregates the amounts by counter kind, and places each kind on the still-valid destination. Counter-placement modifiers apply to those destination placements without changing the amounts removed. Normal removal and placement events are emitted, and later effects wait until the choice is complete.

A triggered ability can instead move a resolved amount of one counter type to the permanent that caused its event:

{
  type: "MOVE_COUNTER",
  from: "SOURCE",
  to: "EVENT_CARD",
  counter: {
    type: "+1/+1",
    amount: 1
  },
  optional: true
}

This event-card form is non-targeting. Both permanents must still be on the battlefield when it resolves, and the source cannot also be the event card. The engine caps the resolved amount at the counters currently available, removes that raw amount from the source, and places it on the event card through normal counter-placement handling. Destination modifiers can therefore change the amount received without changing the amount removed.

optional: true exposes accept and decline actions before either immediate move. Either choice resumes later effects. The pilot prefers counter recipients by the shared Ramp, Draw, Payoff, then Synergy role order. It accepts a bundle with at least one counter other than -1/-1, age, finality, or stun; unknown counter kinds are treated as beneficial.

Forgotten Ancient composes its upkeep ability directly from the upkeep event, the shared count API, and this query-backed effect:

{
  trigger: {
    type: "BEGIN_UPKEEP",
    player: "SELF"
  },
  effects: [{
    type: "MOVE_COUNTER",
    from: "SOURCE",
    to: {
      findCards: {
        zone: "battlefield",
        exclude: "SOURCE",
        filter: { types: ["Creature"] }
      }
    },
    counter: {
      type: "+1/+1",
      amount: { target: "SELF", counters: "+1/+1" }
    },
    distribution: "ANY"
  }]
}

PROLIFERATE

PROLIFERATE uses findCards to identify eligible permanents and separately lists eligible players. The engine snapshots each candidate's counter kinds, then offers one action per remaining candidate plus an action to finish:

{
  type: "PROLIFERATE",
  id?: string,
  permanents: {
    findCards: {
      zone: "battlefield",
      filter: {
        controller: "SELF",
        counters: { type: "any", minimum: 1 }
      }
    }
  },
  players: ["SELF"]
}

Any number of candidates, including zero, may be selected. Finishing gives each selected permanent or player one additional counter of every kind in its snapshot. Counter-placement modifiers apply normally. No counters change while choices are pending, and effects following PROLIFERATE wait until the choice has finished.

When id is present, the engine stores the selected permanents that actually received at least one counter under that card reference. Players are not included. The result is an empty array when no permanent received a counter. Within PROLIFERATE, this differs from permanents.findCards.id, which stores the complete initial candidate snapshot before any proliferate choices are made.

The current goldfish implementation offers only controlled permanents and the self player. The pilot selects candidates unless all their counters are known to be harmful; unknown counter kinds are treated as beneficial.

SACRIFICE_PERMANENT

Sacrifices a battlefield permanent as a resolving game action. This is the uppercase effect form; use sacrificePermanent inside a cost instead.

{
  type: "SACRIFICE_PERMANENT",
  id?: string,
  player?: "SELF" | "EACH_OPPONENT" | "EACH_PLAYER",
    /* Widened API; counted choice only */
  target: "SELF" | "SOURCE" | { filter?: CardFilter } |
    BattlefieldCardQueryChoice, /* Widened API */
  choice?: true | "ANY_NUMBER", /* Widened API */
  count?: EffectValue, /* New API */
  selection?: { /* New API; counted choice only */
    attribute: "MANA_VALUE" | "POWER",
    order: "GREATEST"
  }
}

For example, an end-step delayed ability can sacrifice its own source:

{ type: "SACRIFICE_PERMANENT", target: "SELF" }

Filtered candidates are permanents controlled by the effect's controller. When id is authored, resolution initializes that card reference empty and stores the sacrificed permanent's last-known battlefield information only when the sacrifice succeeds. This also works through choice: true; the pending choice retains the original effect context and resumes later effects afterward.

With choice: true, a filtered sacrifice may specify count. The engine resolves that value, clamps it to the number of eligible permanents, and stages individual select and deselect actions followed by an explicit finish action. Nothing is sacrificed until the completed selection is finished, then the selected permanents are sacrificed simultaneously. When id is present, the stored result is the complete sacrificed group; a resolved count of zero stores an empty group. This models instructions such as “Sacrifice X lands,” including the rule that an impossible count sacrifices every eligible permanent while later effects can still use the original X independently.

player: "EACH_PLAYER" keeps the tracked player's counted selection in that same linear select, deselect, and finish flow. The tracked player selects exactly the resolved count, or every eligible permanent when they control fewer. Each simulated opponent then contributes the same resolved count. Tracked opponent permanents are used first. The engine fills any shortfall with distinct permanents based on the standard assumed opponent permanent, with an exact owner, controller, instance ID, and zone-object identity for each opponent. The engine completes every player's selection before moving anything. It then moves the whole batch simultaneously and emits each permanent's movement, graveyard-entry, death, and sacrifice events after the batch has left the battlefield.

player: "EACH_OPPONENT" stages no tracked-player choice. The engine selects the requested count independently for each simulated opponent. It uses eligible tracked permanents controlled by that opponent, then fills a shortfall with distinct assumed permanents owned and controlled by that exact opponent. The engine completes every opponent selection before moving the group simultaneously through the same event-producing zone-change path used by EACH_PLAYER.

Counted sacrifices can restrict selection to the greatest MANA_VALUE or POWER with { attribute: "MANA_VALUE" | "POWER", order: "GREATEST" }. The engine first applies the target filter, then keeps every candidate tied for the greatest selected attribute. POWER uses current power, including base stat changes, counters, and continuous or temporary modifiers.

For EACH_OPPONENT, the engine computes that maximum separately for each opponent across tracked eligible permanents and one eligible assumed permanent. The assumed permanent uses the standard abstract characteristics; it cannot displace a tracked permanent with a greater known value, but it wins a tie. If the selected group is still too small, the normal assumed-permanent fallback fills it. Other permanent types and lower-valued eligible permanents are not legal choices.

With choice: "ANY_NUMBER", the engine stages a variable-size selection. It offers one select action for each unselected currently legal permanent, one deselect action for each selected permanent, and an explicit finish action at every stage, including when nothing is selected. Selection and deselection do not move permanents. Legal-action generation remains linear in the candidate count and never enumerates subsets.

Finishing revalidates the complete selection against the original permanent objects and the current sacrifice filter. If any selected permanent is stale, unavailable, or no longer legal, the engine rejects the whole completion and sacrifices nothing. Otherwise it sacrifices the selected permanents as one simultaneous group through the ordinary zone-change, death-trigger, and sacrifice-event handling. When id is present, the engine stores that exact successfully sacrificed group, including [] when the player finishes with zero selections. It resumes following effects with the same resolution context, so a later COUNT over { ref: id } reads the completed group size.

Use that result with refExists for an "if you do" payload. Keeping the sacrifice and payloads in one flattened effect list makes failure skip every gated payload without creating another triggered ability:

[
  {
    type: "SACRIFICE_PERMANENT",
    id: "sacrificed-source",
    target: "SOURCE"
  },
  {
    type: "DRAW_CARDS",
    amount: 1,
    condition: { refExists: "sacrificed-source" }
  }
]

A query-backed battlefield target is chosen when the spell or ability is created and revalidated as it resolves. Unlike choice: true on the effect, this is a Magic target and does not pause resolution for a new choice:

{
  type: "SACRIFICE_PERMANENT",
  target: {
    id: "artifact-to-sacrifice",
    choice: true,
    findCards: {
      zone: "battlefield",
      filter: { controller: "SELF", types: ["Artifact"] }
    }
  }
}

SIMULTANEOUS

Resolves a bounded group of targeted zone changes as one instruction without an intervening player choice. The initial public shape supports query-targeted SACRIFICE_PERMANENT and targeted MOVE_CARD; add other operations only when the engine can preserve their rules meaning without exposing an intermediate choice.

{
  type: "SIMULTANEOUS", /* New API */
  requireAllTargetsLegal?: true,
  effects: [
    {
      type: "SACRIFICE_PERMANENT",
      target: BattlefieldCardQueryChoice
    },
    {
      type: "MOVE_CARD",
      target: CardQueryChoice,
      to: MoveCardZone
    }
  ]
}

Every nested target is chosen when the containing spell or ability is created and revalidated before the group begins. With requireAllTargetsLegal: true, the group does nothing unless every declared nested target is still legal. Otherwise, normal partial-target resolution applies. Nested effects execute in authored order inside the uninterrupted group; no nested effect may introduce an optional or resolving choice. Activated-ability enumeration is bounded to the first 4,096 combined target tuples in deterministic zone order.

Goblin Welder uses the required-all form so removing either target before resolution prevents both the sacrifice and the return.

DEAL_DAMAGE

Deals damage to a legal damage target. Damage to permanents emits damage events, can destroy lethal creatures, and removes loyalty counters from planeswalkers. Damage to an opponent player also records opponent damage and opponent life loss for goldfish trackers.

Current shape:

type DealDamageEffect = {
  type: "DEAL_DAMAGE",
  id?: string,
  amount: EffectValue,
  optional?: boolean,
  preventable?: false, /* New API; omitted means preventable */
  source?: "TRIGGERING_SPELL" | { ref: string } | { each: PermanentSet }, /* Widened API */
} & (
  | {
    target:
      | "OPPONENT"
      | "EACH_OPPONENT" /* New API */
      | "PLAYER_SELF" /* New API */
      | "SOURCE"
      | "TARGET_PERMANENT"
      | "TARGET_PLAYER"
      | { ref: string }
      | {
        id: string,
        type: "any" | "creature" | "creature_or_player" | "player", /* New API: "any" */
        controller?: "self" | "opponent" | "any",
        player?: "self" | "opponent" | "any",
        optional?: boolean,
        count?: number | { /* Widened API */
          min: number,
          max: number
        } | {
          exact: EffectValue
        },
        division?: "AS_CHOSEN" /* New API */
      },
    permanents?: never
  }
  | {
    target?: never,
    permanents: PermanentSet /* Widened API */
  }
)

Some effects make each permanent in a set deal damage as a separate source. Pair source.each with target: "SOURCE_CONTROLLER" to route each damage assignment back to that source permanent's controller:

{
  type: "DEAL_DAMAGE",
  amount: 1,
  source: { /* New API */
    each: {
      findCards: {
        zone: "battlefield",
        filter: { types: ["Creature"] }
      }
    }
  },
  target: "SOURCE_CONTROLLER" /* New API */
}

The engine snapshots the tracked source set at resolution. Each source emits its own damage event, uses its exact controller and opponent id, and applies prevention and Lifelink independently. Damage from an opponent-controlled source does not receive the goldfish player's noncombat damage modifiers and cannot gain life for the goldfish player. This form never creates an assumed opponent permanent.

EACH_OPPONENT applies one simultaneous damage instruction to every configured simulated opponent and emits a separate damage event carrying that opponent's id. Lifelink uses the source creature's effective printed or granted keywords, then gains life once from the total damage actually dealt by the instruction. Combat damage similarly totals actual lifelink damage before gaining life. Use source: "TRIGGERING_SPELL" on a cast-triggered ability when the triggering spell, rather than the permanent supplying the trigger, deals the damage. Damage events, modifiers, and lifelink then use that spell as the source.

Use source: { ref: string } (/* Widened API */) when a prior target or choice names the permanent that deals the damage. The resolver uses the exact captured instance for damage events and Lifelink, including its last-known characteristics, rather than attributing the damage to the resolving spell. An effect-local permanent target's id can be that ref: a legal selected permanent is exposed under its target id at resolution, so "It deals damage equal to its power to target creature you don't control" reads both source and amount from the first target. A target cleared as illegal at resolution exposes no ref, so the dependent damage finds no source and deals none. Damage targets with controller: "opponent" offer tracked opponent creatures as well as the assumed opponent permanent; controller: "self" excludes tracked opponent creatures.

Example, Kabira Takedown deals damage equal to controlled creatures:

Kabira Takedown

{
  type: "DEAL_DAMAGE",
  amount: {
    source: "CONTROLLED_PERMANENTS",
    filter: { types: ["Creature"] }
  },
  target: {
    id: "damage-target",
    type: "creature",
    controller: "any"
  }
}

Divided damage uses a staged target declaration so the engine does not materialize the Cartesian product of every allocation and every other target group on the spell:

{
  type: "DEAL_DAMAGE",
  amount: 4,
  target: {
    id: "divided-damage-targets",
    type: "any", /* New API */
    count: { min: 0, max: 4 }, /* New API */
    division: "AS_CHOSEN" /* New API */
  }
}

Fixed damage to a counted set of targets uses the same staged selection without division. The full amount is dealt to each selected target:

{
  type: "DEAL_DAMAGE",
  amount: 1,
  target: {
    id: "counted-damage-targets",
    type: "any",
    controller: "any",
    player: "any",
    count: { min: 1, max: 2 } /* New API */
  }
}

An exact counted target total may resolve from an EffectValue. The cast's chosen X and paid additional costs are available while the staged choice is built. For example, a spell that requires one target plus one for each payment can use:

count: {
  exact: {
    operation: "ADD",
    value: 1,
    amount: { source: "KICKER_COSTS_PAID", id: "example" }
  }
}

Damage to every matching permanent uses a non-targeting permanent set:

{
  type: "DEAL_DAMAGE",
  amount: 13,
  permanents: {
    findCards: {
      zone: "battlefield",
      filter: { types: ["Creature"] }
    }
  }
}

The set is snapshotted when the effect resolves. Each tracked permanent receives the full damage amount through the ordinary permanent-damage pipeline before state-based actions are performed. This form does not generate target choices and affects permanents with Shroud. Lethally damaged creatures are moved by state-based actions after the instruction finishes; effective Indestructible keeps a creature on the battlefield while leaving its damage marked.

Notes:

  • id records the positive damage actually dealt after current modifiers. Later effects can read it with { result: { ref: id, attribute: "DAMAGE_DEALT" } }. Missing, invalid, declined, prevented, or nonpositive damage records zero.
  • Use target: "OPPONENT" for non-targeted damage to each opponent; it emits the abstract per-opponent damage amount used by goldfishing.
  • Use target: "PLAYER_SELF" for non-targeted damage to the player controlling the effect's source.
  • Use DEAL_DAMAGE, not a target-specific damage effect name, for new definitions.
  • target and permanents are mutually exclusive. A permanents.findCards recipient is non-targeting.
  • Damage to the self player is finalized through one prevention-aware pipeline. Permanent recipients use the same prevention-aware finalization path. preventable: false bypasses prevention. The engine records attempted, prevented, and actual damage separately; only actual damage emits DAMAGE_DEALT, changes life or loyalty, marks damage, or contributes to lifelink.
  • optional: true is currently used by effects like Requiem Monolith where the pilot may choose whether to deal the damage.
  • Every divided-damage assignment must be a positive integer, each selected target must be distinct, and the assignments must total amount. A zero-target declaration is legal when count.min is zero.
  • Every counted-damage target must be distinct. When division is omitted, the full amount is dealt to each selected target.
  • A counted-damage choice may select multiple abstract opponent creatures. Each selection represents a distinct opponent-owned creature. The engine emits the exact damage dealt to each one, then assumes any positive damage that was not prevented is lethal and moves that creature through the ordinary battlefield-to-graveyard path. Zero or fully prevented damage does not cause that assumed death. Tracked opponent creatures retain their actual characteristics and use normal state-based damage handling.
  • If one or more targets become illegal, counted damage resolves against every remaining legal target. It fizzles only when all selected targets are illegal.
  • Lethal damage remains marked while the current spell or ability finishes resolving. State-based actions move lethally damaged creatures afterward, including after an in-resolution cast-or-decline choice is completed.
  • type: "any" offers players and every permanent type that the engine recognizes as a legal damage recipient. Newly supported permanent types therefore participate without card-definition changes.

FIGHT

Makes the source creature and another selected creature deal noncombat damage to each other equal to their current power. Both powers are captured before either assignment, and state-based lethal-damage checks occur only after both assignments have been made.

{
  type: "FIGHT", /* New API */
  source: "SOURCE",
  target: {
    id: "fight-target",
    zone: "battlefield",
    excludeSource: true,
    optional: true,
    filter: { types: ["Creature"] }
  }
}

Use excludeSource: true for "another" and optional: true for "up to one." The normal permanent-target pipeline chooses and revalidates the target. If either participant is no longer a creature on the battlefield as the effect resolves, nothing happens. Selecting the abstract opponent permanent materializes it before damage is dealt.

Fight uses the existing prevention-aware permanent-damage path, including damage events and lifelink. The engine does not currently model deathtouch changing lethal damage.

Each successfully resolved fight instruction emits one FIGHT event containing both participating creatures. A trigger filters that pair and fires once even when both creatures match:

{
  trigger: {
    type: "FIGHT", /* New API */
    filter: { controller: "SELF", types: ["Creature"] }
  },
  effects: [{ type: "DRAW_CARDS", amount: 1 }]
}

ADD_COMBAT_REQUIREMENT

Adds a temporary combat requirement to a creature. The goldfish engine models MUST_BE_BLOCKED_IF_ABLE by marking the affected creature blocked if it attacks that combat, and MUST_ATTACK_IF_ABLE by requiring the creature in an attack declaration whenever it is eligible to attack:

{
  type: "ADD_COMBAT_REQUIREMENT", /* New API */
  requirement: "MUST_BE_BLOCKED_IF_ABLE",
  target: "TARGET_PERMANENT",
  until: "end of combat"
}

// The target must attack this combat if it can.
{
  type: "ADD_COMBAT_REQUIREMENT", /* Widened API */
  requirement: "MUST_ATTACK_IF_ABLE",
  target: "TARGET_PERMANENT",
  until: "end of combat"
}

Forced blocks are declared as one abstract batch immediately before combat damage. The engine assumes enough suitable blockers exist, except that a creature with Unblockable cannot be blocked. A blocked attacker remains an attacker but deals no combat damage to the defending player. No concrete blocker, attacker-to-blocker damage, or trample overflow is created. The requirement and blocked state clear before an additional combat or second main phase. A tapped creature, a creature restricted by summoning sickness, or one with Defender is not an eligible attacker, so a must-attack requirement does not make an illegal declaration legal.

ADD_COMBAT_RESTRICTION

Declares a targeted temporary combat restriction whose gameplay consequence the goldfish engine intentionally ignores:

{
  type: "ADD_COMBAT_RESTRICTION", /* New API */
  restriction: "CANT_BLOCK",
  target: {
    id: "creature-that-cant-block",
    zone: "battlefield",
    filter: { types: ["Creature"] }
  },
  until: "end of turn"
}

The target uses ordinary permanent target selection and resolution legality. If every target of the spell becomes illegal, the whole spell does not resolve and later effects are skipped. A legal ADD_COMBAT_RESTRICTION resolves as a no-op because TurnZero does not simulate defending blockers. This preserves targeting and fizzle behavior without adding unused opponent combat state.

DESTROY_PERMANENT

Destroys a battlefield permanent. The target should live on the effect.

Current shape:

{
  type: "DESTROY_PERMANENT",
  id?: string, /* New API; targeted form only */
  target?: PermanentEffectTarget,
  permanents?: PermanentSet,
  estimatedOpponentPermanents?: true /* Widened API; mass form only */
}

Example, Murder:

Murder

{
  type: "DESTROY_PERMANENT",
  target: {
    id: "creature-to-destroy",
    zone: "battlefield",
    filter: { types: ["Creature"] }
  }
}

Example, Broken Bond:

Broken Bond

{
  type: "DESTROY_PERMANENT",
  target: {
    id: "artifact-or-enchantment",
    zone: "battlefield",
    filter: { anyTypes: ["Artifact", "Enchantment"] }
  }
}

Notes:

  • Prefer effect-owned targets over container-level targets for new destroy effects.
  • On the targeted form, id stores the destroyed permanent's last-known battlefield object only when destruction succeeds. Missing, illegal, and Indestructible targets leave the reference absent. Later effects can use the reference with type: "SUM" and source: { ref: id }.
  • SELF can be used for source self-destruction where supported by the target type.
  • target and permanents are mutually exclusive. permanents.findCards destroys every matching tracked permanent without targeting. All destructible permanents in that set leave simultaneously; selected spell modes still resolve sequentially in printed order.
  • When permanents.findCards.id is present, the reference stores last-known snapshots of only the permanents actually destroyed. Later effects can aggregate that group with type: "COUNT" or type: "SUM" and source: { ref: id }.
  • Permanents with effective Indestructible are not destroyed.
  • A targeted assumed opponent permanent is materialized with its opponent owner, controller, and battlefield object identity before destruction. It then uses the normal battlefield-to-owner-graveyard path, including movement history, MOVE_CARD, death events, Morbid, and active modeled death triggers. Resolution revalidates the declared target, and Indestructible still prevents the destruction.
  • estimatedOpponentPermanents: true also destroys every matching permanent on the simulated opponent boards, which hold the permanent spells opponents have resolved (see Opponent Model). Each destroyed board permanent emits normal battlefield-to-graveyard events and leaves its board, so later effects see only what opponents cast afterwards. Tracked opponent permanents are separate objects and are destroyed as usual.

EXILE_PERMANENT

Exiles one or more declared battlefield permanents, or every permanent in a non-targeting battlefield set. A declared target uses the shared permanent-target selection and resolution-time revalidation machinery. An optional id stores the exact exiled object or group only when the exile succeeds, allowing later effects to move those same objects from exile. Other single-target consumers can still inspect its last-known battlefield facts such as controller through the resolution context.

Current shape:

{
  type: "EXILE_PERMANENT", /* New API */
  id?: string,
  target: PermanentEffectTarget,
  permanents?: never
} | {
  type: "EXILE_PERMANENT",
  id?: never,
  target?: never,
  exempt?: PermanentEffectTarget, /* New API */
  permanents: PermanentSet /* New API */
}

Example, Swords to Plowshares:

{
  type: "EXILE_PERMANENT",
  target: {
    id: "creature-to-exile",
    zone: "battlefield",
    filter: { types: ["Creature"] }
  }
}

Example, choose and exile any number of your creatures:

{
  type: "EXILE_PERMANENT",
  id: "exiled-creatures",
  target: {
    id: "chosen-creatures",
    choice: "ANY_NUMBER", /* New API */
    findCards: {
      zone: "battlefield",
      filter: {
        controller: "SELF",
        types: ["Creature"]
      }
    }
  }
}

Example, exile every artifact without targeting:

{
  type: "EXILE_PERMANENT",
  permanents: {
    findCards: {
      zone: "battlefield",
      filter: { types: ["Artifact"] }
    }
  }
}

Notes:

  • The target is selected through the normal effect-owned target flow and is revalidated when the effect resolves.
  • A declared permanent target's count accepts either an exact number or a { min, max } range. Counted spell targets are selected in stages instead of expanding every target combination into a separate cast action.
  • A query-backed choice: "ANY_NUMBER" target exiles the selected legal subset simultaneously. Choosing no permanents succeeds with an empty stored group.
  • Each selected object is revalidated independently at resolution. The spell resolves against the remaining legal objects, but a spell that originally selected at least one object does not resolve when every selected object is illegal. An explicitly empty selection is not an all-targets-illegal result.
  • target and permanents are mutually exclusive.
  • exempt declares a normal target that is omitted from a permanent-set exile. An optional exempt target allows choosing no object, in which case the complete matching set is exiled.
  • permanents.findCards snapshots every matching tracked permanent and exiles that group simultaneously. When the query has an id, the stored reference contains only the permanents actually exiled after applying exempt.
  • Exile does not check Indestructible.
  • Exiling the assumed opponent permanent materializes an exact opponent-owned exile object. If it later returns, it remains a tracked opponent permanent and the generic assumed candidate remains suppressed.
  • A commander that reaches exile creates a post-move owner choice before later ordered effects continue. Self-owned commanders offer leave/move actions tied to exact zone identity; represented opponent owners use the deterministic command-zone policy.
  • EXILE_TARGET_PERMANENT remains as a compatibility effect for existing definitions. New definitions should use EXILE_PERMANENT.

EXILE_GRAVEYARD

Exiles every card in the selected player scope's graveyard.

{
  type: "EXILE_GRAVEYARD",
  player: "SELF" | "EACH_OPPONENT" | "EACH_PLAYER" /* New API */
}

SELF exiles game.graveyard. EACH_OPPONENT exiles every entry in game.opponentGraveyards and leaves game.graveyard alone, which is the scope hate pieces such as Soul-Guide Lantern need. EACH_PLAYER exiles both, the scope Farewell needs.

An opponent's graveyard only holds the cards TurnZero has actually put there, such as creatures it killed, so the scopes that read it are exact about tracked cards and silent about an opponent's unsimulated history. Use a { type: "player", player: "any" } target on MOVE_CARD instead when the card names one player rather than a scope.

DRAW_CARDS

Draws cards and emits normal draw events. Use MOVE_CARD from library to hand instead when a card says "put into your hand" and should not trigger draw-card triggers.

Current shape:

{
  type: "DRAW_CARDS",
  amount: EffectAmount,
  id?: string, /* New API */
  optional?: boolean,
  matchingCountThisTurn?: number, /* New API; requires optional: true and id */
  player?: "ACTIVE_PLAYER" | "TARGET_PLAYER" | "SELF" | "OPPONENT" |
    "EACH_OPPONENT" | { group: "OPPONENTS", count: EffectValue },
  target?: { /* New API; mutually exclusive with player */
    id: string,
    type: "player",
    player: "any" | "opponent" | "self",
    count: TargetCount | { source: "ANY_NUMBER" } /* Widened API */
  }
}

count remains a deprecated compatibility field for existing definitions. New definitions use amount.

An id on a self-draw stores the actual cards drawn under that effect reference. An accepted optional draw creates the reference even when no card can be drawn, storing []; a declined optional draw leaves it absent. Draw replacements that do not result in an actual draw add no card to the reference. Use condition: { refExists: id } for later dependent effects.

Use player: "EACH_OPPONENT" when every simulated opponent draws the stated amount. TurnZero records an abstract draw event and per-opponent count for each opponent; opponent libraries and hands are not tracked.

Use player: "ACTIVE_PLAYER" for text that instructs the player whose turn it is to draw. The tracked player draws from the tracked library on their turn. A simulated opponent produces an abstract draw event with that opponent's exact ID.

Use player: { group: "OPPONENTS", count } when a known number of otherwise interchangeable simulated opponents draw. The engine deterministically uses the first count opponent IDs, capped by the configured opponent count, and records the same abstract draw events without tracking opponent libraries or hands.

Use target when the effect targets a group of players. The engine stages one selection or deselection action per candidate and exposes finish only when the selected count satisfies count. It never enumerates player subsets. Each selected opponent keeps its simulated opponent ID when the draw event is emitted. At resolution, the effect draws once for each target that remains legal.

Example, Solemn Simulacrum dies trigger:

Solemn Simulacrum

{
  trigger: {
    type: "ENTERS",
    from: "battlefield",
    to: "graveyard",
    source: "SELF"
  },
  effects: [
    { type: "DRAW_CARDS", amount: 1 }
  ]
}

Example, Morbid Opportunist:

Morbid Opportunist

{
  trigger: {
    type: "ENTERS",
    from: "battlefield",
    to: "graveyard",
    filter: { types: ["Creature"] },
    another: true,
    matchingCountThisTurn: 1
  },
  effects: [
    { type: "DRAW_CARDS", amount: 1 }
  ]
}

LOSE_LIFE

Activated abilities can declare any number of player targets on this effect:

{
  type: "LOSE_LIFE",
  amount: 2,
  target: { /* New API */
    id: "selected-players",
    type: "player",
    player: "any",
    count: { source: "ANY_NUMBER" }
  }
}

The activation stages select, deselect, finish, and cancel actions before paying costs. Each selection stores exact player identities under effectTargets[id].targetPlayers: "self" or a simulated opponent ID. Selection is linear in the number of candidates and permits an empty set. Use player: "self" or "opponent" in the declaration to restrict eligibility. Finishing revalidates the complete set, then proceeds to any staged sacrifice cost. Resolution removes illegal targets; if every originally selected target is illegal, none of the ability resolves. An originally empty set resolves normally, including subsequent mana and draw effects.

A following counted SACRIFICE_PERMANENT can use player: { ref: "selected-players" } to require a sacrifice from those same players. It uses the existing opponent sacrifice assumptions for each exact selected opponent. If self is selected, the tracked player chooses their creature before all selected players sacrifice simultaneously. Losing life does not make the later sacrifice, mana, or draw conditional on life lost.

Updates the self player's running lifeTotal, records the amount lost this turn, and emits a matching LOSE_LIFE event. Opponent life totals remain abstract in goldfishing.

{
  type: "LOSE_LIFE",
  amount: EffectAmount,
  id?: string /* New API */,
  optional?: boolean /* New API */,
  player?: "TARGET_PLAYER" | "SELF" | "OPPONENT" |
    "EACH_OPPONENT" | /* New API */
    { group: "OPPONENTS", count: EffectValue } /* Widened API */
}

Direct dynamic EffectValue entries remain deprecated compatibility shapes. New definitions wrap counts as { count } and contextual scalars as { value }.

Use player: "EACH_OPPONENT" for new definitions with non-targeted life loss applied to every opponent. The goldfish engine represents that instruction with its abstract opponent life-loss event. OPPONENT remains a compatibility spelling for existing definitions.

Use { group: "OPPONENTS", count } when an effect applies to a counted subset. The engine uses the deterministic first-N simulated opponents—the same convention as counted opponent draws—and records life loss for exactly those opponent identities.

When id is present, the effect records the resolved amount for each affected player under LIFE_LOST. A later effect can sum those results:

{
  type: "SUM",
  values: {
    result: {
      ref: "life-loss",
      attribute: "LIFE_LOST"
    }
  }
}

An optional LOSE_LIFE creates an accept-or-decline choice. Accepting a named effect stores an empty reference marker under its id, so later effects can use condition: { refExists: id }; declining leaves that reference absent.

Use LOSE_LIFE for both the effect and its trigger event:

trigger: {
  type: "LOSE_LIFE",
  player: "OPPONENT",
  matchingCountThisTurn: { count: 1 }
}

player accepts "EACH", "OPPONENT", or "SELF". The object form of matchingCountThisTurn compares against recorded life-loss events from the current turn, so { count: 1 } means the first matching life-loss event.

FOR_EACH_OPPONENT

Resolves one effect group for each configured simulated opponent. Opponents run in ID order, from 1 through the configured count.

{
  type: "FOR_EACH_OPPONENT", /* New API */
  effects: Effect[]
}

Each opponent gets a separate child resolution context. Card, moved-card, value, and effect-result refs copy the parent values at the start of an iteration, but writes stay inside that iteration. A ref produced for opponent 1 cannot satisfy an effect for opponent 2. After the last opponent, resolution returns to the parent context and continues with the effects after the wrapper.

Nested effects read the current opponent from eventOpponentId. Opponent payment policies receive the same ID. If a nested effect pauses for a player choice, the choice keeps that iteration's context and remaining effects. Finishing the choice completes the current opponent before the engine starts the next one.

PAY_COST

Gates nested effects behind a cost payment. Use this for "you may pay... if you do" triggered or spell text. The nested effects only resolve after the cost is paid, so the order is explicit in the DSL.

Current shape:

{
  type: "PAY_COST",
  cost?: { /* Widened API */
    mana?: {
      black?: EffectValue,
      blue?: EffectValue,
      colourless?: EffectValue,
      condition?: ManaCost["condition"],
      generic?: EffectValue,
      green?: EffectValue,
      hybrid?: Array<{ /* New API */
        colours: [ManaColor, ManaColor],
        count: number
      }>,
      red?: EffectValue,
      white?: EffectValue
    },
    moveCard?: {
      id: string,
      from: MoveCardZone,
      to: MoveCardZone,
      count: EffectValue,
      filter?: CardFilter
    } | {
      id: string,
      from: MoveCardZone,
      to: MoveCardZone,
      filter?: CardFilter,
      selection: { /* New API */
        minimum: number,
        maximum: "PAYMENT_REMAINDER"
      },
      payFor: { /* New API */
        amount: number,
        generic: true
      }
    },
    discardCard?: {
      source: "SELF"
    } | {
      id: string,
      count: EffectValue,
      filter?: CardFilter
    },
    sacrificePermanent?: {
      source: "SELF"
    } | {
      filter: CardFilter,
      id: string
      count?: EffectValue, /* Widened API */
      another?: true
    },
    loseLife?: { amount: EffectValue },
    removeCounter?: {
      source: "SELF",
      counter: CounterEffect
    },
    putCounter?: {
      source: "SELF",
      counter: CounterEffect
    },
    x?: {
      min?: number,
      max: EffectValue
    }
  },
  effects: Effect[],
  player?: "OPPONENT", /* New API */
  opponentChoiceId?: string, /* New API */
  optional?: boolean,
  target?: {
    allowPlayers: true,
    id?: string
  }
}

Omit cost for a costless optional choice. This represents a plain “may” decision while reusing the same accept-or-decline flow as an optional payment:

{
  type: "PAY_COST",
  optional: true,
  effects: [
    {
      type: "UNTAP_PERMANENT",
      target: "SELF"
    }
  ]
}

Example, Well of Lost Dreams uses X in both the mana cost and the draw count:

Well of Lost Dreams

{
  trigger: {
    type: "LIFE_GAINED",
    player: "SELF"
  },
  effects: [
    {
      type: "PAY_COST",
      optional: true,
      cost: {
        mana: {
          generic: { variable: "X" }
        },
        x: {
          min: 1,
          max: { event: { attribute: "LIFE_GAINED_AMOUNT" } }
        }
      },
      effects: [
        {
          type: "DRAW_CARDS",
          count: { variable: "X" }
        }
      ]
    }
  ]
}

Example, Veinwitch Coven pays before choosing a creature card from the graveyard:

Veinwitch Coven

{
  trigger: {
    type: "LIFE_GAINED",
    player: "SELF"
  },
  effects: [
    {
      type: "PAY_COST",
      optional: true,
      cost: {
        mana: { black: 1 }
      },
      effects: [
        {
          type: "MOVE_CARD",
          from: "graveyard",
          to: "hand",
          count: 1,
          choice: true,
          filter: { types: ["Creature"] }
        }
      ]
    }
  ]
}

Notes:

  • Use PAY_COST instead of trigger-level optional costs for new definitions.
  • optional: true exposes both pay and decline choices.
  • player: "OPPONENT" is limited to an optional loseLife cost and requires opponentChoiceId. It resolves through the named assumptions.opponent.choices answer, PAY or DECLINE, or an injected opponent payment policy. Each trigger occurrence asks the policy separately with the exact opponent identity and referenced cards. It never exposes that choice as a tracked-player legal action. An explicit DECLINE assumption is the default goldfish answer unless an injected policy overrides it. Opponent life totals are not tracked, so PAY emits a normal opponent LOSE_LIFE event for the accepted payment without changing tracked life, then resolves the nested effects. If the exact referenced card has already left its zone, the engine declines without charging life because the decline branch can no longer return it.
  • An omitted cost is treated as an empty cost. With optional: true, the engine still exposes both accept and decline choices; without optional, the nested effects resolve immediately.
  • cost.x enumerates payable xValue choices and makes { variable: "X" } available to nested effects. A named ref also stores the chosen value for effects that accept resolved-value references.
  • If a nested choice effect has no legal choices, the pay action is not offered.

TAP_PERMANENT

Taps one or more battlefield permanents. A declared target uses the shared permanent target-selection machinery and is revalidated when the spell or ability resolves. A permanent set deterministically taps every matching permanent without turning those permanents into targets.

{
  type: "TAP_PERMANENT", /* New API */
  target?: PermanentEffectTarget,
  permanents?: PermanentSet /* New API */
}

Example, tap a target creature:

{
  type: "TAP_PERMANENT",
  target: {
    id: "creature-to-tap",
    zone: "battlefield",
    filter: { types: ["Creature"] },
    count?: number | { min: number, max: number } /* New API */
  }
}

Example, tap every creature controlled by a targeted player:

{
  type: "TAP_PERMANENT",
  permanents: { /* New API */
    findCards: {
      zone: "battlefield",
      filter: {
        types: ["Creature"],
        controller: { ref: "target-player" } /* New API */
      }
    }
  }
}

Notes:

  • Supply exactly one of target or permanents. Use a normal declared target or an existing { ref: "..." } target reference for targeted tapping.
  • A controller { ref } resolves a player selected under that effect-target id. A missing or non-player selection matches no permanents.
  • A numeric count requires exactly that many distinct targets. The ranged form allows every distinct target count from min through max. Counted targets are selected in stages instead of expanding every combination into a separate cast action.
  • An already tapped permanent remains tapped and emits no event. An untapped permanent emits TAP_PERMANENT and BECOMES_TAPPED exactly once when this effect taps it. Permanents entering tapped do not emit either event because they never transition from untapped to tapped.
  • The assumed opponent permanent can be tapped when the declared target allows an opponent-controlled permanent. It counts as one distinct target and can be mixed with tracked permanents in the same selection.
  • Attack declaration without vigilance, tap costs, mana payments, Convoke, Enlist, and this effect emit these events for each real untapped-to-tapped transition.

UNTAP_PERMANENT

Untaps one or more battlefield permanents. Use this instead of older target-specific untap effect names.

Current shape:

{
  type: "UNTAP_PERMANENT",
  target?: PermanentEffectTarget,
  permanents?: PermanentSet,
  choice?: { /* New API */
    id: string,
    findCards: CardQuery & { zone: "battlefield" }
  },
  count?: EffectValue
}

Example, an MDFC land can pay life to untap itself as it enters:

Witch-Blessed Meadow

{
  type: "PAY_COST",
  optional: true,
  cost: { loseLife: { amount: 3 } },
  effects: [
    {
      type: "UNTAP_PERMANENT",
      target: "SELF"
    }
  ]
}

Example, Frantic Search untaps up to three lands without target selection:

{
  type: "UNTAP_PERMANENT",
  count: 3,
  permanents: {
    findCards: {
      zone: "battlefield",
      filter: { types: ["Land"] }
    }
  }
}

Notes:

  • target: "SELF" untaps the source permanent.

  • A declared permanent target belongs directly to UNTAP_PERMANENT:

    target: {
      id: "permanent-to-untap",
      zone: "battlefield",
      filter: { types: ["Land"] }
    }
    
  • target and permanents are mutually exclusive.

  • choice makes a resolution-time, non-target choice among matching battlefield permanents. It is mutually exclusive with target and permanents.

  • permanents.findCards untaps every matching tapped permanent without target selection. Optional count limits that queried set in battlefield order.

PREVENT_NEXT_UNTAP

Prevents matching permanents from untapping during the tracked player's next untap step.

Current shape:

{
  type: "PREVENT_NEXT_UNTAP", /* New API */
  player: "SELF",
  filter: CardFilter
}

Example, prevent lands you control from untapping during your next untap step:

{
  type: "PREVENT_NEXT_UNTAP",
  player: "SELF",
  filter: { controller: "SELF", types: ["Land"] }
}

Notes:

  • The engine evaluates the filter during the next self untap step after phased out permanents phase in. Permanents that enter after this effect resolves can match.
  • Matching permanents remain tapped without emitting UNTAP_PERMANENT.
  • The engine consumes every pending self prevention during that untap step, even when no permanent matches. Later untap steps proceed normally.
  • A card definition's skipsUntapStep flag remains a separate permanent-level restriction.

REMOVE_FROM_COMBAT

Removes a targeted attacking creature from the current combat without changing zones.

Current shape:

{
  type: "REMOVE_FROM_COMBAT",
  target: PermanentEffectTarget
}

Example, Reconnaissance pulls a creature out of combat before untapping it:

{
  type: "REMOVE_FROM_COMBAT",
  target: "TARGET_PERMANENT"
}

Notes:

  • The target must still be on the battlefield as the effect resolves.
  • The effect removes the creature from the current attacking set and its attack assignment, so later combat damage from that attacker is prevented if it has not already been dealt.
  • It does not model blockers or other defensive combat interactions beyond the current attacker-to-player goldfish combat flow.

PHASE_OUT

Phases one or more tracked battlefield permanents out. A phased-out permanent is not moved to another zone and is treated as though it does not exist until it phases in.

Current shape:

{
  type: "PHASE_OUT",
  target?: PermanentEffectTarget,
  permanents?: PermanentSet,
  choice?: "ANY"
}

Use exactly one of target or permanents. The permanent-set form resolves a battlefield query without declaring targets.

A spell can phase out one declared permanent:

{
  type: "PHASE_OUT",
  target: {
    id: "permanent-to-phase-out",
    zone: "battlefield",
    filter: { types: ["Creature"] }
  }
}

Or a prior effect can record several permanents under one reference and phase out the entire referenced group:

{
  type: "PHASE_OUT",
  target: { ref: "chosen-permanents" }
}

Set choice: "ANY" to let the player phase out any subset of the resolved group, including none or all of them:

{
  type: "PHASE_OUT",
  target: { ref: "countered-permanents" },
  choice: "ANY"
}

The choice only offers referenced permanents that are still on the battlefield. The engine resumes later effects after the player finishes the selection. With choice omitted, every resolved permanent phases out immediately.

Notes:

  • Phasing does not emit enter, leave, dies, or zone-movement events.
  • Counters, tapped state, controller, and other permanent state stay on the same card object.
  • Permanents attached to a permanent phase out with it and retain their attachment relationship.
  • Phased-out permanents are excluded from targeting, battlefield queries, continuous abilities, combat, and state-based-action checks.
  • At the start of their controller's next untap step, the engine phases them in before untapping permanents. This is an engine step, not a trigger, and it does not use the stack.
  • There is intentionally no PHASE_IN DSL effect.

LOOK_AT_LIBRARY

Looks privately at a resolved number of library cards. It is strictly an information primitive: it never moves cards or changes library order. With id, it records the exact viewed group so following primitive effects can reference it. All resulting card movement belongs in explicit MOVE_CARD effects.

Current shape:

{
  type: "LOOK_AT_LIBRARY",
  count: EffectValue, /* New API */
  id?: string,
  optional?: boolean
}

Planar Atlas demonstrates the separation of responsibilities. The look records the four viewed cards, one MOVE_CARD optionally returns a revealed land to the top, and another moves the remaining referenced cards to the bottom:

Planar Atlas

[
  {
    type: "LOOK_AT_LIBRARY",
    optional: true,
    id: "planar-atlas-looked-cards",
    count: 4
  },
  {
    type: "MOVE_CARD",
    source: { ref: "planar-atlas-looked-cards" },
    from: "library",
    to: "library_top",
    count: 1,
    choice: { minimum: 0, maximum: 1 },
    filter: { types: ["Land"] },
    reveal: true
  },
  {
    type: "MOVE_CARD",
    source: { ref: "planar-atlas-looked-cards" },
    from: "library",
    to: "library_bottom",
    all: true,
    randomOrder: true
  }
]

Notes:

  • Use REVEAL_TOP when Oracle says reveal.
  • LOOK_AT_LIBRARY must not select, reorder, or relocate cards. Keeping those responsibilities out of the look primitive makes zone movement use the normal legality, event, metric, and pilot-choice machinery.
  • Reference a look's id from later movement as source: { ref: id }.
  • Optional looks defer following effects. Declining leaves no referenced cards, so subsequent moves from that reference do nothing.
  • LOOK_AT_LIBRARY may also be used as a staticAbilities entry when a permanent continuously lets its controller look at the top of the library:
staticAbilities: [
  { type: "LOOK_AT_LIBRARY", count: 1 } /* New API */
]

Static look information is derived from the current library[0]; it does not persist a stale known-card id after the top card changes or the source leaves.

This is the general primitive-composition pattern, not only a Planar Atlas special case. Use SCRY or SURVEIL when the card specifically names one of those mechanics:

{ type: "SCRY", count: 2 } /* New API */
{ type: "SURVEIL", count: 2 } /* New API */

Both mechanics use the distributed-movement choice machinery internally. Destination assignment and ordering are staged, so legal actions remain bounded even for large counts. Scry's library-to-library arrangement does not emit zone-change events. For an instruction that looks at cards and arranges them without naming Scry or Surveil, compose LOOK_AT_LIBRARY with MOVE_CARD explicitly.

REVEAL_TOP

Reveals cards from the top of library. It supports single-card references, repeated reveal-until loops, and reveal-top-N selection. Public information is not separately displayed yet, but card references are preserved for later effects.

Single-card shape:

{
  type: "REVEAL_TOP",
  from: "library",
  amount?: EffectValue,
  id: string
}

One-shot branch shape:

{
  type: "REVEAL_TOP",
  from: "library",
  filter: CardFilter,
  matched: { effects: Effect[] },
  rest: {
    effects: Effect[],
    randomOrder?: true
  }
}

Repeated shape:

{
  type: "REVEAL_TOP",
  from: "library",
  repeat: true,
  optional?: boolean,
  amount?: EffectValue,
  untilMatchedCount: EffectValue,
  filter: CardFilter,
  matched: { effects: Effect[] },
  rest: { effects: Effect[] }
}

Collected repeated shape:

{
  type: "REVEAL_TOP",
  from: "library",
  repeat: true,
  id: string,
  untilMatchedCount: EffectValue,
  filter: CardFilter
}

The collected shape reveals through the requested number of matching cards and stores every revealed card under id, including cards that did not match. It does not move or reorder those cards. Later effects can use source: { ref: id } to move selected cards, then shuffle the library to handle the rest.

Example, Open the Way:

Open the Way

{
  type: "REVEAL_TOP",
  from: "library",
  repeat: true,
  untilMatchedCount: { variable: "X" },
  filter: { types: ["Land"] },
  matched: {
    effects: [
      {
        type: "MOVE_CARD",
        card: "REVEALED_CARD",
        from: "library",
        to: "battlefield",
        count: 1,
        tapped: true
      }
    ]
  },
  rest: {
    effects: [
      {
        type: "MOVE_CARD",
        card: "REVEALED_CARD",
        from: "library",
        to: "library_bottom",
        count: 1
      }
    ]
  }
}

Notes:

  • amount defaults to 1. The single-reference shape stores one card object when amount is one and a card-group reference when it is greater than one.
  • Use the one-shot branch shape for card-specific instructions that reveal exactly one card and branch on its characteristics.
  • Use the repeated shape only for reveal-until effects like Open the Way.
  • Use the collected repeated shape when the choice is made from the complete revealed group rather than independently as each card is revealed.
  • optional: true offers accept and decline actions before revealing any cards. Declining leaves the library unchanged and resumes later effects.
  • rest.randomOrder: true randomizes, as one group, the rejected cards that its branch effects moved to the library bottom. Unrevealed cards retain their relative order ahead of that group.
  • A repeated reveal examines each card that was in the library when it began at most once, so moving rejected cards to the bottom terminates even if the requested number of matches does not exist.
  • Branch effects are responsible for moving the revealed card.
  • If a repeated reveal branch leaves the revealed card on top, the loop stops to avoid an infinite loop.

SEPARATE_INTO_PILES

Separates an exact referenced card group into two named piles, optionally reveals one pile, then lets the declared player choose which pile moves to each destination. Cards stay in their current zone while piles are formed and revealed. The final selection moves each complete pile through normal zone machinery.

Current shape:

{
  type: "SEPARATE_INTO_PILES", /* New API */
  source: { ref: string },
  formation:
    | { type: "CHOOSE", player: "OPPONENT" | "SELF" }
    | { type: "IN_ORDER" },
  piles: [
    { id: string, count: number },
    { id: string, rest: true }
  ],
  visibility:
    | "PUBLIC"
    | {
      type: "FACE_DOWN",
      reveal?: {
        player: "OPPONENT" | "SELF",
        count: 1
      }
    },
  selection: {
    player: "OPPONENT" | "SELF",
    chosen: { to: MoveCardZone },
    rest: { to: MoveCardZone }
  }
}

formation.type: "CHOOSE" exposes every legal first-pile composition to the declared player. formation.type: "IN_ORDER" takes the first pile's cards from the referenced group's current order and places every remaining card in the second pile. When both piles have the same size, swapped copies of the same partition are not offered twice.

Both pile IDs are stored as exact card-group references. Face-down visibility does not hide card identity from the engine; it records the reveal stage so the pilot can make a visibility-aware choice. The final selection identifies its acting player independently from the player that formed or revealed the piles.

AMASS

AMASS is the first-class keyword action for “amass [subtype] N”:

{
  type: "AMASS", /* New API */
  subtype: "Zombie",
  amount: 1
}

If its controller has no Army, the engine creates a black 0/0 [subtype] Army token. It then chooses exactly one Army they control and puts the resolved number of +1/+1 counters on that Army. The printed subtype is added only when it is absent and no existing subtype is removed, so amassing Orcs onto a Zombie Army produces a Zombie Orc Army. Multiple Armies create one staged legal action per Army; the engine does not enumerate subsets or permutations. amount uses the ordinary EffectValue API (including { variable: "X" }); zero creates or chooses the Army and preserves subtype addition without placing a zero counter.

CREATE_TOKEN

Creates one or more token permanents using an existing card definition name as the token template.

Current shape:

{
  type: "CREATE_TOKEN",
  count: EffectValue,
  id?: string,
  name: CardName,
  optional?: boolean, /* Widened API */
  player?: EffectPlayer, /* Widened API */
  target?: { /* New API; mutually exclusive with player */
    id: string,
    type: "player",
    player: "any" | "opponent" | "self"
  },
  tapped?: boolean,
  attacking?: true | { /* New API */
    defenderAssignment: "EACH_OPPONENT"
  }
}

Example, Acorn Catapult creates a Squirrel after dealing damage:

Acorn Catapult

{
  type: "CREATE_TOKEN",
  count: 1,
  name: "Squirrel"
}

Notes:

  • The token name must have a card definition or token definition the engine can instantiate.
  • Tokens enter through normal battlefield entry handling.
  • id stores the complete created-token array for later { ref: id } effects.
  • optional: true offers accept and decline actions before resolving the count. Accepting creates tokens through normal replacement handling and resumes later effects. Declining creates no token event and resumes later effects.
  • Every token in an authored group records the exact creating source object for later CardFilter.createdBy queries. This also holds for named replacement outputs, investigate, token copies, Afterlife, Offspring, and legacy token creation paths.
  • player defaults to SELF. An opponent-scoped token is represented by an ephemeral token with the resolved opponent owner and controller: its printed entry state and counters are prepared normally, its battlefield ENTERS event is emitted, then the token is discarded because opponent battlefield contents are not retained. It stores an empty array when the resolved count creates no tokens.
  • attacking: true creates the tokens during post-declare-attackers combat and assigns each one to a simulated opponent. In multiplayer, the engine exposes one opponent choice per token before creating the group. Each token is registered as attacking before its ENTERS event, but it was not declared as an attacker and does not emit ATTACK or ATTACKS.
  • Use count: { source: "OPPONENT_COUNT" } with attacking.defenderAssignment: "EACH_OPPONENT" for “for each opponent, create a token that's tapped and attacking that player.” The resolved base count must equal the configured opponent count. The engine assigns one base token to each opponent without a pilot choice. Token-creation replacements expand the group evenly, so doubling three base tokens creates six final tokens with two attacking each opponent. Every final token receives its assignment before the group enters simultaneously.

CREATE_EMBLEM

Creates a persistent emblem for the player. Emblems are not cards or permanents, do not enter a zone, and remain active after their source leaves.

Current shape:

{
  type: "CREATE_EMBLEM", /* New API */
  name: string,
  staticAbilities: Array<
    | {
      type: "MODIFY_STATS",
      filter: CardFilter,
      power?: number,
      toughness?: number
    }
    | {
      type: "GRANT", /* Widened API */
      kind: "keyword",
      target: StaticGrantCardTarget<StaticKeywordGrantTargetZone>,
      keyword: CardKeyword
    }
    | {
      type: "GRANT", /* Widened API */
      kind: "triggered ability",
      target: StaticGrantCardTarget<StaticTriggeredAbilityGrantTargetZone>,
      ability: TriggeredAbility
    }
  >, /* Widened API: optional */
  triggeredAbilities?: TriggeredAbility[] /* Widened API */
}

Filtered stat modifiers apply continuously to matching current and future battlefield permanents. Multiple emblems are distinct: their numeric modifiers stack, while duplicate keyword grants remain semantically idempotent.

staticAbilities is optional, so an emblem may carry only triggered abilities. The static keyword and triggered-ability grants are the same shapes permanents use, so an emblem can grant a keyword or an ability to cards in another zone, including the stack, as the Ral, Crackling Wit emblem does with "Instant and sorcery spells you cast have storm."

triggeredAbilities are the emblem's own abilities. They function from the command zone and are never filtered by a source zone. The emblem stores a snapshot of the card whose effect created it, and that card is the source of these abilities on the stack, so SOURCE in their effects refers to the creating card and per-turn caps such as matchingCountThisTurn count against it. An emblem with triggeredAbilities therefore requires a resolution context with a source: resolving one without a source throws.

CHOOSE_PLAYER

{
  type: "CHOOSE_PLAYER", /* New API */
  id: "chosen-opponent",
  player: "OPPONENT"
}

During resolution, pause for the controller to choose one player from ANY, SELF, or OPPONENT. This is mandatory and does not target. The engine exposes only legal player choices, validates the selection, stores its exact identity under id, and resumes the remaining effects. Existing { ref: string } player references can read that identity. Missing refs do not select a default opponent. Each delayed ability snapshots its own player refs, so later choices under the same id cannot overwrite an earlier delayed ability's selection.

CREATE_DELAYED_TRIGGER

Creates a triggered ability that waits outside the battlefield for its first matching event. The delayed ability is singular by default: its first matching event consumes it, whether or not its captured source is still in the required zone and whether or not the player later declines a choice created by its effects. Do not add oneShot or count: 1 for this normal case.

Set repeat: true when every matching event should trigger until the delayed ability's until duration expires. Omitted repeat remains one-shot. Use a numeric uses value only for an exact finite number of matches; repeat and uses are mutually exclusive.

{
  type: "CREATE_DELAYED_TRIGGER", /* New API */
  source: {
    ref: "rebound-card",
    zone: "exile"
  },
  trigger: {
    type: "BEGIN_UPKEEP",
    player: "SELF"
  },
  effects: [{
    type: "CAST_SPELL",
    card: { ref: "rebound-card" },
    zone: "exile",
    free: true
  }]
}

The source ref must identify an exact captured card object or card group in exile when the effect resolves. A group creates one delayed trigger, not one trigger per card. On the matching event, at least one captured object must still be in exile for the delayed ability to go on the stack; later effects using the group ref see only captured objects that remain there. Leaving exile and returning later does not reconnect an object, even if TurnZero retains the same physical-card id.

Use { card: "SOURCE" } without a zone when Oracle attributes a delayed trigger to a source but does not require that source to remain anywhere. This form captures the resolving controller and source LKI, is created even when a referenced payload batch is empty, and waits for its first matching event independently of later source movement:

{
  type: "CREATE_DELAYED_TRIGGER",
  source: { card: "SOURCE" }, /* New API: no zone dependency */
  trigger: { type: "BEGIN_END_STEP" },
  effects: [{
    type: "EXILE_PERMANENT",
    target: { ref: "created-token-batch" }
  }]
}

For a chosen player's next end step, use trigger: { type: "BEGIN_END_STEP", player: { ref: "chosen-opponent" } } with a ref produced by CHOOSE_PLAYER. This widened trigger API matches exact player identity using the delayed ability's captured refs. Opponent end-step events carry opponentId; an event without identity cannot consume a trigger waiting for a specific opponent. No extra oneShot flag is needed.

The captured delayed source is an exact zone object. A source event such as UNTAP_PERMANENT source: "SELF" must involve that object, not merely a card with the same physical-card id. If the source leaves and returns, the new object cannot reconnect to the delayed ability. Phasing does not change zone object identity and therefore preserves the match.

Use triggers: [Trigger, ...Trigger[]] when one delayed ability waits for the first of several alternative events. The first matching alternative consumes the one delayed ability and queues its effects once. This is an alternative event set, not one delayed ability per array entry. Existing single-event definitions keep trigger: Trigger.

{
  type: "CREATE_DELAYED_TRIGGER",
  source: { card: "SOURCE" },
  triggers: [ /* New API */
    { type: "UNTAP_PERMANENT", source: "SELF" },
    { type: "LOSE_CONTROL", source: "SELF", player: "SELF" }
  ],
  effects: [{ type: "EXILE_PERMANENT", target: { ref: "linked-card" } }]
}

LOSE_CONTROL source: "SELF", player: "SELF" compares the exact captured source object and the player captured when the delayed ability was created. It fires when that player stops controlling the permanent, including when the permanent leaves the battlefield. A control-loss event that happened before the delayed ability existed is not replayed. A later object represented by the same physical card cannot satisfy it.

Battlefield cleanup through EXILE_PERMANENT target: { ref } uses captured object identity rather than only physical-card ids. A token or permanent that left is ignored, and a newly created or returned object with the same id does not reconnect. Exiled tokens cease to exist and are not retained in exile.

CAST_SPELL creates the ordinary optional free-cast choice. The engine owns timing and target legality; the pilot chooses whether to cast and selects among the legal targets. Declining, or having no legal target, leaves the card in exile after the delayed ability has been consumed.

CREATE_REFLEXIVE_TRIGGER

Creates a normal triggered ability on the stack as the containing effect resolves. Use it for Oracle's reflexive trigger clauses, such as “When you do,” when the triggering action is part of the immediately preceding instruction. It does not listen for a future event and therefore does not have a trigger field.

{
  type: "CREATE_REFLEXIVE_TRIGGER", /* New API */
  effects: [{
    type: "DEAL_DAMAGE",
    amount: 1,
    target: "EACH_OPPONENT"
  }]
}

The nested effects do not resolve inline. The engine queues a triggered stack object with the resolving source and controller, and preserves available card and movement refs. If this is created while paying for a spell, the trigger is put above that spell after the mana ability produces mana; players may respond to the trigger even though they cannot respond to the mana ability or its costs. Use CREATE_DELAYED_TRIGGER instead when Oracle waits for a later named event.

COPY

Copies a selected object. Battlefield targets create token copies, a source target copies the resolving effect's source card, and a stack target creates an independent copy of a spell, activated ability, or triggered ability.

Current shape:

{
  type: "COPY",
  count?: EffectValue, /* Widened API */
  resultId?: string,
  target: {
    id: string,
    zone: "battlefield",
    controller?: "self",
    excludeSource?: boolean,
    filter?: CardFilter
  },
  overrides?: { /* New API */
    effects: Effect[] /* New API */
  }
}

// Copy the permanent currently equipped by the resolving source.
{
  type: "COPY",
  mode: "CREATE_TOKEN_COPY",
  target: { attachment: "EQUIPPED_PERMANENT" }, /* Widened API */
  resultId?: string,
  overrides?: {
    effects: Effect[]
  }
}

// Copy the resolving source with characteristic overrides.
{
  type: "COPY",
  mode: "CREATE_TOKEN_COPY",
  source: "SOURCE",
  count?: EffectValue, /* Widened API */
  resultId?: string,
  overrides?: {
    power?: number,
    toughness?: number,
    colors?: ManaColor[],
    additionalSubtypes?: CardSubtype[],
    manaCost?: ManaCost | null,
    effects?: Effect[] /* Widened API: existing copy overrides */
  },
  tapped?: boolean, /* New API */
  attacking?: true | { /* New API */
    defenderAssignment: "EACH_OPPONENT" | "EACH_OTHER_OPPONENT" /* New API */
  },
  exileAt?: "end of combat" /* New API */
}

// Copy every permanent in one battlefield snapshot.
{
  type: "COPY",
  mode: "CREATE_TOKEN_COPY",
  source: {
    each: {
      findCards: {
        zone: "battlefield",
        filter: {
          controller: "SELF",
          isToken: true,
          enteredBattlefieldThisTurn: true
        }
      }
    }
  },
  resultId?: string
}

// Have the resolving permanent become a temporary copy while retaining its name.
{
  type: "COPY",
  applyTo: "SOURCE",
  mode: "BECOME_COPY",
  target: {
    id: "creature-to-copy",
    zone: "battlefield",
    controller: "self",
    excludeSource: true,
    filter: { types: ["Creature"] },
    optional: true
  },
  resultId: "copied-creature", /* Widened API */
  until: "end of turn", /* Widened API */
  retain: { name: true } /* New API */
}

// Copy one selected kind of stack object.
{
  type: "COPY",
  mayChooseNewTargets: true,
  target: {
    id: "stack-object-to-copy",
    choice: true,
    zone: "stack",
    filter: {
      anyOf: [
        {
          types: ["SPELL"],
          source: { anyTypes: ["Instant", "Sorcery"] }
        },
        { types: ["ACTIVATED", "TRIGGERED"] }
      ]
    }
  }
}

// Choose a linked exiled card at resolution and copy it as a token.
{
  type: "COPY",
  mode: "CREATE_TOKEN_COPY",
  source: {
    id: "linked-copy-source",
    choice: true,
    findCards: {
      zone: "exile",
      linkedTo: { id: "imprint", source: "SOURCE" }
    }
  },
  resultId: "created-copy"
}

// Choose an artifact or creature you control at resolution and copy it as a token.
{
  type: "COPY",
  mode: "CREATE_TOKEN_COPY",
  source: {
    id: "copy-source",
    choice: true,
    findCards: {
      zone: "battlefield",
      filter: {
        controller: "SELF",
        anyOf: [{ types: ["Artifact"] }, { types: ["Creature"] }]
      }
    }
  },
  resultId: "created-copy"
}

// Choose one card from a stored event group and copy it as a token.
{
  type: "COPY",
  mode: "CREATE_TOKEN_COPY",
  source: { /* New API */
    id: "entering-copy-source",
    choice: true,
    cards: { ref: "graveyard-entrants" }
  },
  resultId: "created-copy"
}

// Copy the exact card successfully moved by an earlier effect.
{
  type: "COPY",
  mode: "CREATE_TOKEN_COPY",
  source: { ref: "exiled-card" }, /* Widened API */
  resultId: "created-copy"
}

source.each resolves its battlefield query once, then creates one token copy of every permanent in that snapshot. The complete copy group enters simultaneously, so none of the new copies can be selected recursively by the same effect. Ordinary CardFilter fields determine which permanents belong to the snapshot.

Example, Jaxis:

Jaxis, the Troublemaker

{
  type: "COPY",
  resultId: "jaxis-copy",
  target: {
    id: "creature-to-copy",
    zone: "battlefield",
    controller: "self",
    excludeSource: true,
    filter: { types: ["Creature"] }
  }
}

Notes:

  • count defaults to one. Targeted and source token copies resolve the complete count as one logical token group, so token-creation replacement effects see and modify that group once.
  • resultId lets later effects target the created token with { ref: "jaxis-copy" }.
  • applyTo: "SOURCE", mode: "BECOME_COPY" makes the resolving battlefield object copy a selected permanent. With until: "end of turn", the engine records its original copyable state and restores it only if that same battlefield object remains there during cleanup. A zone change clears the snapshot instead of restoring a later object with the same card id. resultId records the successfully copied target as a single-card ref; an optional target that is declined records no result. retain: { name: true } keeps the object's physical name while its copied definition supplies its other copiable characteristics and abilities. Name-based rules, including the legend rule, continue to use the physical name.
  • An equipped-permanent target reads the source's live attachment when the effect resolves. It creates no token when the source is unattached or the recorded battlefield object has left. If the source leaves first, a pending triggered ability retains its last-known attachment to a permanent that remains on the battlefield.
  • A ref-sourced copy reads the exact referenced object only while it remains in exile. A missing ref, a movement redirected away from exile, or a stale object creates no token. The result still records the complete replacement-expanded token group.
  • A query-sourced copy chooses one matching battlefield permanent as the effect resolves. This is a choice, not a target, so Shroud does not exclude a matching permanent. If the chosen permanent changes zones before the choice is made, the stale choice fails and creates no token.
  • A targeted token copy's overrides.effects run against that token after its copied characteristics are established but before it enters the battlefield. Use GRANT kind: "characteristics" for fieldwise copy exceptions and keep each SET, ADD, or REMOVE operation in its own grant. Copy-origin characteristic grants remain copiable when another effect copies the resulting permanent. Compatibility grants can still remove Legendary. One-shot effects such as placing counters are not themselves copiable.
  • Source token copies use the same overrides.effects mechanism and the same tapped/attacking entry machinery as named tokens. Replacement effects see one finalized logical group. Defender assignments are chosen for the final group, registered before simultaneous ETB events, and do not emit ATTACK or ATTACKS or update declared-attacker history.
  • attacking: { defenderAssignment: "EACH_OTHER_OPPONENT" } requires the resolving event's opponent identity. It creates one base token copy for each configured opponent other than that opponent, assigns each copy to its corresponding opponent, and safely creates none when the event has no valid defender context. With one configured opponent, it creates no copies.
  • exileAt: "end of combat" records each created token's exact battlefield zone-object identity. At the beginning of end combat, before end-of-combat actions, only permanent objects still carrying those identities are exiled. A token that left and returned is a new object and is not affected by the stale cleanup marker.
  • Source copies use the resolving source's current copiable values when it is present and its retained last-known source object after it leaves. Counters, damage, attachments, and ordinary temporary grants are not copied.
  • A linked-exile copy source is a non-targeted resolution choice. It re-queries the exact source object's current links when the effect resolves, carries physical and zone-object ids in its choice action, and rejects stale choices. A linked nonpermanent card remains a legal choice but creates no token.
  • A referenced-group copy source is also a non-targeted resolution choice. Its cards.ref reads the stored card group instead of searching a zone. One referenced member is selected through CHOOSE_COPY_SOURCE_CARD, using its physical and zone-object ids. The engine exposes one action per member rather than every subset or ordering, then creates one token copy of the selected permanent. A one-card group resolves without opening a redundant choice. Filters belong on the effect that stored the group, so ineligible event members never become copy candidates.
  • Omitting until from a battlefield keyword GRANT creates an indefinite, noncopiable grant on that exact object. Normal zone-change cleanup removes it; source movement and turn cleanup do not. Mimic Vat uses this for haste.

Mimic Vat composes the linked query with a durationless haste grant and an exact, source-independent delayed cleanup batch:

[
  {
    type: "COPY",
    mode: "CREATE_TOKEN_COPY",
    source: {
      id: "mimic-vat-copy-source",
      choice: true,
      findCards: {
        zone: "exile",
        linkedTo: { id: "mimic-vat-imprint", source: "SOURCE" }
      }
    },
    resultId: "mimic-vat-token-copy"
  },
  {
    type: "GRANT",
    kind: "keyword",
    target: { ref: "mimic-vat-token-copy" },
    keyword: "Haste"
  },
  {
    type: "CREATE_DELAYED_TRIGGER",
    source: { card: "SOURCE" },
    trigger: { type: "BEGIN_END_STEP" },
    effects: [{
      type: "EXILE_PERMANENT",
      target: { ref: "mimic-vat-token-copy" }
    }]
  }
]

The Jolly Balloon Man uses one SET grant and one ADD grant so omitted copied characteristics remain unchanged while the complete exception stays copiable:

{
  type: "COPY",
  mode: "CREATE_TOKEN_COPY",
  resultId: "jolly-balloon-copies",
  target: {
    id: "jolly-balloon-creature-to-copy",
    zone: "battlefield",
    controller: "self",
    excludeSource: true,
    filter: { types: ["Creature"] }
  },
  overrides: {
    effects: [
      {
        type: "GRANT",
        kind: "characteristics",
        target: "SOURCE",
        operation: "SET",
        power: 1,
        toughness: 1
      },
      {
        type: "GRANT",
        kind: "characteristics",
        target: "SOURCE",
        operation: "ADD",
        colors: ["red"],
        subtypes: ["Balloon"],
        keywords: ["Flying", "Haste"]
      }
    ]
  }
}

Living Laser composes turn-scoped discard history, source copies, a nonlegendary copy exception, tapped-attacking entry, a saved final group, and source-independent exact cleanup:

{
  type: "COPY",
  mode: "CREATE_TOKEN_COPY",
  source: "SOURCE",
  count: { source: "CARDS_DISCARDED_THIS_TURN", player: "SELF" },
  resultId: "living-laser-token-copies",
  overrides: {
    effects: [{
      type: "GRANT",
      kind: "type",
      target: "SOURCE",
      until: "source leaves",
      remove: ["Legendary"]
    }]
  },
  tapped: true,
  attacking: true
}
  • manaCost: null means the copy has no mana cost and therefore has mana value zero. Omitting an override retains the copied characteristic.
  • manaValue overrides the value derived from manaCost. Use it for a face whose rules mana value comes from another face, rather than inventing a payable mana cost. Face-down cards still have mana value zero, and an instance-level override takes precedence over the definition.
  • Characteristic overrides are copiable values when another effect copies the resulting token.
  • A stack-object filter uses types: ["SPELL"] for spells and types: ["ACTIVATED", "TRIGGERED"] for abilities. source applies the ordinary CardFilter vocabulary to the spell card or ability source. anyOf composes alternatives without introducing a card-specific copy effect.
  • Stack copies retain the original object's targets by default. Set mayChooseNewTargets: true only when the effect explicitly grants that permission. Targeted ability copies then receive a complete target-bundle choice: keeping every original target is always legal at copy time, or the controller may change any subset to currently legal targets while preserving target counts and divided amounts. Ordinary resolution later removes illegal targets individually and counters the copy only when every originally chosen target is illegal. A copied non-targeted ability still makes its own independent resolution choices; for example, copying a fetch-land ability performs two searches without paying the activation cost twice.
  • COPY_SPELL and COPY_ABILITY remain supported for existing definitions, but new effects that can copy more than one stack-object kind should use COPY with a stack target.

COPY_SPELL

Copies a spell already on the stack. target: "SELF" copies the resolving spell associated with the current effect context, while legacy spell effects may use target: "TARGET" with their containing spell's stack target. Inside a cast-triggered ability, target: "TRIGGERING_SPELL" explicitly copies the spell whose cast created that ability. count accepts any EffectValue and defaults to one. Each resulting copy is a separate stack object. Copies retain the original spell's targets by default. Set mayChooseNewTargets: true only when the copying effect or keyword explicitly grants that permission; a targeted copy then receives its own legal target-selection step. Copies are not casts and do not add spell-cast history records.

An activated ability should declare its stack target directly on COPY_SPELL:

{
  type: "COPY_SPELL",
  mayChooseNewTargets: true,
  target: { /* New API */
    id: "spell-to-copy",
    choice: true,
    findCards: {
      zone: "stack", /* New API */
      filter: {
        controller: "SELF",
        anyTypes: ["Instant", "Sorcery"]
      }
    }
  }
}

The engine creates one legal activation for each matching spell and retains the selected spell under the target id. The target is revalidated when the ability resolves; if it is no longer on the stack or no longer matches, the ability does not resolve. A copied spell uses the original choices, then offers the pilot new legal targets when the spell has target choices.

TurnZero normally settles its stack automatically. When a freshly cast spell has a legal effect-owned stack-target activation, the engine pauses at a bounded response choice. That choice contains only those legal activations and a decline action; it does not expose the normal main-phase action tree or model an opponent priority cycle. Cast triggers already placed on the stack resolve before this bounded response point.

COPY_ABILITY

Copies a controlled activated or triggered ability already on the stack. The target uses an ability-specific filter rather than a card filter:

{
  type: "COPY_ABILITY", /* New API */
  target: {
    id: "ability-to-copy",
    choice: true,
    zone: "stack",
    filter: {
      controller: "SELF",
      types: ["ACTIVATED", "TRIGGERED"]
    }
  }
}

filter.source applies an ordinary CardFilter to the source of the ability. The engine gives stack abilities stable identities, revalidates the selected ability when the copying effect resolves, and places an independent copy above the original. The copy retains the original ability's effects, event context, paid-cost information, source identity, linked references, and targets. Ability control is captured when the ability enters the stack and does not change when the source changes control. Mutable target and reference containers are cloned for the copy, while captured card objects retain their last-known identity. Set mayChooseNewTargets: true only when the copying effect grants that permission; a targeted copy then receives its own complete target-bundle selection step. Keeping the original bundle is always offered, changed targets must currently be legal, and only newly selected recipients emit a new BECOMES_TARGET event. A non-targeted copy still resolves independently and makes its own resolution choices.

Kirol, Attentive First-Year combines this target with a selectable tap cost and a true per-turn activation limit. The two creatures are tapped as payment before Kirol's ability reaches the stack; summoning sickness is irrelevant because the cost does not use Kirol's {T} symbol:

{
  id: "copy-controlled-triggered-ability",
  maxActivationsPerTurn: 1,
  cost: {
    tapPermanent: {
      id: "kirol-tapped-creatures",
      count: 2,
      filter: { controller: "SELF", types: ["Creature"] }
    }
  },
  effects: [{
    type: "COPY_ABILITY",
    mayChooseNewTargets: true,
    target: {
      id: "kirol-triggered-ability",
      choice: true,
      zone: "stack",
      filter: { controller: "SELF", types: ["TRIGGERED"] }
    }
  }]
}

Because TurnZero normally settles abilities automatically, it pauses at a bounded response choice when a legal ability-copy spell or activation is available. The response contains only legal copy actions and a decline action.

GRANT

Grants an ability, keyword, type, or rules modifier. Most resolving GRANT effects create temporary grants. A player static ability grant with no duration creates a continuous rules effect for the rest of the game. As a staticAbilities entry, GRANT creates a continuous grant while the source remains on the battlefield.

Effect shape:

{
  type: "GRANT",
  kind: "activated ability" | "triggered ability" | "mana ability" |
    "keyword" | "type" | "subtype" | "characteristics" | /* New API */
    "play permission" |
    "enters with counters" |
    "damage prevention" | /* New API */
    "spell resolution replacement" | /* New API */
    "static ability" | /* New API */
    "life total can't change", /* New API */
  target?: PermanentEffectTarget | {
    zones: Array<"hand" | "commander" | "graveyard" | "exile" | "library">,
    filter: CardFilter
  }, /* New API; temporary Flash grants only */
  permanents?: PermanentSet,
  card?: EffectCardReference,
  filter?: CardFilter,
  zone?: "exile" | "graveyard",
  choice?: true | { count: 1 },
  cost?: Cost, /* New API */
  playableFrom?: "next turn", /* New API */
  free?: true,
  resolutionDestination?: "exile",
  until?: "end of turn" | "end of next turn" | "your next end step" |
    "source leaves" | "leaves battlefield" | "leaves exile" |
    "your next turn", /* New API */
  ability?: ActivatedAbility | TriggeredAbility | ManaAbility |
    PlayerStaticAbility | MoveCardReplacementEffect, /* Widened API */
  keyword?: CardKeyword,
  mode?: "add" | "replace",
  types?: CardType[],
  subtypes?: CardSubtype[],
  operation?: "SET" | "ADD" | "REMOVE", /* Widened API */
  colors?: ManaColor[], /* New API on characteristic grants */
  keywords?: CardKeyword[], /* New API on characteristic grants */
  power?: EffectValue, /* Widened API on characteristic SET grants */
  toughness?: EffectValue /* Widened API on characteristic SET grants */
}

A resolving spell or ability can grant the player a static ability:

{
  type: "GRANT",
  kind: "static ability", /* New API */
  target: "PLAYER_SELF",
  ability: {
    type: "NO_MAXIMUM_HAND_SIZE"
  }
}

A battlefield NO_MAXIMUM_HAND_SIZE ability applies to its controller by default. Set player: "EACH" when the printed ability applies to every player; the tracked player then has no cleanup hand limit while that source remains on the battlefield, regardless of who controls it.

The grant is stored independently from its source. Without until it remains active for the rest of the game and is not removed by turn cleanup or by the source changing zones. With until: "end of turn" the cleanup step removes it. A player static ability may also be a graveyard-entry replacement, which is how a turn-scoped "if a card would be put into your graveyard from anywhere this turn, exile it instead" is created:

{
  type: "GRANT",
  kind: "static ability",
  target: "PLAYER_SELF",
  until: "end of turn", /* New API */
  ability: {
    type: "REPLACEMENT_EFFECT",
    match: {
      effect: { type: "PUT_INTO_GRAVEYARD", owner: "SELF" },
      controller: "ANY"
    },
    replace: { to: "exile" }
  }
}

A resolving effect can grant a zone-change replacement to the exact battlefield object it references. The grant remains on that object until it leaves the battlefield, including when the resolving source leaves first. Omitting match.effect.to matches a move to every destination.

{
  type: "GRANT",
  kind: "static ability",
  target: { ref: "returned-creature" },
  ability: {
    type: "REPLACEMENT_EFFECT",
    sourceZones: ["battlefield"],
    match: {
      effect: { type: "MOVE_CARD", from: "battlefield" },
      source: "SELF"
    },
    replace: { to: "exile" }
  }
}

The grant is stored independently from its source and has no until field. It remains active for the rest of the game and is not removed by turn cleanup or by the source changing zones.

kind: "enters with counters" is a one-shot spend bonus. Its target is "PAID_SPELL"; the grant travels with that spell and is consumed if it becomes a permanent. The counters are present before its ENTERS event is emitted.

kind: "spell resolution replacement" is a one-shot grant from a spell-cast trigger to its "EVENT_SPELL". It records a replacement on that exact spell only while the spell remains on the stack:

{
  type: "GRANT",
  kind: "spell resolution replacement", /* New API */
  target: "EVENT_SPELL", /* New API */
  match: {
    destination: "graveyard" /* New API */
  },
  replace: {
    destination: "exile" | "hand", /* Widened API */
    counters: [{ type: "dream", amount: 1 }]
  }
}

The replacement applies only after the spell resolves successfully and would otherwise move to the matched destination. Countering the spell, moving it from the stack during its own resolution, or resolving it to another native destination does not apply the replacement. Counters are added after the card enters the replacement zone. Replacing the graveyard move with a move to hand models effects such as Buyback; replacing it with exile models effects such as Ojer Pakpatiq, Deepest Epoch. Multiple grants are checked in order against the destination produced by the preceding replacement.

For counters intrinsic to the permanent itself, use the card-level entersWithCounters field instead:

entersWithCounters: [
  {
    type: "+1/+1",
    amount: { variable: "X" }
  }
]

Intrinsic entry counts resolve with the entering card and its cast X value, defaulting X to zero for non-cast entry. They are placed before ENTERS and pass through active counter-placement modifier grants. This differs from the one-shot GRANT, which is carried by a paid spell because of mana spent on it.

For permanent grants, use exactly one of target or permanents. permanents.findCards is a non-targeting snapshot of every matching permanent when the effect resolves. It does not create a pilot target choice, and permanents entering later are not included.

A temporary keyword grant can instead affect the player:

{
  type: "GRANT",
  kind: "keyword",
  target: "PLAYER_SELF", /* New API */
  until: "end of turn",
  keyword: "Hexproof"
}

Player keyword grants use the same end of turn and source leaves durations as permanent keyword grants. They are tracked independently from the battlefield. Opponent targeting of the player is not currently simulated, so player Hexproof has no target-legality effect in goldfishing.

A resolving keyword grant may instead use a zone-and-filter target with keyword: "Flash" and until: "end of turn". This creates a dynamic timing grant rather than snapshotting the cards currently in those zones. The grant does not provide permission to cast a card from a zone; when another effect or ability provides that permission later in the turn, the matching card may immediately be cast at instant speed. Filters are checked against the effective spell face, and cleanup removes the grant at end of turn.

A temporary player rules grant can make the self player's life total immutable:

{
  type: "GRANT",
  kind: "life total can't change", /* New API */
  target: "PLAYER_SELF",
  until: "end of turn"
}

While this grant applies, effects cannot gain or lose life for the self player, and positive life payments—including Phyrexian mana and life-paying mana abilities—are illegal. Zero-life payments remain legal. Attempts to gain or lose life do not update turn metrics or emit life-change events.

This is life-total immutability, not damage prevention. Damage is still dealt and may produce damage events or other damage-based results; only the resulting change to the self player's life total is disallowed. Cleanup removes the grant after end-step triggers have resolved.

A player damage-prevention grant prevents every represented preventable damage assignment until the next self turn begins:

{
  type: "GRANT",
  kind: "damage prevention", /* New API */
  target: "PLAYER_SELF",
  amount: "ALL",
  until: "your next turn"
}

The grant survives every intervening opponent turn and is removed before the next self untap step. It does not prevent life loss, life payments, damage to permanents, or a DEAL_DAMAGE instruction marked preventable: false. Prevented amounts emit DAMAGE_PREVENTED and increment damagePreventedThisTurn; fully prevented damage emits no DAMAGE_DEALT, causes no life loss, and produces no lifelink gain.

A continuous static ability can prevent all damage to the permanent currently equipped by its source:

{
  type: "PREVENT_DAMAGE", /* New API */
  condition: { count: { source: "PARTY_SIZE" }, minimum: 4 },
  target: "EQUIPPED_PERMANENT",
  amount: "ALL"
}

EQUIPPED_PERMANENT is a direct continuous target, not a declared target or a snapshot. The engine checks the source's current attachment and condition for each damage assignment. Protection therefore ends as soon as the source detaches, attaches elsewhere, leaves the battlefield, or stops meeting its condition. See literal and filtered counts for the reusable PARTY_SIZE assignment rules. This ability does not add another party calculation.

Permanent and player prevention use the same DAMAGE_PREVENTED accounting. The event records attemptedAmount, preventedAmount, dealtAmount, and the legacy amount field for the prevented amount. A permanent event identifies the exact recipient in card; a player event keeps player and optional opponentId. preventable: false bypasses continuous prevention. Fully prevented damage is not marked, removes no loyalty, emits no DAMAGE_DEALT, causes no lethal state-based action, and gives the damage source no lifelink gain.

Morningtide's Light composes these primitives in order: an empty-capable EXILE_PERMANENT target group, a source-independent delayed trigger that returns the exact surviving group tapped under owner control, this prevention grant, and an explicit MOVE_CARD of SOURCE from the stack to exile. The explicit source move runs only when the spell resolves, unlike a generic resolution destination.

A fieldwise characteristic grant patches one or more copiable fields in a single operation:

{
  type: "GRANT",
  kind: "characteristics", /* New API */
  target: "SOURCE",
  operation: "SET",
  types: ["Enchantment"],
  subtypes: []
}

Use one GRANT object per operation. Multiple characteristic grants apply in effect order:

Operation Supported fields Semantics
SET types, subtypes, colors, keywords, power, toughness Replaces only each explicitly supplied field. An omitted field is unchanged; an explicit empty array clears that list field.
ADD types, subtypes, colors, keywords Adds the supplied values and removes duplicates.
REMOVE types, subtypes, colors, keywords Removes only the supplied values.

Characteristic-grant subtypes may contain { ref: id } entries that resolve stored creature-type choices from the effect source. An unresolved reference contributes no subtype.

Power and toughness are valid only for SET and accept the shared EffectValue shape. They resolve once when the grant is created, so a source- or target-derived value is snapshotted for the grant's duration. Use MODIFY_STATS for temporary arithmetic. The readers for effective types, subtypes, colors, keywords, base power, and base toughness all apply the ordered patches before later counters or stat modifiers.

For example, this sets the target's base power and toughness to the resolving ability source's current power:

{
  type: "GRANT",
  kind: "characteristics",
  target: "TARGET_PERMANENT",
  operation: "SET",
  power: { source: { attribute: "POWER" } }, /* Widened API */
  toughness: { source: { attribute: "POWER" } }, /* Widened API */
  until: "end of turn"
}

Omitting until from a resolving characteristic grant ties it to that exact battlefield object for the rest of the object's lifetime. Turn cleanup and the granting source leaving do not remove it; a zone change does. Explicit end of turn, source leaves, and your next turn durations remain available.

When a characteristic grant resolves inside COPY.overrides.effects, it is a copy-origin patch and contributes to the resulting permanent's copiable values. Copying that permanent again retains the patch. The same durationless grant resolved as an ordinary runtime continuous effect is noncopiable. A copy-origin ADD that supplies Haste removes summoning sickness for entry and attack legality just like printed Haste.

The compatibility kind: "type", kind: "subtype", and kind: "keyword" shapes remain supported. Their existing duration and replacement semantics do not change. Prefer kind: "characteristics" when one fieldwise operation must patch several characteristics together.

Permanent keyword grants may use until: "your next turn" for text such as “Those creatures gain flying until your next turn.” The engine snapshots the affected permanents when the effect resolves, keeps the keyword through every opponent turn, and removes it as the next self turn begins before untap. The duration does not depend on the granting source remaining on the battlefield.

Static ability shape:

staticAbilities: [
  {
    type: "GRANT",
    kind: "activated ability" | /* New API */
      "triggered ability" | "mana ability" | "static ability" | /* New API */
      "keyword",
    condition?: CountCondition, /* New API; activated-ability and keyword grants */
    turnContext?: "OWN_TURN" | "OPPONENT_TURN", /* New API; keyword grants only */
    target: {
      zones: Array<"battlefield" | "commander" | "graveyard" | "exile" |
        "hand" | "library" /* New API; keyword grants only */ |
        "stack" /* New API; triggered-ability grants only */>,
      filter: CardFilter
    } | "SELF" /* New API; keyword grants only */ | {
      attachment: "ENCHANTED_PERMANENT" | "EQUIPPED_PERMANENT"
    } | {
      player: "SELF"
    },
    ability?: ActivatedAbility | TriggeredAbility | ManaAbility |
      PlayerStaticAbility, /* New API */
    keyword?: CardKeyword
  }
]

hand, library, and target: "SELF" are supported only for static keyword grants. stack is supported only for static triggered-ability grants. Those grants materialize onto matching cards while the source remains on the battlefield. A spell is placed on the stack before its completed cast emits CAST_SPELL, so a matching stack grant is reconciled in time for its granted cast trigger to fire. This supports continuous effects that grant abilities such as storm to spells rather than triggering from the granting permanent.

Instance-aware keyword grants can affect cast timing, such as granted Flash. A library keyword grant does not itself provide permission to cast that card; it composes dynamically with a separate cast permission. A keyword grant's optional count-backed condition is continuously reevaluated.

A keyword grant's optional turnContext limits it to your turns ("OWN_TURN") or opponents' turns ("OPPONENT_TURN"). The engine reconciles static grants as each turn begins, so the keyword is present only on matching turns, and combat, filters and triggers read it like any other keyword. Kain, Traitorous Dragoon:

staticAbilities: [
  {
    type: "GRANT",
    kind: "keyword",
    target: "SELF",
    turnContext: "OWN_TURN", /* New API */
    keyword: "Flying"
  }
]

Attachment targets are supported for static mana-ability, triggered-ability, player-static-ability, and keyword grants. Mana-ability grants support ENCHANTED_PERMANENT; triggered-ability, player-static-ability, and keyword grants support both ENCHANTED_PERMANENT and EQUIPPED_PERMANENT. A granted player static ability applies to the attached permanent's controller. An attachment grant applies only to the battlefield permanent currently referenced by the source's attachment. Reconciliation removes the grant when the source detaches, moves to another permanent, or leaves the battlefield.

A static activated-ability grant materializes the ability onto every matching card while its source remains on the battlefield. Its optional count-backed condition is continuously reevaluated; when the condition stops matching, the granted ability is removed. Activating the granted ability captures its effects on the stack, so a later condition change does not undo that activation:

staticAbilities: [
  {
    type: "GRANT",
    kind: "activated ability", /* New API */
    condition: { /* New API */
      count: {
        findCards: {
          zone: "battlefield",
          filter: { controller: "SELF", types: ["Artifact"] }
        }
      },
      minimum: 3
    },
    target: {
      zones: ["battlefield"],
      filter: { controller: "SELF", subtypes: ["Equipment"] }
    },
    ability: {
      id: "equip-zero",
      timing: "sorcery",
      cost: {},
      effects: [{
        type: "EQUIP",
        target: {
          id: "equip-zero-target",
          zone: "battlefield",
          filter: { controller: "SELF", types: ["Creature"] }
        }
      }]
    }
  }
]

Static top-library permission and alternate-cost grants use dynamic targets rather than materializing grant state onto a particular card instance:

staticAbilities: [
  {
    type: "GRANT",
    kind: "play permission", /* New API */
    target: {
      zone: "library",
      position: "top",
      filter: { /* New API */
        anyOf: [
          { types: ["Land"] },
          { subtypes: ["Bird"] }
        ]
      }
    },
    castUsing: { /* New API */
      grantedAlternateCostId: "bolas-citadel-life"
    }
  },
  {
    type: "GRANT",
    kind: "alternate cost", /* New API */
    id: "bolas-citadel-life",
    target: { zone: "library", position: "top" },
    cost: {
      loseLife: {
        amount: { source: { attribute: "MANA_VALUE" } }
      }
    }
  }
]

An unrestricted static play permission leaves the card's printed and otherwise available costs intact. castUsing.grantedAlternateCostId instead restricts spells cast through that permission to the named granted alternate cost; the printed mana cost and other alternate costs are not offered. The granted cost is the shared Cost shape, not a zero-mana ManaCost, so its life payment is a single replacement cost rather than an optional additional cost.

Only the current top card receives these grants. Normal spell timing, target, and mandatory additional-cost rules still apply. A granted cost with no X component casts an X spell with X equal to zero. Lands use the play permission and an available land play, but do not pay a spell alternate cost. Successfully casting or playing the top card counts as impulse access; merely looking does not.

An optional target filter restricts which top-card land or spell faces receive the permission. The engine checks the selected face's characteristics, so a Bird permanent face can qualify without incorrectly granting permission to cast its non-Bird Adventure, and a qualifying modal land face can be played even when its front face does not match.

Play permission applies to the physical top card rather than only its front face. The engine offers every legal spell face of a modal double-faced card, its legal land back, and either the permanent or Adventure spell of an Adventure card. The selected face uses its own timing, costs, targets, and resolution behavior. A castUsing alternate-cost restriction still limits the permission to the named granted cost and therefore does not offer other faces that require their own printed cost.

Static graveyard permission also uses a dynamic target. Every currently matching card in the player's graveyard is playable while the granting source remains on the battlefield, including cards that enter the graveyard after the source. It does not materialize temporary permission state on those cards:

staticAbilities: [
  {
    type: "GRANT",
    kind: "play permission",
    target: {
      zone: "graveyard", /* New API */
      filter: { types: ["Land"] }
    }
  }
]

Static graveyard permission uses normal timing and printed costs. Playing a land this way still consumes an available land play.

An optional turnContext on the graveyard grant limits it to "OWN_TURN" or "OPPONENT_TURN", the same as a keyword grant's turnContext. Omitting it grants during any turn. An optional usesPerTurn bounds how many times that grant may be spent each turn, counted per source permanent and reset at the start of each turn; omitting it keeps the grant unlimited, as for Crucible of Worlds. A card that matches only one grant charges that grant's slot automatically. A card that matches two or more unspent budgeted slots at once — for example an Artifact Creature in a graveyard where both an artifact grant and a creature grant have uses remaining — cannot be attributed automatically, since charging the wrong slot would either double-spend or leave a slot free that should have been spent. The engine instead offers one cast (or land-play) variant per matching unspent slot, naming that slot's playPermissionId, and drops the plain action; picking a variant charges exactly the one slot it names, never zero and never more than one. Muldrotha, the Gravetide, is the prototypical example: a Land, Artifact, Creature, Enchantment, Planeswalker, and Battle grant, each of the non-land grants budgeted to one use per turn, so an Artifact Creature in the graveyard offers a choice between its artifact slot and its creature slot while a plain Artifact or plain Creature is attributed without a choice.

Counter-placement replacement example, Kami of Whispered Hopes:

staticAbilities: [
  {
    type: "REPLACEMENT_EFFECT", /* New API */
    match: {
      effect: { type: "PUT_COUNTER" }, /* New API */
      target: {
        zones: ["battlefield"],
        filter: { controller: "SELF" }
      },
      counter: "+1/+1",
      count: { comparison: "AT_LEAST", value: 1 }
    },
    replace: {
      count: { operation: "ADD", value: 1 }
    }
  }
]

This is a normal replacement effect, not a GRANT or PUT_COUNTER trigger. A positive matching placement is modified before counters are put onto the permanent. ADD changes X to X + the configured value, while MULTIPLY changes X to X times the configured value.

Set match.onlyEffectPlacements: true for wording such as Doubling Season's “if an effect would put” restriction. It includes counters created as part of an effect-driven permanent entry, but excludes counters paid as costs and counters placed by turn actions. Omit it for wording such as Hardened Scales, which applies to all positive placements on matching permanents.

Zero stays zero. When additive and multiplicative replacements apply together, the goldfish engine applies additions before multiplications to maximize the final placement. The resulting PUT_COUNTER event contains that final amount. counter: "any" matches every actual counter type without placing a counter named any. Player counter replacements use target: { player: "SELF" } and the same replacement operations. The replacement also applies to matching permanents entering the targeted zone with counters; its source must already be on the battlefield, so Kami does not modify its own entry counters but can modify counters placed on itself later.

Activated ability example:

Scorn-Blade Berserker

{
  type: "GRANT",
  kind: "activated ability",
  target: { ref: "backup-target" },
  until: "end of turn",
  ability: {
    id: "sacrifice-to-draw",
    cost: { sacrificePermanent: { source: "SELF" } },
    effects: [
      { type: "DRAW_CARDS", count: 1 }
    ]
  }
}

EQUIP

Attaches an Equipment on the battlefield to a selected permanent. By default, the Equipment is the resolving spell or ability source. equipment may instead use a normal card reference, allowing a trigger to attach the Equipment that caused it:

{
  type: "EQUIP",
  equipment: { ref: "entering-equipment" }, /* New API */
  target: "TARGET_PERMANENT",
  optional: true /* New API */
}

The referenced card and target must both remain on the battlefield when the effect resolves. optional: true exposes the ordinary accept and decline actions before changing the attachment. A trigger may capture its entering Equipment by setting the same id on its ENTERS trigger.

ATTACH

Attaches a referenced Aura or Equipment on the battlefield to a referenced permanent. This is distinct from EQUIP: it is a rules effect and does not pay or activate an equip ability.

{
  type: "ATTACH", /* New API */
  attachment: "SOURCE" | { ref: string } | { attachment: "SOURCE" },
  target: PermanentEffectTarget,
  replaceAuraRestriction?: "ATTACHED_PERMANENT" /* New API */
}

For an instruction that chooses a recipient without targeting it, replace target with a battlefield query choice. optional: true adds a decline action:

{
  type: "ATTACH",
  attachment: "SOURCE",
  optional: true, /* Widened API */
  choice: { /* Widened API */
    id: "face-up-attachment",
    choice: true,
    findCards: {
      zone: "battlefield",
      filter: { types: ["Creature"] }
    }
  }
}

This choice ignores Shroud because it is not a target. The selected permanent must still satisfy the Aura's enchant restriction. A colored Aura cannot be attached to a permanent with protection from each color.

replaceAuraRestriction: "ATTACHED_PERMANENT" replaces a graveyard Aura's initial restriction with the exact battlefield object it just returned. Once that object is absent, the Aura is put into its owner's graveyard as a state-based action. Effects may consume the current relationship with target: { attachment: "ENCHANTED_PERMANENT" }.

UNATTACH

Removes the source permanent's current attachment. Reconfigure derives this effect for its attached-mode activated ability:

{ type: "UNATTACH", target: "SOURCE" } /* New API */

Reconfigure

Reconfigure is a structured keyword containing its shared activation cost:

keywords: [{
  type: "Reconfigure", /* New API */
  cost: { mana: { generic: 2, blue: 1 } }
}]

While unattached, the keyword derives a sorcery-speed ability that pays the cost and attaches the permanent to another target creature its controller controls. While attached, it instead derives a sorcery-speed ability that pays the same cost and unattaches the permanent. Neither ability requires tapping, so a tapped permanent may reconfigure.

Attaching through reconfigure creates an ordered continuous characteristic record that removes Creature and every creature subtype while retaining Equipment. That record lasts until the permanent becomes unattached, even if the permanent loses the reconfigure ability. If a later continuous effect makes the attached Equipment a creature, a state-based action unattaches it.

Crew

Crew is a structured keyword carrying the printed power threshold:

keywords: [{ type: "Crew", power: 1 }] /* New API */

On the battlefield it derives an activated ability with id crew whose cost is tapPermanent with totalPower over untapped creatures you control other than the source, and whose effect adds the Creature type to the source until end of turn. A Vehicle keeps its printed subtypes and its printed power and toughness apply once it is a creature, so no base-stat effect is needed. The ability has no timing restriction and no activation limit: a crewed Vehicle may be crewed again, and a crewed Vehicle is itself a creature that can pay another Vehicle's crew cost. Tapping for crew is a cost rather than the tap symbol, so summoning-sick creatures may pay it.

VOTE

Collects a named vote before later effects use VOTE_COUNT. The pilot is given a labelled choice. Each simulated opponent uses the matching assumptions.opponent.choices entry on the source card.

{
  type: "VOTE",
  id: "council-vote",
  players: "EACH_PLAYER",
  startingWith: "SELF",
  options: [
    { id: "past", label: "Past" },
    { id: "present", label: "Present" }
  ]
}

For a vote made only by opponents, use players: "EACH_OPPONENT" and omit startingWith. The engine resolves every opponent vote from the matching assumption without creating a pilot choice:

{
  type: "VOTE",
  id: "opponent-vote",
  players: "EACH_OPPONENT",
  options: [
    { id: "fame", label: "Fame" },
    { id: "fortune", label: "Fortune" }
  ]
}

Mana ability example, Vernal Bloom:

Vernal Bloom

staticAbilities: [
  {
    type: "GRANT",
    kind: "mana ability",
    target: {
      zones: ["battlefield"],
      filter: { subtypes: ["Forest"] }
    },
    ability: {
      id: "vernal-bloom-extra-green",
      trigger: { type: "TAPPED_FOR_MANA" },
      mana: { amount: 1, fixed: ["green"] }
    }
  }
]

Static keyword example, Party Thrasher. While Party Thrasher remains on the battlefield, this grants Convoke to each matching noncreature card in exile:

staticAbilities: [
  {
    type: "GRANT",
    kind: "keyword",
    target: {
      zones: ["exile"],
      filter: { not: { types: ["Creature"] } }
    },
    keyword: "Convoke"
  }
]

Static keyword targets may use battlefield, commander, graveyard, exile, hand, library, or stack (/* Widened API */). Stack grants are reconciled before cast events, so characteristic readers and cast-trigger filters see the granted keyword on the live spell. The grant is removed from a spell still on the stack as soon as its source stops applying.

Materialized static grants default to sourceZones: ["battlefield"]. A grant whose Oracle text functions from another supported zone declares that structural availability directly. This is separate from condition, which evaluates dynamic game state while the source remains in an allowed zone:

staticAbilities: [
  {
    type: "GRANT",
    kind: "keyword",
    sourceZones: ["graveyard"], /* New API */
    condition: {
      count: {
        findCards: {
          zone: "battlefield",
          filter: { controller: "SELF", subtypes: ["Island"] }
        }
      },
      minimum: 1
    },
    target: {
      zones: ["battlefield"],
      filter: { controller: "SELF", types: ["Creature"] }
    },
    keyword: "Flying"
  }
]

Unlike singular condition.sourceZone on a resolving effect, static-grant sourceZones observes the source card's current zone prospectively. Moving the source out of every declared zone removes its grants during reconciliation.

Triggered ability example, Warriors' Lesson:

Warriors' Lesson

{
  type: "GRANT",
  kind: "triggered ability",
  target: { ref: "lesson-creatures" },
  until: "end of turn",
  ability: {
    trigger: {
      type: "DAMAGE_DEALT",
      damageDealt: {
        kind: "COMBAT",
        recipient: "PLAYER"
      },
      player: "OPPONENT",
      source: "SELF"
    },
    effects: [
      { type: "DRAW_CARDS", count: 1 }
    ]
  }
}

Static ability example, Feywild Visitor:

Feywild Visitor

staticAbilities: [
  {
    type: "GRANT",
    kind: "triggered ability",
    target: {
      zones: ["battlefield", "commander", "graveyard", "exile"],
      filter: { types: ["Creature"], isCommander: true }
    },
    ability: {
      id: "feywild-visitor-faerie-dragon",
      trigger: {
        type: "DAMAGE_DEALT",
        damageDealt: {
          kind: "COMBAT",
          perPlayer: true,
          recipient: "PLAYER"
        },
        player: "OPPONENT",
        filter: { types: ["Creature"], isToken: false }
      },
      effects: [
        { type: "CREATE_TOKEN", count: 1, name: "Faerie Dragon" }
      ]
    }
  }
]

Filtered static grants may exclude the source card instance from their target set. Use this for Oracle text such as "other creatures you control," including when the source itself also matches the filter:

staticAbilities: [
  {
    type: "GRANT",
    kind: "triggered ability",
    target: {
      zones: ["battlefield"],
      exclude: "SOURCE", /* New API */
      filter: { controller: "SELF", types: ["Creature"] }
    },
    ability: {
      id: "other-creatures-trigger",
      trigger: { type: "CAST_SPELL", player: "SELF" },
      effects: []
    }
  }
]

exclude: "SOURCE" compares card-instance IDs, so a different permanent with the same card name remains eligible for the grant.

Keyword example:

Jaxis, the Troublemaker

{
  type: "GRANT",
  kind: "keyword",
  target: "SELF",
  until: "end of turn",
  keyword: "Haste"
}

Type example, Liquimetal Torque:

Liquimetal Torque

{
  type: "GRANT",
  kind: "type",
  target: {
    id: "liquimetal-permanent",
    zone: "battlefield",
    filter: { cardKinds: ["Permanent"] }
  },
  until: "end of turn",
  mode: "add",
  types: ["Artifact"]
}

mode: "add" adds the listed types to the permanent's current types. mode: "replace" substitutes its printed types for the grant's duration; additive type grants applied later still apply on top of that replacement.

Subtype additions use kind: "subtype", mode: "add", and until: "leaves battlefield" for text that changes the affected permanent without depending on the granting source remaining in play:

{
  type: "GRANT",
  kind: "subtype",
  target: { ref: "chosen-creature" },
  until: "leaves battlefield",
  mode: "add",
  subtypes: ["Mutant"]
}

The added subtype survives turn cleanup and the granting source leaving. It is cleared when the affected permanent changes zones.

Play permission example, Party Thrasher:

{
  type: "GRANT",
  kind: "play permission",
  card: { ref: "party-thrasher-exiled" },
  zone: "exile",
  choice: { count: 1 },
  until: "end of turn"
}

Graveyard play permission may use either a card reference or a filter. Without choice, a filter marks every currently matching card in the graveyard:

{
  type: "GRANT",
  kind: "play permission",
  zone: "graveyard",
  filter: { anyTypes: ["Instant", "Sorcery"] },
  resolutionDestination: "exile",
  until: "end of turn"
}

Set choice: true to have the pilot select exactly one currently matching card and grant play permission only to that card:

{
  type: "GRANT",
  kind: "play permission",
  zone: "graveyard",
  filter: { types: ["Artifact"] },
  choice: true, /* New API */
  until: "end of turn"
}

Set uses instead to create a dynamic player-scoped permission. No card is chosen when the effect resolves, and the permission is created even when no card currently matches. Cards that enter the graveyard later can use it. A successfully completed cast or land play selected through that permission consumes one use; legality probes and cancelled choices do not. uses and choice are mutually exclusive:

{
  type: "GRANT",
  kind: "play permission",
  zone: "graveyard",
  filter: { types: ["Creature"] },
  uses: 1, /* New API */
  until: "end of turn"
}

A spell that declares a graveyard cardTarget may grant permission only to its selected card by using card: "TARGET_CARD":

{
  type: "GRANT",
  kind: "play permission",
  card: "TARGET_CARD",
  zone: "graveyard",
  resolutionDestination: "exile",
  until: "end of turn"
}

Exile play permission may add free: true. That grants an additional, timing-respecting way to cast each granted nonland spell without paying its mana cost. It does not replace ordinary play permission, and lands still use a normal available land play:

{
  type: "GRANT",
  kind: "play permission",
  card: { ref: "exiled-cards" },
  zone: "exile",
  until: "end of next turn",
  free: true
}

Persistent normal exile permission uses until: "leaves exile" without a cost or while condition. Normal timing, mana costs, additional costs, and land-play restrictions apply. The permission remains attached to each exact card if the granting source leaves, and ends when that card changes zones:

{
  type: "GRANT",
  kind: "play permission",
  card: { ref: "exiled-card" },
  zone: "exile",
  until: "leaves exile" /* New API */
}

Costed exile permission uses cost with until: "leaves exile". It grants only the stated casting cost for as long as each referenced card remains exiled. Normal timing and mandatory additional costs still apply, and moving the card out of exile removes the permission:

{
  type: "GRANT",
  kind: "play permission",
  card: { ref: "airbent-creatures" },
  zone: "exile",
  cost: {
    mana: { generic: 2 }
  }, /* New API */
  until: "leaves exile" /* New API */
}

Conditional exile permission uses while with until: "leaves exile". It grants normal play permission for as long as each referenced card remains exiled and the count condition is currently satisfied. The condition is re-evaluated as game state changes; if it stops matching, the permission remains attached to the card and becomes usable again if the condition later matches. Normal timing, mana costs, and land-play restrictions still apply:

{
  type: "GRANT",
  kind: "play permission",
  card: { ref: "exiled-cards" },
  zone: "exile",
  until: "leaves exile",
  while: { /* New API */
    count: {
      findCards: {
        zone: "battlefield",
        filter: {
          controller: "SELF",
          subtypes: ["Wizard"]
        }
      }
    },
    comparison: "AT_LEAST",
    value: 1
  }
}

Notes:

  • GRANT_ABILITY and GRANT_KEYWORD still exist as compatibility aliases in current definitions.
  • Prefer GRANT for new definitions unless a nearby card already uses the compatibility form and a tiny local edit is clearer.
  • keyword: "Trample" records printed or granted trample. The current blocker-free goldfish combat model does not simulate excess-damage assignment.
  • keyword: "Lifelink" applies to combat and noncombat creature damage. The source controller gains life equal to the damage actually dealt.
  • keyword: "Protection from each color" remains the legacy tracking-only spelling for protection from white, blue, black, red, and green, but not colorless.
  • Dynamic commander-identity protection uses the structured keyword { type: "Protection", colors: { exclude: "COMMANDER_COLOR_IDENTITY" } }. Its protected colors are the five entries in manaColors absent from the game commander's combined color identity. Qualifying colored sources cannot target, damage, block, enchant, or equip the protected permanent. Colorless sources and colors in the commander identity remain legal. Unpreventable damage is not prevented. Illegal Auras go to their owner's graveyard, while other attachments detach as a state-based action.
  • Printed or granted keyword: "Defender" prevents a creature from being declared as an attacker.
  • Printed or granted keyword: "Vigilance" prevents a creature from tapping when it is declared as an attacker.
  • Use keyword: "Unblockable" as the goldfish representation of "can't be blocked".
  • Do not use until on static grants; the source remaining in a declared sourceZones zone defines the duration.
  • kind: "play permission" grants normal play permission, so timing and land-play restrictions still apply.
  • Temporary effect play permission supports zone: "exile" or zone: "graveyard". Exile uses choice: { count: 1 } for a referenced card group; graveyard uses choice: true with a filter. Static play permission supports dynamic target: { zone: "library", position: "top" } and target: { zone: "graveyard", filter: CardFilter } shapes.
  • Filtered graveyard permission without uses applies to cards matching when the effect resolves; cards that enter the graveyard later are not included. A limited uses permission remains dynamic until consumed or expired.
  • Temporary exile and graveyard play permissions ending this turn are cleared during turn-end cleanup, before the next opponent or player turn can use them. Permissions ending on a future turn and costed exile permissions that last until the card leaves exile remain in place. Conditional exile permissions also remain attached until the card leaves exile, even while their while condition is not satisfied.
  • until: "your next end step" expires during the current turn's end-step cleanup when granted during your turn. When granted during an opponent's turn, it survives the remaining opponent turns and expires during your next turn's end-step cleanup.
  • resolutionDestination: "exile" moves a spell cast with that permission from the stack to exile after it resolves. The Great Work uses this for its third chapter; it does not change the card's ordinary resolution destination.

MODIFY_STATS

As an effect, MODIFY_STATS applies a resolved power and toughness modifier to either a target permanent or a resolved set of permanents for the stated duration:

{
  type: "MODIFY_STATS", /* New API */
  target: {
    id: "creature-to-modify",
    zone: "battlefield",
    filter: { types: ["Creature"] }
  },
  power: {
    count: {
      source: "CARDS_DRAWN_THIS_TURN",
      player: "SELF"
    }
  },
  toughness: 0,
  until: "end of turn"
}

Use permanents to modify every permanent matching a battlefield query:

{
  type: "MODIFY_STATS",
  permanents: {
    findCards: {
      zone: "battlefield",
      filter: {
        controller: "SELF",
        types: ["Creature"]
      }
    }
  },
  power: 1,
  toughness: 0,
  until: "end of turn"
}

An Equipment source can instead modify the permanent it is currently attached to as the effect resolves:

{
  type: "MODIFY_STATS",
  target: "EQUIPPED_PERMANENT", /* New API */
  power: 2,
  toughness: 2,
  until: "end of turn"
}

EQUIPPED_PERMANENT is not a declared target and creates no target choice. The source must be on the battlefield and attached when the effect resolves. The modifier is stored on that permanent, so moving the Equipment afterward does not move the modifier. An absent or unattached source makes the effect a no-op.

power and toughness accept EffectAmount; omitted values are zero. Each amount is evaluated when the effect resolves and the resulting numeric modifier is stored on each affected permanent. A permanents query snapshots its matching permanents as the effect resolves, so permanents that enter later do not receive the modifier. Later game-state changes do not recalculate that temporary modifier. End-of-turn cleanup removes it.

When a declared target selects the assumed opponent permanent, MODIFY_STATS materializes that choice as an opponent-owned and opponent-controlled battlefield object before storing the modifier. Normal state-based actions can then move a creature with nonpositive toughness to its owner's graveyard and emit the same movement and death events as any tracked creature. A surviving materialized target loses an end-of-turn modifier during normal cleanup. Resolution still checks the target's filter and targeting protections; an illegal target receives no modifier.

For a set, { value: { each: { attribute: "POWER" } } } and the corresponding TOUGHNESS form (/* New API */) evaluate the named current characteristic separately for every member. The each value is available only when MODIFY_STATS uses permanents; singular targets use ordinary EffectAmount values. The resolver snapshots every member's power and toughness before it stores any modifier. This supports effects that double each creature's own current stats without reusing one member's value for the whole set.

This effect form is distinct from the MODIFY_STATS static ability, whose count-backed values are recalculated continuously while its source remains on the battlefield.

SET_BASE_STATS

SET_BASE_STATS creates a live, filtered base-power-and-toughness setting for the stated duration:

{
  type: "SET_BASE_STATS", /* New API */
  filter: {
    controller: "SELF",
    types: ["Creature"]
  },
  power: "mirror-entity-x", /* New API */
  toughness: "mirror-entity-x", /* New API */
  until: "end of turn"
}

power and toughness accept literal numbers or a named X-cost reference. The referenced value is resolved when the effect resolves. The filter remains live until cleanup, so matching permanents that enter later are included. Later SET_BASE_STATS effects take precedence over earlier settings. Counters and additive MODIFY_STATS effects apply on top of the resulting base values.

GAIN_CONTROL

Models gaining control of an estimated opponent permanent. This is currently a goldfish primitive for cards whose common targets are known and useful enough to state directly in the DSL.

Current shape:

{
  type: "GAIN_CONTROL",
  source: "ESTIMATED_OPPONENT_PERMANENT",
  assumptionId: string
}

The named group lives in the source card's assumptions.opponent.boardState.permanentGroups. The engine rolls and caches one candidate for the source card instance and group ID. Legal actions expose only the matching xValue if that kicked cast is payable. On resolution, the assumed permanent is created on your battlefield with an opponent owner; if it leaves your battlefield, it disappears instead of going to your zones.

Example, Thieving Skydiver:

Thieving Skydiver

assumptions: {
  opponent: {
    boardState: {
      permanentGroups: [
        {
          id: "skydiver-artifact-target",
          candidates: [
            { card: "Sol Ring", xValue: 1, chance: 0.15 },
            { card: "Arcane Signet", xValue: 2, chance: 0.85 }
          ]
        }
      ]
    }
  }
},
triggeredAbilities: [{
  trigger: { type: "ENTERS", to: "battlefield", source: "SELF" },
  effects: [{
    type: "GAIN_CONTROL",
    source: "ESTIMATED_OPPONENT_PERMANENT",
    assumptionId: "skydiver-artifact-target"
  }]
}]
}

TAKE_INITIATIVE

Models "you take the initiative" for goldfishing. The pilot keeps the initiative once it has it. Taking the initiative immediately ventures into Undercity, and future pilot upkeeps venture again before the draw step.

{
  type: "TAKE_INITIATIVE"
}

The engine owns Undercity legality and room effects. Branching rooms expose a CHOOSE_UNDERCITY_ROOM legal action for the pilot. Completing Throne of the Dead Three records the persistent COMPLETED_DUNGEON player status.

RING_TEMPTS_YOU

Models "the Ring tempts you" and the parts of the Ring emblem that affect goldfishing:

{
  type: "RING_TEMPTS_YOU" /* New API */
}

Each resolution increments game.ring.temptationCount; the count is retained past four while the unlocked Ring level is capped at four. If you control a creature, the engine requires a CHOOSE_RING_BEARER action and pauses later effects until a live controlled creature is selected. The current bearer is a legal selection. With no controlled creature, temptation still progresses and emits the RING_TEMPTS_YOU event without a bearer.

The selected bearer is treated as legendary and remains the bearer until another creature is selected or it leaves the battlefield. At Ring level two, the bearer attacking queues a draw-then-discard triggered ability. At level four, each combat-damage event it deals to an opponent queues one each-opponent loss-of-3-life triggered ability.

Definitions can listen for completed temptations with:

{
  trigger: { type: "RING_TEMPTS_YOU" }, /* New API */
  effects: [...]
}

The level-one blocking restriction and level-three blocked-creature sacrifice are intentionally outside the engine because TurnZero does not model blockers.

Ability Containers

Effect Conditions

Individual effects may include condition when a payload should only run after some prior choice, cost, or game-state requirement is true. Prefer conditions over inventing one-off wrapper effects for each Oracle wording.

Card-attribute conditions reuse the same match vocabulary as triggered abilities and filters. EVENT_CARD is the card carried by the event and SOURCE is the resolving spell or ability's source:

condition: {
  match: [{
    left: "EVENT_CARD",
    comparison: "GREATER_THAN",
    right: { count: 2 },
    attribute: "POWER"
  }]
}

Use CONDITIONAL when Oracle text has matched and otherwise branches that must be selected from one condition check. Separate conditional effects recheck their conditions independently as each effect resolves.

condition.timing: "OWN_MAIN_PHASE" gates an effect on a resolving source spell that was cast during either of its controller's main phases:

{
  type: "PUT_COUNTER",
  target: { ref: "resolved-creatures" },
  counter: { type: "+1/+1", amount: 1 },
  condition: {
    timing: "OWN_MAIN_PHASE" /* New API */
  }
}

The condition is false during combat and opponent turns. It is also false for a spell copy because the copy was not cast. Normal spell timing remains engine-owned; this condition changes only whether its effect resolves.

Triggered abilities use a structured combat timing condition when the event must happen during either player's combat phase:

condition: {
  timing: {
    turnStep: "COMBAT" /* New API */
  }
}

The condition checks the turn step when the trigger event occurs. It matches combat on any turn by default, including the simulated opponent combat window. Add scope: "OWN_TURN" /* New API */ to restrict it to the tracked player's own combat; scope: "ANY_TURN" states the default explicitly. An active self-cast listener with this condition keeps the simulator's abstract opponent end-of-combat priority window open, allowing legal Instant and Flash casts before the opponent simulation advances.

condition.sourceZone gates an effect on the recorded zone from which the resolving spell was cast. This is an observation, not top-level sourceZones availability: it controls whether the effect resolves after the spell is already on the stack and never grants permission to cast or activate anything.

{
  type: "MOVE_CARD",
  card: "SOURCE",
  to: "exile",
  count: 1,
  condition: { sourceZone: "hand" } /* New API */
}

For "if you do" text, store the meaningful prior effect with id, then gate the later effect with condition.refExists. This tests whether the named ref was stored, not whether its card array is nonempty. Prefer this composition over an artificial CHOOSE_ONE containing accept and decline branches.

condition.refMissing is the narrow counterpart for a failure branch: it is true only when neither a card ref nor a moved-card ref was stored for that ID. Use it directly for an ordered fallback such as an optional as-enters discard followed by moving the pending source to its owner's graveyard on decline.

[
  {
    type: "DISCARD_CARDS",
    id: "discarded-card",
    count: 1,
    choice: true,
    optional: true
  },
  {
    type: "MOVE_CARD",
    from: "library",
    to: "exile",
    count: 2,
    condition: {
      refExists: "discarded-card"
    }
  }
]

Target-gated payloads use target conditions:

{
  type: "DRAW_CARDS",
  count: 1,
  condition: {
    type: "TARGET_PERMANENT_CONTROLLER",
    target: { ref: "artifact" },
    controller: "SELF"
  }
}

To gate an effect on the characteristics of a captured card, compose the same card ref with a normal CardFilter. Currency Converter uses this for its Land-to-Treasure and nonland-to-Rogue branches:

condition: {
  card: { ref: "currency-converter-returned" },
  filter: { types: ["Land"] }
}

Count-gated payloads use a composable count plus minimum:

{
  type: "PLAY_CARD",
  card: "SOURCE_HIDEAWAY_CARD",
  zone: "exile",
  condition: {
    count: {
      type: "SUM",
      zone: "battlefield",
      filter: { types: ["Creature"] },
      attribute: "POWER"
    },
    minimum: 10
  }
}

Counter-gated effects use condition.counters:

{
  type: "CAST_SPELL",
  card: { ref: "discarded-card" },
  zone: "graveyard",
  free: true,
  condition: {
    counters: {
      target: "SOURCE",
      type: "chorus",
      amount: 4,
      comparison: "AT_LEAST"
    }
  }
}

Keep condition types reusable. If a proposed condition has a card name or a very specific Magic phrase in it, look for a composition of refs, counts, counters, targets, or zones first.

CAST_SPELL can use card: { ref: "..." } with zone: "hand", "exile", or "graveyard" to offer only the referenced card as the free-cast choice. For an ability or spell with a declared graveyard target, card: "TARGET_CARD" offers only that target and rechecks that it remains in the declared cast zone. Use card: "CHOICE" with filter: CardFilter to limit the choice to matching cards in that zone:

{
  type: "CAST_SPELL",
  card: "CHOICE",
  zone: "exile",
  free: true,
  filter: { /* New API */
    counters: {
      type: "dream",
      minimum: 1
    }
  }
}

Use copyOf: { ref: "..." } for an effect that copies one exact referenced card in exile and offers the copy as a free cast:

{
  type: "CAST_SPELL",
  copyOf: { ref: "exiled-card" }, /* New API */
  free: true
}

The reference must still identify the same zone object in exile. The engine creates a temporary card copy with fresh card and zone-object identity, leaving the original in exile. Casting uses the ordinary free-cast modes, targets, additional costs, and X = 0 rules. The copy ceases when it would leave the stack. Declining removes only the temporary copy and emits no movement event.

The filter is checked against the cards currently in the zone when the pending choice exposes legal actions. It composes with a referenced card and with maxManaValue when those fields are also present.

Use resolutionDestination: "exile" when a spell cast through the choice is exiled after resolving instead of going to its normal graveyard destination:

{
  type: "CAST_SPELL",
  card: "TARGET_CARD",
  zone: "graveyard",
  free: true,
  resolutionDestination: "exile" /* New API */
}

Declining leaves the targeted card in its current zone. The destination is carried only by a spell actually cast through this pending choice.

A free-cast choice may offer a spell with {X} in its mana cost. Its generated CAST_SPELL action fixes xValue: 0, even when the card's normal modeling minimum is greater than zero. Other values of X are illegal while casting without paying the mana cost.

Hideaway-style "you may play the card" effects use PLAY_CARD so lands can be played when a land play is available while spells can still be cast through the choice:

{
  type: "PLAY_CARD",
  card: "SOURCE_HIDEAWAY_CARD",
  zone: "exile",
  free: true
}

PLAY_CARD pushes a PLAY_CARD_CHOICE. Nonland spells are cast from the specified zone, with free: true meaning "without paying its mana cost" and with normal timing restrictions bypassed for this pending choice. Lands are offered only on your own first or second main phase while a land play remains. Declining leaves the card in its zone.

Use cost: Cost instead of free to offer a spell for a stated alternate casting cost:

{
  type: "PLAY_CARD",
  card: { ref: "madness-card" },
  zone: "exile",
  cost: {
    mana: { generic: 1, red: 1 }
  }
}

The engine validates and pays this cost, applies spell-cost modifiers and mandatory additional costs, and ignores normal card-type timing for the pending choice. cost and free are mutually exclusive. When PLAY_CARD appears before later effects in the same effect list, those later effects wait for the play-or-decline choice and retain the original effect context.

First-class Madness keywords generate this costed PLAY_CARD flow. Authors should use the structured keyword documented under Madness instead of repeating its replacement effect and discard trigger.

specialActions

specialActions declares player actions that do not use the stack. The engine pays the declared cost and resolves the ordered primitive effects immediately, then returns priority to the acting player. Do not use this container for an activated ability: activated abilities belong in activatedAbilities and create stack objects.

Shape:

specialActions: [
  {
    id: "action-id",
    label: "Action label",
    sourceZones?: [
      "battlefield" | "commander" | "exile" | "graveyard" | "hand"
    ],
    timing?: {
      turn?: "SELF",
      steps?: Array<"FIRST_MAIN" | "COMBAT" | "SECOND_MAIN">,
      stack?: "EMPTY"
    },
    cost?: Cost,
    effects: Effect[]
  }
]

sourceZones defaults to ["hand"]. timing.turn: "SELF" restricts the action to its controller's turn. steps restricts it to the named modeled steps, and stack: "EMPTY" requires an empty stack. Omitted timing fields add no restriction beyond being at a legal engine decision point.

cost uses the shared Cost API. The source card remains available as "SOURCE" while the ordered effects resolve, so an action can move its own card, capture it with an id, and grant the captured reference later permissions. The generated TAKE_SPECIAL_ACTION carries both the source card id and the definition's action id.

Use specialActions for bespoke declarative card-defined special actions. Foretell and Plot are first-class mechanics whose special actions are generated by the engine. Playing a land and turning a face-down permanent face up are also Magic special actions generated directly by the engine.

activatedAbilities

Activated abilities have an id, a cost, and ordered effects. They use the stack unless they are modeled as manaAbilities.

Shape:

activatedAbilities: [
  {
    id: "ability-id",
    mechanic?: "Cycling" | "Exhaust", /* Widened API */
    timing?: "sorcery" | { /* New API */
      turnStep: "FIRST_MAIN" | "COMBAT" | "SECOND_MAIN"
    } | {
      turnContext: "OWN_TURN" /* New API */
    },
    sourceZones?: ["battlefield" | "graveyard" | "hand"], /* New API */
    condition?: {
      zone: "battlefield" | "graveyard",
      filter: CardFilter
    },
    cost: {
      mana?: ManaCost,
      tap?: boolean,
      x?: {
        min?: number,
        max?: EffectValue, /* New API */
        ref?: string /* New API */
      },
      moveCard?: {
        source: "SELF",
        from: MoveCardZone,
        to: MoveCardZone
      } | {
        id: string,
        from: MoveCardZone,
        to: MoveCardZone,
        count: EffectValue,
        filter?: CardFilter
      } | {
        id: string,
        from: MoveCardZone,
        to: MoveCardZone,
        filter?: CardFilter,
        selection: { /* New API */
          minimum: number,
          maximum: "PAYMENT_REMAINDER"
        },
        payFor: { /* New API */
          amount: number,
          generic: true
        }
      },
      discardCard?: {
        source: "SELF"
      } | {
        id: string,
        count: EffectValue,
        filter?: CardFilter
      },
      sacrificePermanent?: { source: "SELF" } | {
        id: string,
        count?: EffectValue, /* Widened API */
        filter: CardFilter,
        another?: true
      },
      loseLife?: { amount: EffectValue },
      removeCounter?: {
        source: "SELF",
        counter: CounterEffect
      },
      putCounter?: {
        source: "SELF",
        counter: CounterEffect
      }
    },
    effects: Effect[]
  }
]

Example, Liquimetal Torque:

activatedAbilities: [
  {
    id: "make-artifact",
    cost: { tap: true },
    effects: [
      {
        type: "GRANT",
        kind: "type",
        target: {
          id: "liquimetal-permanent",
          zone: "battlefield",
          filter: { cardKinds: ["Permanent"] }
        },
        until: "end of turn",
        mode: "add",
        types: ["Artifact"]
      }
    ]
  }
]

Notes:

  • timing: "sorcery" limits activation to your main phase timing.
  • timing: { turnContext: "OWN_TURN" } permits activation during any modeled legal priority window on your turn without imposing sorcery timing.
  • mechanic: "Exhaust" marks an Exhaust ability. The engine permits that specific ability to be activated only once for the lifetime of its current battlefield object. The restriction persists across turns and control changes, while leaving and returning creates a new object with fresh Exhaust abilities. Multiple Exhaust abilities on one permanent are tracked independently.
  • timing: { turnStep: "COMBAT" } allows the ability during any modeled combat priority window and leaves the exact combat substeps to the engine.
  • sourceZones defaults to battlefield. A hand-zone ability can use discardCard.source: "SELF" to discard its own source as a cost.
  • moveCard.source: "SELF" moves exactly the ability's source from the declared origin zone as a cost and does not create a selection action.
  • Activated abilities do not declare a sibling target. Each target belongs to the effect that requires it; named cost selections remain inside cost.
  • cost.x exposes one legal activation for every payable value from min (default zero) through the resolved max. When max is omitted, available mana supplies the enumeration ceiling and normal cost payment determines which values are legal.
  • The engine chooses X before it enumerates effect-owned card targets. Target filters read that X while actions are generated, when an activation is revalidated, and when the ability resolves. A variable counted sacrifice cost therefore produces one action per legal X and target pair. Artifact selections stay inside the staged cost choice rather than multiplying the activation actions by every possible subset.
  • If every target becomes illegal before resolution, the ability does not resolve its effects. Mana and non-mana costs already paid remain paid.
  • The chosen value remains available as { variable: "X" }. When ref is present, the activation also stores that value under the given name for resolving effects that accept named value references.

Transmute is composed from an activated ability in hand, sorcery timing, a self-discard cost, and an exact-mana-value library search. For example, Muddle the Mixture's Transmute ability is:

{
  id: "transmute",
  sourceZones: ["hand"],
  timing: "sorcery",
  cost: {
    mana: { generic: 1, blue: 2 },
    discardCard: { source: "SELF" }
  },
  effects: [{
    type: "SEARCH_LIBRARY",
    count: 1,
    filter: { manaValue: 2 }, /* New API */
    destination: "hand",
    reveal: true,
    shuffle: true
  }]
}

Each card with Transmute supplies its own printed mana value to manaValue. The engine needs no dedicated Transmute action or keyword behavior.

The Astonishing Ant-Man uses one chosen X for both removing counters as a cost and creating that many tokens:

{
  id: "remove-counters-create-insects",
  cost: {
    mana: { generic: 2, green: 1 },
    tap: true,
    x: {
      min: 0,
      max: { target: "SELF", counters: "+1/+1" }
    },
    removeCounter: {
      source: "SELF",
      counter: {
        type: "+1/+1",
        amount: { variable: "X" }
      }
    }
  },
  effects: [{
    type: "CREATE_TOKEN",
    count: { variable: "X" },
    name: "Insect"
  }]
}

manaAbilities

Mana abilities are non-stack abilities used by mana payment. Use these for lands, rocks, mana dorks, and any source where producing mana has a cost or immediate effect.

Shape:

manaAbilities: [
  {
    id: string,
    condition?: ActivatedAbilityCondition,
    timing?: { turnContext: "OWN_TURN" }, /* Widened API */
    cost: {
      mana?: ManaCost,
      tap?: boolean,
      moveCard?: Cost["moveCard"],
      effects?: Effect[]
    },
    effects?: Effect[], /* New API */
    mana:
      | {
          amount: number |
            { count: EffectCount | ConditionalEffectValue } |
            { value: ContextValue },
          colour: Array<ManaColor | "ANY_COLOUR" | "ANY_ONE_COLOUR" | "COMMANDER_COLOURS" | "COLOURLESS"> | {
            linkedTo: { id: string, source: "SOURCE" } /* New API */
          },
          independent?: true, /* New API */
          constraint?: CardFilter | {
            anyOf: Array<
              | { payment: "SPELL", filter: CardFilter }
              | { payment: "ACTIVATED_ABILITY", filter: CardFilter } /* New API */
              | { payment: "CLASS_LEVEL" }
            >
          }, /* Widened API */
          bonus?: { appliesTo?: CardFilter, effects: Effect[] }
        }
      | { amount: number, fixed: ManaColor[] }
      | {
          eachColor: { /* New API */
            amount: number,
            findCards: CardQuery
          }
        }
      | ManaProduction
  }
]

Use colour: ["ANY_ONE_COLOUR"] when a dynamic amount must all be one chosen colour, such as “add X mana of any one color.” Use colour: ["ANY_COLOUR"] when each produced mana may be assigned independently. An array of concrete colours restricts the available colour choices. By default, the whole amount must use one of those colours. Add independent: true when each produced mana may independently use any listed colour, such as “add two mana in any combination of {U}, {B}, and/or {R}”:

mana: {
  amount: 2,
  colour: ["blue", "black", "red"],
  independent: true /* New API */
}

Use fixed when the source produces exact colours, such as {G}{U}. Use eachColor when one activation produces a fixed amount of every distinct current color among cards found by a query. Colorless cards add nothing, and a multicolored card contributes each of its colors. The resolved result is exact colored mana, not flexible mana:

mana: {
  eachColor: {
    amount: 1,
    findCards: {
      zone: "battlefield",
      filter: { controller: "SELF" }
    }
  }
}

Use colour: ["CONTROLLED_LAND_MANA_TYPES"] for “a mana type that a land you control could produce.” It derives the choices from current lands, and a multi-mana amount is produced as one chosen type. manaProduction remains supported for old definitions and is treated as a { tap: true } mana ability.

Use colour: { linkedTo: { id, source: "SOURCE" } } when the legal colours come from the exact card linked in exile to this source. The ability produces no mana if that link is absent, the linked card has no colours, or the linked card is no longer in exile. Each source object resolves only its own link.

The engine also derives the intrinsic tap-for-one-mana ability of each effective basic land type: Plains produces white, Island blue, Swamp black, Mountain red, and Forest green. These are separate choices when a land has multiple basic land types. An equivalent printed or legacy mana ability is not duplicated.

Example, Millikin:

Millikin

manaAbilities: [
  {
    id: "mill-for-colorless",
    cost: {
      tap: true,
      effects: [
        {
          type: "MILL",
          count: 1
        }
      ]
    },
    mana: { colourless: 1 }
  }
]

Treasure sacrifices itself as part of its mana ability cost:

manaAbilities: [
  {
    id: "sacrifice-for-any-colour",
    cost: {
      tap: true,
      effects: [
        {
          type: "SACRIFICE_PERMANENT",
          target: "SELF"
        }
      ]
    },
    mana: { any_colour: 1 }
  }
]

Rubble Rouser combines a selectable zone-change cost with a reflexive trigger:

manaAbilities: [{
  id: "add-red",
  cost: {
    tap: true,
    moveCard: {
      id: "rubble-rouser-exiled-card",
      count: 1,
      from: "graveyard",
      to: "exile"
    }
  },
  mana: { red: 1 },
  effects: [{
    type: "CREATE_REFLEXIVE_TRIGGER",
    effects: [{
      type: "DEAL_DAMAGE",
      amount: 1,
      target: "EACH_OPPONENT"
    }]
  }]
}]

Notes:

  • Prefer manaAbilities for new mana-source definitions. manaProduction remains supported as deprecated sugar for simple { tap: true } producers.

  • Mana abilities are not exposed as normal ACTIVATE_ABILITY legal actions.

  • Cost primitives resolve immediately when the payment engine uses the mana ability. Selectable moveCard costs stage a card choice before payment; no tap, move, or other payment mutation occurs until that choice completes.

  • Prefer primitive cost fields such as tap, mana, and moveCard for text before the colon. cost.effects remains available for legacy effect-shaped costs that do not yet have a primitive cost field.

  • SACRIFICE_PERMANENT with target: "SELF" is supported in cost.effects for mana abilities that sacrifice their own source.

  • REMOVE_COUNTER with target: "SOURCE" is supported in cost.effects. Availability requires the full resolved amount, and payment emits the normal removal event.

  • Top-level effects resolve immediately after the ability produces mana. Use them for effects that are part of the mana ability rather than its activation cost. Wrap a “When you do” clause in CREATE_REFLEXIVE_TRIGGER so its nested effects use the stack instead of resolving as part of the mana ability.

  • A mana cost is paid before the ability produces mana. The source being tapped cannot pay its own activation cost, and availability reports the net mana left after that prerequisite cost rather than gross production.

  • Multiple tap-cost mana abilities on one permanent are alternatives: payment chooses one legal ability from that source, rather than combining them.

  • A plain constraint filter is checked against the spell being paid for. Use the structured anyOf form when mana can pay for more than one kind of payment. payment: "SPELL" matches the spell being cast, payment: "ACTIVATED_ABILITY" matches the permanent that is the source of the activated ability being paid for, and payment: "CLASS_LEVEL" matches a Class-level activation:

    constraint: {
      anyOf: [
        {
          payment: "SPELL",
          filter: { anyTypes: ["Instant", "Sorcery"] }
        },
        { payment: "CLASS_LEVEL" }
      ]
    }
    
    constraint: {
      anyOf: [
        { payment: "SPELL", filter: { subtypes: ["Elemental"] } },
        {
          payment: "ACTIVATED_ABILITY", /* New API */
          filter: { subtypes: ["Elemental"] }
        }
      ]
    }
    

    A Class level-up is itself an activated ability, so it satisfies payment: "ACTIVATED_ABILITY" when the Class matches the filter as well as the dedicated payment: "CLASS_LEVEL" alternative. A mana ability's own activation cost is a separate payment path and matches neither.

  • condition uses the same game-state condition shape as an ordinary activated ability and determines whether that mana ability is currently legal.

  • timing: { turnContext: "OWN_TURN" } uses the same timing rule as an ordinary activated ability. Outside your turn, the mana ability contributes neither capacity nor mana for payment.

  • A bonus resolves only when that mana ability is activated as part of paying for a spell. appliesTo can narrow the bonus independently of constraint; when omitted, the bonus applies to every spell allowed to spend the mana.

triggeredAbilities

Triggered abilities listen for engine events and push triggers onto the stack.

Opponent land entries use ordinary ENTERS events. The abstract entering card retains its opponent owner and controller without becoming a retained battlefield permanent:

{
  trigger: {
    type: "ENTERS",
    to: "battlefield",
    filter: {
      types: ["Land"],
      controller: "OPPONENT"
    }
  },
  condition: {
    type: "OPPONENT_CONTROLS_MORE_LANDS_THAN_YOU",
    opponent: "EVENT_OPPONENT" /* New API */
  },
  effects: [
    {
      type: "MOVE_CARD",
      from: "hand",
      to: "battlefield",
      count: 1,
      choice: true,
      optional: true,
      filter: { types: ["Land"] }
    }
  ]
}

OPPONENT_SEARCHED_LIBRARY listens for the goldfish model's abstract estimate that the active opponent searched their own library. It does not inspect or invent an opponent library, search source, choice, result, or shuffle:

{
  trigger: { /* New API */
    type: "OPPONENT_SEARCHED_LIBRARY"
  },
  effects: [
    { type: "GAIN_LIFE", amount: 1 },
    { type: "DRAW_CARDS", amount: 1 }
  ]
}

An opponent-paid generic mana tax belongs on opponentTax. Its cost is an EffectValue: use a number for a fixed tax or a contextual value for a dynamic tax. The opponent decides whether to pay when the triggered ability resolves, so a source-power tax uses the source's current power or its last-known power if it has left the battlefield. Resolved costs are clamped to a nonnegative integer.

opponentTax: {
  cost: 1
}

opponentTax: { /* New API */
  cost: {
    source: { attribute: "POWER" }
  }
}

opponentTax models only generic mana. Use a different rules primitive for an opponent choice involving a sacrifice or another nonmana action.

COUNTER_TARGET_SPELL uses the same generic-mana policy for “unless its controller pays” text. Counterspell resolution remains abstract interaction in the goldfish engine, while the definition retains the printed payment:

{
  type: "COUNTER_TARGET_SPELL",
  opponentTax: { cost: 3 } /* New API */
}

COUNTER_STACK_OBJECT

COUNTER_STACK_OBJECT counters one chosen spell, activated ability, or triggered ability. Its StackObjectTarget retains the exact stack object id, then validates that object and its filter again as the effect resolves. An original spell moves from the stack to its normal graveyard destination, including active graveyard-entry replacements. A spell copy and a countered activated or triggered ability simply leave the stack. Mana abilities do not enter the stack, so they are never candidates.

{
  type: "COUNTER_STACK_OBJECT", /* New API */
  target: {
    id: "stack-object",
    choice: true,
    zone: "stack",
    filter: { types: ["SPELL", "ACTIVATED", "TRIGGERED"] }
  }
}

condition.match compares the current power or toughness of an event card with the source or with a shared count result. The condition succeeds when any comparison matches and is checked both when the ability triggers and when it resolves:

condition: {
  match: [
    {
      left: "EVENT_CARD",
      comparison: "GREATER_THAN",
      right: "SOURCE",
      attribute: "POWER"
    },
    {
      left: "EVENT_CARD",
      comparison: "GREATER_THAN",
      right: "SOURCE",
      attribute: "TOUGHNESS"
    }
  ]
}

The right operand may instead resolve any shared count:

condition: {
  match: [{
    left: "EVENT_CARD",
    comparison: "GREATER_THAN",
    right: {
      count: {
        type: "MAX",
        attribute: "POWER",
        zone: "battlefield",
        exclude: "EVENT_CARD"
      }
    },
    attribute: "POWER"
  }]
}

A triggered ability can also compare a shared count directly. This form is checked both when the trigger is created and when it resolves. When the count uses target: "EVENT_CARD", the trigger retains that event card and its last-known counters:

condition: {
  count: { target: "EVENT_CARD", counters: "any" },
  comparison: "AT_LEAST",
  value: 1
}

Shape:

triggeredAbilities: [
  {
    sourceZones?: Array<"battlefield" | "graveyard">, /* New API */
    trigger: Trigger,
    condition?: TriggeredAbilityCondition,
    target?: SpellTarget | SpellCardTarget,
    effects: Effect[]
  }
]

condition: { source: CardFilter } is an intervening source condition. The source must still be on the battlefield and match both when the event occurs and when the triggered ability resolves.

sourceZones declares where the card's triggered ability is active. It defaults to ["battlefield"]; include "graveyard" for abilities whose Oracle text functions from that zone. The engine uses the current tracked zone when discovering listeners and preserves its existing last-known-information rules for a source that moves while an event is emitted. A graveyard-source trigger also retains that exact graveyard object for resolution, so a source-return effect cannot move a card that left and returned as a new object.

An ENTERS trigger may set id to capture its event card under a normal card reference. This is useful when later effects aggregate the card's last-known battlefield characteristics:

{
  trigger: {
    type: "ENTERS",
    id: "dead-creature",
    from: "battlefield",
    to: "graveyard",
    turnContext: "OWN_TURN",
    filter: { types: ["Creature"] }
  },
  effects: [{
    type: "DRAW_CARDS",
    amount: {
      count: {
        type: "SUM",
        attribute: "POWER",
        source: { ref: "dead-creature" }
      }
    }
  }]
}

Use ENTERS_GROUP when Oracle treats one or more permanents entering simultaneously as one event. The engine emits one logical group after all members have entered. It does not infer groups from card names or adjacent single-card events.

{
  trigger: {
    type: "ENTERS_GROUP", /* New API */
    another: true,
    to: "battlefield",
    filter: { controller: "SELF", types: ["Creature"] },
    origin: { /* New API */
      anyOf: [
        { from: "graveyard" },
        { sourceZone: "graveyard" }
      ]
    },
    id: "graveyard-entrants"
  },
  effects: Effect[]
}

The filter, another, and origin clauses apply to each member. The trigger is created once when at least one member qualifies. id stores only qualifying members under a card-group reference, so later choices cannot select an ineligible member of a mixed group. another: true excludes the exact source object from that group.

origin.anyOf combines entry provenance tests. from matches a direct zone move. sourceZone matches the zone from which a permanent spell was cast, even though that permanent entered from the stack. A cast from a graveyard therefore matches { sourceZone: "graveyard" }, while a direct graveyard return matches { from: "graveyard" }. Entry groups retain provenance separately for every member.

turnContext can restrict an ENTERS trigger to OWN_TURN or OPPONENT_TURN; ANY is equivalent to omitting it.

sourceZone observes the zone from which the entering permanent spell was cast. Exact values match only cast entries with that provenance. A negated value such as { not: "hand" } also matches known non-cast entries, whose spell source zone is absent. The provenance is retained through deferred as-enters choices and is matched when the trigger is created:

{
  trigger: {
    type: "ENTERS",
    to: "battlefield",
    sourceZone: { not: "hand" } /* New API */
  },
  effects: Effect[]
}

Use cast: false to match only permanents that entered without being cast, regardless of which zone they moved from. Use cast: true to match any cast permanent without restricting its casting zone:

{
  trigger: {
    type: "ENTERS",
    to: "battlefield",
    cast: false /* New API */
  },
  effects: Effect[]
}

A triggered ability that targets exactly one battlefield permanent declares that target on the ability:

target: {
  zone: "battlefield",
  count: 1,
  filter: CardFilter
}

If there is one legal permanent, the engine selects it automatically. If there are several, it exposes one CHOOSE_TRIGGERED_ABILITY_TARGET action for each candidate. A mandatory trigger with no legal target is not created. The chosen target is retained on the stack and its filter is checked again on resolution; an illegal or missing target makes the ability resolve without effects. Filter comparisons against SOURCE use the source's current characteristics, or its last-known characteristics if it has left the battlefield.

Effect-owned permanent targets use the same resolution-time validation for spells, activated abilities, and triggered abilities. If a single mandatory target is illegal or missing, the whole stack object resolves without effects, so later effects in the same ability are skipped. Counted multi-target effects retain their legal selections when partial resolution is allowed.

When a triggered ability has multiple effect-owned targets, the engine stages them in effect order and retains each selection on the same triggered ability. Legal actions cover only the current target instead of enumerating the Cartesian product of every target choice. Optional targets include an explicit decline action; after the final target is chosen, the complete target set is emitted and the ability resolves normally. This also applies to effect-local player targets, including MILL targets.

Attribute match conditions follow the same rule for EVENT_CARD: a card still on the battlefield is rechecked using its current characteristics, while a departed event card uses its last-known power or toughness at both trigger creation and resolution.

A triggered ability that targets exactly one card in your graveyard uses the shared card-target shape:

target: {
  count: 1,
  zone: "graveyard",
  filter: {
    cardKinds: ["Permanent"],
    maxManaValue: 3
  }
}

If several cards are legal, the engine exposes one CHOOSE_TRIGGERED_ABILITY_TARGET action per candidate, carrying targetCardIds: [id] (/* New API */). The chosen card and target filter are retained on the triggered ability and checked again when it resolves. If the target has left the graveyard or no longer matches, none of the ability's effects resolve. For “you may return target…” text, keep the target mandatory and put optional: true on the MOVE_CARD effect so the may choice happens only after target revalidation.

Multi-card targets use the same combination generator as spells. Set count: { source: "ANY_NUMBER" } when the empty group is legal, and constrain the combined mana value with constraints: { totalManaValue: { maximum: EffectValue } }. Target selection never exposes a group above the resolved maximum. On resolution, illegal cards are removed from a nonempty selected group while legal targets remain; if all original targets are illegal, the whole ability is countered. An originally empty group has no targets and resolves normally.

Example, Solemn Simulacrum:

Solemn Simulacrum

triggeredAbilities: [
  {
    trigger: {
      type: "ENTERS",
      to: "battlefield",
      source: "SELF"
    },
    effects: [
      {
        type: "SEARCH_LIBRARY",
        count: 1,
        filter: { landKinds: "basic" },
        destination: "battlefield",
        tapped: true
      },
      { type: "SHUFFLE_LIBRARY" }
    ]
  },
  {
    trigger: {
      type: "ENTERS",
      from: "battlefield",
      to: "graveyard",
      source: "SELF"
    },
    effects: [
      { type: "DRAW_CARDS", count: 1 }
    ]
  }
]

Notes:

  • Use ENTERS with to: "battlefield" and source: "SELF" for "when this permanent enters".
  • Use ENTERS with from: "battlefield", to: "graveyard", and source: "SELF" for "when this creature dies".
  • Use ENTERS.id when the effects need the event card as a reusable card ref.
  • Use ENTERS.turnContext for zone-change text limited to your turn or an opponent's turn.
  • Trigger conditions such as ALTERNATE_COST_PAID live beside the trigger, not inside the effect list.
  • Saga timing abilities put a lore counter on themselves. Chapters use PUT_COUNTER triggered abilities with exact lore-counter conditions. An exact array such as amount: [1, 2] shares one chapter body across discrete totals; arrays require comparison: "EQUAL" and do not describe a range.
  • Saga chapter triggers are created for every exact threshold crossed by the actual lore placement. Once queued, they do not recheck the lore condition. After the final chapter leaves the stack, a Saga that still has at least that many lore counters is sacrificed as a state-based action.

Triggered Ability Conditions

condition checks a fact when the trigger resolves. Use the first-class MORBID condition for the named Magic mechanic:

condition: { type: "MORBID" } /* New API */

MORBID is true when a creature moved from the battlefield to a graveyard during the current turn. Use it only when the card specifically names Morbid.

OPPONENT_LOST_LIFE_THIS_TURN totals all opponent LOSE_LIFE events during the current turn. Set minimum to require a threshold:

condition: { type: "OPPONENT_LOST_LIFE_THIS_TURN", minimum: 2 }

ZONE_CHANGE_THIS_TURN matches any other recorded movement this turn; provide any combination of from, to, and filter to narrow it.

condition: {
  type: "ZONE_CHANGE_THIS_TURN",
  from?: Zone,
  to?: Zone,
  filter?: CardFilter
}

Examples:

// A card left your graveyard this turn.
{ type: "ZONE_CHANGE_THIS_TURN", from: "graveyard" }

// A card entered a graveyard this turn.
{ type: "ZONE_CHANGE_THIS_TURN", to: "graveyard" }

Relic Retriever uses the first form at the beginning of each end step:

{
  trigger: { type: "BEGIN_END_STEP" },
  condition: { type: "ZONE_CHANGE_THIS_TURN", from: "graveyard" },
  effects: [{ type: "CREATE_TOKEN", count: 1, name: "Treasure" }]
}

Counter conditions inspect the source when the trigger resolves:

condition: {
  counters: {
    target: "SOURCE",
    type: "lore",
    amount: 2,
    comparison: "EQUAL"
  }
}

Use an exact nonempty array to share one condition across discrete totals. An array requires comparison: "EQUAL"; scalar conditions retain the existing AT_LEAST, EQUAL, and LESS_THAN comparisons.

OPPONENT_CONTROLS_MORE_LANDS_THAN_YOU is true when any simulated opponent controls more lands than you. Set opponent: "EVENT_OPPONENT" (/* New API */) when the Oracle condition refers specifically to the opponent retained by an abstract event, which compares that one opponent instead. The event opponent must exist, and the land comparison is checked both when the trigger is created and when it resolves. Each opponent's lands come from their own game.opponentBoards entry (see Opponent Model), so opponents differ once one of them takes an extra land drop.

staticAbilities

Static abilities describe continuous state while the source is on the battlefield. Use the plural staticAbilities container, matching the other ability containers.

Soulbond pair grants

Soulbond is a keyword object, { type: "Soulbond" }. It creates one optional pairing trigger when its creature enters and one when another creature enters under the same controller. Pairing uses exact battlefield object identity. Both creatures must be unpaired creatures with the same controller when the choice resolves. A pair breaks if either object leaves, stops being a creature, or its controller differs. It does not survive a blink.

Use target: "SOULBOND_PAIR" for a static triggered-ability grant that applies to the source and its current partner only while that pair exists:

staticAbilities: [{
  type: "GRANT",
  kind: "triggered ability",
  target: "SOULBOND_PAIR",
  ability: { id: "paired-draw", trigger: { type: "DAMAGE_DEALT", source: "SELF" }, effects: [{ type: "DRAW_CARDS", count: 1 }] }
}]

Shape:

staticAbilities: [
  StaticAbility
]

Additional entry counters

ENTERS_WITH_ADDITIONAL_COUNTERS adds counters as matching permanents enter. The amount is resolved from the static ability's battlefield source at entry time. Counter-placement modifiers then apply before the engine emits the entering permanent's ENTERS event. Cast creatures, tokens, and permanents moved from another zone all use this entry path.

{
  type: "ENTERS_WITH_ADDITIONAL_COUNTERS", /* New API */
  excludeSource: true,
  filter: {
    controller: "SELF",
    types: ["Creature"]
  },
  counters: [{
    type: "+1/+1",
    amount: { source: { attribute: "TOUGHNESS" } } /* Widened API */
  }]
}

excludeSource compares battlefield object identity, not card name. A second copy can therefore receive counters from a copy already on the battlefield. A source entering in the same simultaneous batch is not active early enough to modify that batch.

Optional untap-step choices

Use this static ability for "You may choose not to untap" text:

{
  type: "MAY_REMAIN_TAPPED_DURING_UNTAP_STEP" /* New API */
}

At the start of the controller's untap step, the engine collects one keep- tapped or untap decision for each tapped permanent with this ability. These are turn-based choices, not triggered abilities, and they do not use the stack. The engine collects every decision before it untaps any permanent, then untaps the selected permanents together with ordinary permanents. Each pending permanent exposes two legal actions, so adding more optional-untap permanents does not enumerate subsets.

skipsUntapStep still prevents a permanent from untapping and suppresses this choice. Phasing in happens before optional untap decisions.

ATTACKS_EACH_COMBAT_IF_ABLE

ATTACKS_EACH_COMBAT_IF_ABLE requires the static ability's source to be included whenever attackers are declared if that creature is able to attack. The requirement is active only while the source is on the battlefield.

{
  type: "ATTACKS_EACH_COMBAT_IF_ABLE" /* New API */
}

The current goldfish combat model considers an untapped creature without summoning sickness able to attack. The engine rejects an attacker declaration that omits an able required attacker; the pilot includes required attackers even when attacking would not otherwise produce a modeled resource benefit.

COMBAT_RESTRICTION

COMBAT_RESTRICTION records that its source can't attack, can't block, or both, optionally only while an unless count condition is false ("can't attack or block unless you control seven or more lands"):

{
  type: "COMBAT_RESTRICTION", /* New API */
  target: "SELF",
  restrictions: ["CANT_ATTACK", "CANT_BLOCK"],
  unless: {
    count: { source: "LAND_COUNT" },
    comparison: "AT_LEAST",
    value: 7
  }
}

unless reuses the ordinary CountCondition shape; omit it for an unconditional restriction. The engine re-evaluates unless from current game state (for example the controller's current land count) every time attacker eligibility is checked, not once when the source entered, so the restriction can start or stop applying as state changes within or across combats. CANT_ATTACK excludes the source from getEligibleAttackers, the same attacker-eligibility list Defender and summoning sickness use, so both CHOOSE_ATTACKERS and a direct DECLARE_ATTACKERS action reject it while the restriction applies. CANT_BLOCK is recorded only; like ADD_COMBAT_RESTRICTION, the goldfish engine does not simulate defending blockers, so it has no gameplay effect.

SKIP_DRAW_STEP

SKIP_DRAW_STEP omits the controller's draw step while the static ability is active. The engine does not begin a draw step, emit BEGIN_DRAW_STEP, or perform the normal and additional turn-based draws. Other effects that draw cards continue to work normally. When the source leaves the battlefield, the next draw step proceeds normally.

{
  type: "SKIP_DRAW_STEP" /* New API */
}

The same ability can be stored as a granted player static ability through the ordinary GRANT path.

ADDITIONAL_TRIGGER

ADDITIONAL_TRIGGER causes a matching triggered ability to trigger additional times while the static ability's source remains on the battlefield. Its filter uses the same ability vocabulary as COPY_ABILITY; filter.source examines the source of the triggered ability, not the event that caused it to trigger. It uses that source's last-known power when the source has left the battlefield.

{
  type: "ADDITIONAL_TRIGGER", /* New API */
  amount: 1,
  filter: { /* Widened API */
    controller: "SELF",
    types: ["TRIGGERED"],
    source: {
      types: ["Creature"],
      maxPower: 2
    }
  }
}

Set another: true (/* New API */) when the source of the matching triggered ability must be different from the permanent supplying ADDITIONAL_TRIGGER. Subtype references inside filter.source resolve against that static ability source, while the resulting subtype requirement is tested against the source of the triggered ability.

Use events when only abilities caused by particular events receive the additional occurrences. Each entry has the same shape and matching semantics as a triggered ability's trigger; matching any entry qualifies the event. The ability filter still applies independently:

{
  type: "ADDITIONAL_TRIGGER",
  amount: 1,
  filter: {
    controller: "SELF",
    types: ["TRIGGERED"]
  },
  events: [ /* New API */
    {
      type: "CAST_SPELL",
      player: "SELF",
      filter: {
        anyTypes: ["Instant", "Sorcery"]
      }
    },
    {
      type: "SPELL_COPIED",
      filter: {
        anyTypes: ["Instant", "Sorcery"]
      }
    }
  ]
}

Each additional occurrence is created independently. Targets, trigger-time choices, and tracked trigger counts are not shared between occurrences. Multiple matching ADDITIONAL_TRIGGER abilities add their amounts.

MODIFY_STATS can modify the source itself or an attached permanent. Its power and toughness modifiers accept EffectAmount, so count-backed values are recalculated from current game state. Its optional count-backed condition is also reevaluated continuously:

{
  type: "MODIFY_STATS",
  target: "SELF",
  condition: {
    count: { source: "LIFE_TOTAL" },
    comparison: "AT_LEAST",
    value: 30
  },
  power: {
    count: {
      findCards: {
        zone: "graveyard",
        filter: { types: ["Creature"] }
      }
    }
  },
  toughness: {
    count: {
      findCards: {
        zone: "graveyard",
        filter: { types: ["Creature"] }
      }
    }
  }
}

For a filtered modifier that says "other," set excludeSource: true. The engine compares battlefield-object IDs, so separate copies still modify each other while each excludes only itself:

{
  type: "MODIFY_STATS",
  filter: { controller: "SELF", types: ["Artifact", "Creature"] },
  excludeSource: true,
  power: 2,
  toughness: 2
}

For characteristic-defining power or toughness, put the count directly in the card's power or toughness field rather than using a battlefield-only modifier:

power: { /* New API */
  count: {
    findCards: {
      zone: "battlefield",
      filter: {
        controller: "SELF",
        types: ["Creature"]
      }
    }
  }
}

This value is recalculated in every zone. On the battlefield it includes the source itself when the query matches it. Temporary base-stat settings apply after the resolved characteristic value. Counters and continuous modifiers apply afterward.

Example, Feywild Visitor:

staticAbilities: [
  {
    type: "GRANT",
    kind: "triggered ability",
    target: {
      zones: ["battlefield", "commander", "graveyard", "exile"],
      filter: { types: ["Creature"], isCommander: true }
    },
    ability: {
      id: "feywild-visitor-faerie-dragon",
      trigger: {
        type: "DAMAGE_DEALT",
        damageDealt: {
          kind: "COMBAT",
          perPlayer: true,
          recipient: "PLAYER"
        },
        player: "OPPONENT",
        filter: { types: ["Creature"], isToken: false }
      },
      effects: [
        { type: "CREATE_TOKEN", count: 1, name: "Faerie Dragon" }
      ]
    }
  }
]

Use a static characteristic grant when a source continuously adds a subtype to every matching card while it remains active:

staticAbilities: [{
  type: "GRANT",
  kind: "characteristics", /* New API */
  target: {
    zones: ["battlefield"],
    filter: { types: ["Land"] }
  },
  operation: "ADD",
  subtypes: ["Swamp"]
}]

Static characteristic grants currently support ADD and REMOVE operations for subtypes. They apply to current and newly matching cards, preserve printed characteristics, and disappear immediately when the source leaves its active zone.

Notes:

  • Static grants are materialized onto matching card instances and removed when the grant source leaves.

Events

Events update their histories and create matching triggered-ability objects at the time they occur. If a spell or ability is resolving, those trigger objects remain pending until its complete effect sequence—and every resolving choice in that sequence—has finished. The engine then puts the pending triggers onto the stack and applies the normal simultaneous-trigger ordering workflow. A pending trigger never interrupts the effects of the spell or ability that created it.

TRANSFORMS_INTO

Emitted after an existing permanent changes to the named face and the engine has reconciled its characteristics and static grants. The public trigger shape is:

type TransformsIntoTrigger = { /* New API */
  type: "TRANSFORMS_INTO"
  face: CardName
  source?: "SELF"
}

The engine reads triggered abilities from the new face. source: "SELF" matches the exact battlefield object that transformed. Transforming an object to the face it already has is a no-op and emits nothing. The event does not emit an enters- or leaves-battlefield event.

BECOMES_TAPPED

Emitted once when an authoritative game action changes a tracked permanent from untapped to tapped. The public trigger shape is:

type BecomesTappedTrigger = { /* New API */
  type: "BECOMES_TAPPED"
  filter?: CardFilter
  source?: "SELF"
}

source: "SELF" matches only when the permanent bearing the ability is the permanent that became tapped. A filter matches the transitioned permanent.

The event follows these rules:

  • Tapping an already tapped permanent is a no-op and emits nothing. Untapping it and tapping it again creates a new event.
  • A permanent entering the battlefield tapped does not become tapped and emits no event.
  • Declaring a creature as an attacker emits the event unless that creature has Vigilance.
  • Paying a legal tap cost emits the event. A failed or illegal cost payment does not tap the permanent and emits nothing.
  • Tapping a creature for Convoke or Enlist emits the event.
  • Tapping a permanent for mana emits one BECOMES_TAPPED event and retains the existing single TAPPED_FOR_MANA mana-production handling. Neither path recursively emits or duplicates the other.

MILLED

Emitted once for each resolved MILL effect that moves one or more tracked cards. The event contains the complete moved group. A trigger can filter that group, react only when its own source card was milled, and capture the group:

{
  trigger: {
    type: "MILLED",
    filter: { types: ["Creature"] },
    id: "milled-creatures"
  },
  effects: [
    {
      type: "MOVE_CARD",
      source: { ref: "milled-creatures" },
      from: "graveyard",
      to: "hand",
      count: 1,
      choice: true
    }
  ]
}

The filter determines whether the grouped event matches; the named ref still contains every card in that Mill event. Set source: "SELF" for text that triggers when the card bearing the ability is itself milled.

Opponent land entries

The opponent-turn abstraction always emits one ordinary ENTERS event for a normal land play. It independently estimates a second ramp entry with a 25% chance on turns 2–5 and a 10% chance from turn 6 onward; turn 1 has no second entry. Each event identifies the acting opponent through the abstract card's owner and controller without retaining that card on the battlefield.

{
  type: "ENTERS",
  card: {
    id: "estimated-opponent-normal-land-3-2",
    name: "Forest",
    types: ["Land"],
    owner: "opponent 2",
    controller: "opponent 2"
  },
  from: "hand",
  to: "battlefield"
}

The optional ramp entry omits from because the abstraction does not choose between a ramp spell, fetch effect, and additional land play. More than two entries and opponent land entries outside the active opponent's turn are not generated by the baseline simulation.

OPPONENT_SEARCHED_LIBRARY

Emitted by the opponent-turn goldfish abstraction when its single 10% search estimate succeeds. At most one event is emitted per simulated opponent turn. The event means that the active opponent searched their own library, without creating a library, search source, selected card, shuffle, or zone movement.

{ type: "OPPONENT_SEARCHED_LIBRARY" } /* New API */

The simulator does not emit this event for searches during the goldfish player's turn, including an opponent search caused by the player's effect. Normal tracked SEARCH_LIBRARY effects search the goldfish player's library and do not emit this opponent event.

Events are emitted by the engine when game actions happen. triggeredAbilities listen for these event names through their trigger.type.

Triggerable event families:

type TurnHookEvent =
  | "BEGIN_UNTAP_STEP" /* New API */
  | "BEGIN_UPKEEP"
  | "BEGIN_DRAW_STEP" /* New API */
  | "BEGIN_FIRST_MAIN"
  | "BEGIN_COMBAT"
  | "BEGIN_SECOND_MAIN"
  | "BEGIN_END_STEP"
  | "BEGIN_CLEANUP_STEP" /* New API */

type SpellAndDrawEvent =
  | "CAST_SPELL"
  | "SPELL_COPIED"
  | "DRAW_CARD"

type DrawCardTrigger = {
  type: "DRAW_CARD"
  player: "EACH" | "OPPONENT" | "SELF" /* Widened API */
  condition?: TriggeredAbilityCondition /* New API: checked only when triggering */
  matchingCountThisTurn?: number /* New API */
  turnContext?:
    | "ANY"
    | "EVENT_PLAYER_TURN" /* New API */
    | "NOT_EVENT_PLAYER_TURN" /* New API */
    | "OPPONENT_TURN"
    | "OWN_TURN"
}

type SpellSourceZone =
  | "commander"
  | "exile"
  | "graveyard"
  | "hand"
  | "library" /* New API */

type CastSpellTrigger = {
  type: "CAST_SPELL"
  filter?: CardFilter
  matchingCountThisTurn?:
    | number
    | {
        count: number
        player?:
          | "EVENT_CONTROLLER"
          | "EVENT_PLAYER" /* Widened API: exact opponent identity */
          | "OPPONENT"
          | "SELF"
      }
  player?: "EACH" | "OPPONENT" | "SELF" /* Widened API */
  manaSpentFrom?: { filter: CardFilter } /* New API */
  source?: "SELF" /* Widened API */
  sourceZone?: SpellSourceZone | { not: SpellSourceZone }
  /**
   * @deprecated Prefer `filter: { types: ["Creature"] }` or
   * `filter: { not: { types: ["Creature"] } }`.
   */
  spellKind?: "CREATURE" | "NONCREATURE"
  turnContext?:
    | "ANY"
    | "EVENT_PLAYER_TURN" /* New API */
    | "OPPONENT_TURN"
    | "OWN_TURN"
}

type LoseLifeTrigger = {
  type: "LOSE_LIFE"
  player: "EACH" | "OPPONENT" | "SELF"
  matchingCountThisTurn?:
    | number
    | {
        count: number
        player?:
          | "EVENT_CONTROLLER"
          | "EVENT_PLAYER"
          | "OPPONENT"
          | "SELF"
      }
}

type Zone =
  | "battlefield"
  | "commander"
  | "exile"
  | "graveyard"
  | "hand"
  | "library"
  | "stack"

type ZoneChangingEvent =
  | {
      type: "ENTERS"
      from?: Zone
      to: Zone
      filter?: CardFilter
    }
  | {
      type: "ENTERS_GROUP" /* New API */
      another?: boolean
      to: "battlefield"
      filter?: CardFilter
      origin?: { /* New API */
        anyOf: Array<
          | { from: Zone }
          | { sourceZone: SpellSourceZone }
        >
      }
      id?: string
    }
  | {
      type: "MOVE_CARD"
      another?: boolean
      from?: Zone
      source?: "SELF"
      to?: Zone
      filter?: CardFilter
    }

type DiscardedTrigger = {
  type: "DISCARDED" /* New API */
  player?: "EACH" | "OPPONENT" | "SELF" /* Widened API */
  source?: "SELF"
  filter?: CardFilter
  id?: string
}

type CycledTrigger = {
  type: "CYCLED"
  another?: true
  source?: "SELF"
  filter?: CardFilter
  id?: string
}

type MilledTrigger = {
  type: "MILLED" /* New API */
  source?: "SELF"
  filter?: CardFilter
  id?: string
}

type PermanentStateEvent =
  | "TAP_PERMANENT" /* New API */
  | "BECOMES_TAPPED" /* New API */
  | "UNTAP_PERMANENT"
  | "LOSE_CONTROL" /* New API */
  | "PERMANENT_SACRIFICED"

type CounterEvent = "PUT_COUNTER" | "REMOVE_COUNTER"

type DamageAndCombatEvent =
  | "DAMAGE_DEALT"
  | "ATTACK"
  | "ATTACKS"
  | "OPPONENT_ATTACKED"
  | "OPPONENT_ATTACK"

type LifeEvent =
  | "LIFE_GAINED"
  | "LOSE_LIFE"

LOSE_CONTROL records the exact battlefield object and the player who stopped controlling it. It is emitted both by control changes and by battlefield departure. player: "SELF" is interpreted relative to the ability's captured controller, not the permanent's later controller.

Use normal card filters to distinguish creature and noncreature spells:

filter: { types: ["Creature"] }

filter: { not: { types: ["Creature"] } }

Set source: "SELF" for an ability printed on a spell that triggers when that spell itself is cast. The engine reads that ability from the spell on the stack; it does not require the source to be a battlefield permanent.

spellKind remains available only for compatibility with older definitions.

NOT_EVENT_PLAYER_TURN compares the drawing player with the active player. An opponent drawing during your turn matches. During an opponent's turn, that active opponent's draw does not match, while a different opponent's draw does. Opponent draw events retain the drawing opponent's ID for this comparison.

Use player: "EACH" when printed text says “a player.” It matches both self and opponent events without duplicating triggered abilities. For draw and cast triggers, EVENT_PLAYER_TURN requires the event player to be the exact active player. It matches self only during OWN_TURN; during an opponent turn, both the event and the active turn must carry the same opponent ID. Missing opponent identity does not match.

For cast triggers, matchingCountThisTurn.player: "EVENT_PLAYER" counts only the exact caster represented by the current event. Opponent 1 and opponent 2 therefore have independent matching counts. Explicit player: "OPPONENT" continues to aggregate all opponent spell records when exact event-player matching is not requested.

A DRAW_CARD trigger can use a trigger-only historical condition to match an exact draw ordinal:

trigger: {
  type: "DRAW_CARD",
  player: "SELF",
  condition: {
    count: {
      event: {
        type: "DRAW_CARD",
        filter: { player: "EVENT_PLAYER" }
      },
      scope: "THIS_TURN"
    },
    comparison: "EQUAL",
    value: 2
  }
}

The engine records the current draw before checking this condition. A multi-card DRAW_CARDS effect therefore triggers when it crosses the requested ordinal, then finishes its remaining draws before exposing the deferred trigger on the stack. EVENT_PLAYER means the exact current drawing player, so different opponents have independent counts. The trigger condition is checked only when the event occurs. It is not copied to the triggered stack object or checked again on resolution. A sibling TriggeredAbility.condition keeps its intervening-if behavior and is checked both when triggering and on resolution.

A numeric matchingCountThisTurn limits successful triggers for the exact source object and exact triggered ability. The engine consumes one use only after the event and condition match and any required target can be chosen. Events before that source object existed do not consume uses. Each source copy has its own count, and a permanent that leaves and returns is a new object with a fresh count. Counts reset as each player's turn begins.

When matchingCountThisTurn appears on a trigger, it counts qualifying occurrences of that triggered ability for that source. When it appears on an optional DRAW_CARDS effect, it counts accepted execution of that exact ID-identified effect for that source instead.

Use the object form, such as matchingCountThisTurn: { count: 2, player: "EVENT_PLAYER" }, when the ability asks for an event ordinal. The engine compares the current event with the complete matching turn history, including events that occurred before the source entered. ATTACKS keeps its documented source-object attack-ordinal behavior.

Use numeric matchingCountThisTurn: 1 for draw abilities that say the ability triggers only once each turn. player, condition, and turnContext still decide whether the current event matches before the source-local use is consumed.

Zone-changing trigger examples:

// Enters the battlefield from anywhere.
trigger: {
  type: "ENTERS",
  to: "battlefield",
  source: "SELF"
}

// Dies.
trigger: {
  type: "ENTERS",
  from: "battlefield",
  to: "graveyard",
  filter: { types: ["Creature"] }
}

// Milled land card.
trigger: {
  type: "ENTERS",
  from: "library",
  to: "graveyard",
  filter: { types: ["Land"] }
}

// Leaves the battlefield for any destination.
trigger: {
  type: "MOVE_CARD",
  from: "battlefield",
  source: "SELF"
}

// Another countered creature leaves the battlefield for any destination.
trigger: {
  type: "MOVE_CARD",
  from: "battlefield",
  another: true,
  filter: { types: ["Creature"], controller: "SELF" }
}

// One or more cards move from the graveyard, once per completed movement.
trigger: {
  type: "MOVE_CARD",
  from: "graveyard"
}

MOVE_CARD filters match when at least one card in its moved group matches. Battlefield exits emit one event per permanent with its last-known battlefield state. Other multi-card movements remain grouped and emit once per completed operation, including when cards are exiled to pay an Escape cost.

DISCARDED means a card was discarded from hand, regardless of the card's final destination after replacement effects. Omitted player means SELF; use OPPONENT or EACH for broader listeners. source: "SELF" lets a card trigger from its post-discard zone, and id captures that exact card for later effects. Moving a card from hand to exile without a discard instruction does not emit this event. A first-class Madness keyword automatically replaces the graveyard destination with exile and reacts to this same semantic event.

CYCLED is emitted only after a first-class Cycling or Landcycling ability is activated and its cost is paid. It is distinct from the accompanying DISCARDED event. source: "SELF" supports "when you cycle this card" from the card's post-discard zone; another, filter, and id support battlefield listeners and references to the cycled card. A Cycling X value is carried into the resulting triggered ability.

Permanent Death Triggers

A permanent dies when it moves from the battlefield to the graveyard. Model that as an ENTERS event with from: "battlefield" and to: "graveyard", then use an ordinary CardFilter to describe what died. This keeps death triggers composable instead of adding separate events such as CREATURE_DIED, ARTIFACT_DIED, or TOKEN_DIED.

When several permanents leave simultaneously, the engine snapshots the battlefield before moving the batch. Triggered abilities on departing permanents therefore observe the other simultaneous deaths using last-known information. An another: true listener that dies in the same batch as three other matching creatures creates three triggers.

Morbid Opportunist is the complete pattern for “Whenever one or more other creatures die, draw a card. This ability triggers only once each turn”:

{
  trigger: {
    type: "ENTERS",
    from: "battlefield",
    to: "graveyard",
    filter: { types: ["Creature"] },
    another: true,
    matchingCountThisTurn: 1
  },
  effects: [
    { type: "DRAW_CARDS", count: 1 }
  ]
}

The numeric count above belongs to each Morbid Opportunist object and this specific ability. Earlier creature deaths do not consume it, two copies may both trigger, and a returned object has a fresh use.

Use the filter to express other kinds of death triggers:

// Whenever an artifact dies.
trigger: {
  type: "ENTERS",
  from: "battlefield",
  to: "graveyard",
  filter: { types: ["Artifact"] }
}

// Whenever another permanent dies.
trigger: {
  type: "ENTERS",
  from: "battlefield",
  to: "graveyard",
  another: true
}

// When this permanent dies.
trigger: {
  type: "ENTERS",
  from: "battlefield",
  to: "graveyard",
  source: "SELF"
}

Tokens emit the same battlefield-to-graveyard event before they cease to exist. They are not retained in the graveyard array, but isToken can still distinguish them at trigger-matching time:

// Whenever a token creature dies.
filter: {
  types: ["Creature"],
  isToken: true
}

// Whenever a nontoken creature dies.
filter: {
  types: ["Creature"],
  isToken: false
}

The engine emits CREATURE_DIED after the composable ENTERS death event for legacy compatibility. New definitions should use ENTERS plus filters rather than introducing type- or token-specific death events.

DAMAGE_DEALT event

DAMAGE_DEALT is the unified event for combat and noncombat damage to players and permanents. It is an event for triggered abilities to observe, not an effect that card definitions resolve. damageDealt.recipient describes where the damage went without implying that the recipient was targeted, while damageDealt.kind distinguishes combat from noncombat damage.

trigger: {
  type: "DAMAGE_DEALT",
  damageDealt: {
    recipient: "PLAYER" | "PERMANENT",
    kind?: "COMBAT" | "NONCOMBAT",
    perPlayer?: true
  },
  player?: "SELF" | "OPPONENT",
  source?: "SELF",
  filter?: CardFilter
}

Every emitted event has a concrete kind; a trigger may omit kind to match either. For a permanent recipient, source: "SELF" means the listening permanent was dealt damage. For an individual player-damage event, it means the listening permanent dealt the damage. Filters match the damaged permanent or the individual damaging source, respectively.

Use source: "SELF" for a creature that watches its own combat damage. Vorosh uses this shape for “Whenever Vorosh deals combat damage to a player”:

trigger: {
  type: "DAMAGE_DEALT",
  damageDealt: {
    kind: "COMBAT",
    recipient: "PLAYER"
  },
  player: "OPPONENT",
  source: "SELF"
}

The event exposes the damage amount through { event: { attribute: "DAMAGE_AMOUNT" } }. Cold-Eyed Selkie uses it to draw cards equal to the combat damage it dealt:

{
  trigger: {
    type: "DAMAGE_DEALT",
    damageDealt: {
      kind: "COMBAT",
      recipient: "PLAYER"
    },
    player: "OPPONENT",
    source: "SELF"
  },
  effects: [
    {
      type: "DRAW_CARDS",
      count: { event: { attribute: "DAMAGE_AMOUNT" } }
    }
  ]
}

Omit source: "SELF" and add a filter when an ability watches combat damage from any matching creature. The event carries the individual damaging creature for filter matching:

trigger: {
  type: "DAMAGE_DEALT",
  damageDealt: {
    kind: "COMBAT",
    recipient: "PLAYER"
  },
  player: "OPPONENT",
  filter: { types: ["Creature"], isToken: false }
}

Use perPlayer: true for “Whenever one or more creatures deal combat damage to a player.” This emits once for each opponent damaged, with the matching group of damaging creatures and their combined damage amount:

trigger: {
  type: "DAMAGE_DEALT",
  damageDealt: {
    kind: "COMBAT",
    perPlayer: true,
    recipient: "PLAYER"
  },
  player: "OPPONENT",
  filter: { types: ["Creature"], isToken: false }
}

The event context records the damage amount, damaged opponent, and damaging creature or creature group. The current goldfish combat model emits combat damage only to opposing players; combat damage to permanents is not modeled.

ATTACK and ATTACKS events

Use ATTACK for aggregate “Whenever a player attacks” triggers. SELF fires once after you declare one or more attackers, even when several creatures attack or attackers are split among several opponents. Declaring no attackers does not emit it. OPPONENT observes one abstract attack per simulated opponent turn, and ANY matches either kind of attack.

trigger: {
  type: "ATTACK",
  player: "SELF"
}

The opponent event does not expose individual attackers, blockers, or combat damage. It opens an end-of-combat priority window when its resolution leaves a choice on the stack or mana retained through combat, allowing instant-speed actions before the pilot advances the opponent to second main.

Use ATTACKS for a creature-specific attack trigger. It fires separately for each declared attacker. source: "SELF" represents “Whenever this creature attacks”; omit it and supply a filter to watch matching attackers.

trigger: {
  type: "ATTACKS",
  source: "SELF"
}

Use matchingCountThisTurn for source-specific attack ordinals. The current attack is included, so matchingCountThisTurn: 1 means “attacks for the first time each turn.” Counts persist through additional combats, control changes, transforms, and phasing, reset at each player's new turn, and reset when the object changes zones. A permanent that leaves and returns can therefore have a new first attack that turn.

BECOMES_BLOCKED event

BECOMES_BLOCKED is emitted once for the complete abstract forced-block batch. Its filter matches when one or more blocked attackers match, so several matching creatures becoming blocked simultaneously produce one trigger:

trigger: {
  type: "BECOMES_BLOCKED", /* New API */
  filter: { controller: "SELF", types: ["Creature"] }
}

BECOMES_TARGET event

BECOMES_TARGET fires after a battlefield permanent becomes a target. The engine emits the event from shared target selection for spells, activated abilities, and triggered abilities, including copied spells after their targets are finalized. A permanent targeted more than once by the same spell or ability produces one event.

Use source: "SELF" for “this permanent becomes a target.” Use by to restrict what targeted it, or omit by to observe every targeting object:

trigger: {
  type: "BECOMES_TARGET", /* New API */
  source: "SELF",
  by?: "SPELL" | "ACTIVATED_ABILITY" | "TRIGGERED_ABILITY"
}

This is a targeting event, not a cast or copy event. A copied spell that targets the permanent emits BECOMES_TARGET with by: "SPELL" even though the copy was not cast. Targeting by an activated or triggered ability emits the same event with its corresponding by value.

CRIME event

CRIME fires once when a player puts an original spell, activated ability, or triggered ability on the stack after choosing its targets and paying any applicable costs. It fires when at least one target is an opponent player, a permanent an opponent controls, or a card an opponent owns in a graveyard. Multiple qualifying targets still produce one event.

trigger: {
  type: "CRIME", /* New API */
  player: "SELF",
  matchingCountThisTurn: 1
}

The engine classifies qualifying targets internally. A copied object does not commit a crime merely because it retains or receives targets, and changing an existing object's targets does not create another CRIME event.

Set numeric matchingCountThisTurn to cap qualifying crime triggers for the exact source object and triggered ability during the turn. For example, matchingCountThisTurn: 1 models "This ability triggers only once each turn." The cap resets as a new turn begins.

PUT_COUNTER event

Fires after the PUT_COUNTER effect successfully places counters on a permanent or player. It carries the final counter type and resolved amount. Permanent events carry card; player events carry player: "SELF". A Saga chapter can observe its new lore total using condition.counters.

Triggered effects may read the actual placed amount with { event: { attribute: "COUNTER_AMOUNT" } }. Counter replacement effects apply before the event, so this value is the final amount rather than the instruction's original amount.

trigger: {
  type: "PUT_COUNTER",
  source: "SELF",
  counter: "lore"
}

Use the object form of matchingCountThisTurn for abilities that care about the ordinal of a matching counter placement. source: "SELF" scopes the history to the source permanent, as used by Danny Pink's granted ability:

trigger: {
  type: "PUT_COUNTER",
  source: "SELF",
  matchingCountThisTurn: { count: 1 }
}

Placement counts are tracked independently per permanent even when no matching ability is currently active. One operation placing several counters counts as one placement; separate operations count separately. When counter is present, only placements of that counter type contribute. Zero-counter operations do not contribute. Counts reset at the beginning of every player's turn.

Omit source: "SELF" and add a filter when any matching permanent can cause the ability to trigger. A numeric count still limits the specific source ability rather than the global placement history. This represents “Whenever a counter is put on a creature you control. This ability triggers only once each turn”:

trigger: {
  type: "PUT_COUNTER",
  filter: {
    types: ["Creature"],
    controller: "SELF"
  },
  matchingCountThisTurn: 1
}

For the structured ordinal form, the history comparison uses the complete trigger: counter, source, and filter. Player counter placements do not match permanent filters. Counters placed on a permanent as it enters are emitted before the ENTERS event, so abilities such as Hollowmurk Siege can observe them.

EARTHBEND event

Fires after an EARTHBEND effect successfully animates its target and places its counters. It carries the earthbent land as card and the resolved count as amount. Use it for “Whenever you earthbend” abilities:

trigger: {
  type: "EARTHBEND" /* New API */
}

An illegal or missing target emits no event. The event is emitted after the normal PUT_COUNTER event from the keyword action.

Sagas keep their timing and chapter effects separate: an ENTERS or BEGIN_FIRST_MAIN ability puts on lore, then every exact chapter threshold crossed by that placement triggers. A queued chapter does not recheck the lore condition. Removing lore and crossing a threshold later can trigger that chapter again. The greatest exact lore threshold is the final chapter; after that chapter leaves the stack, a Saga still holding at least that much lore is sacrificed as a state-based action.

REMOVE_COUNTER event

Fires after one or more counters are successfully removed from a permanent. It carries the actual counter type and amount removed. Wildcard removal emits one event for each affected counter type.

trigger: {
  type: "REMOVE_COUNTER",
  source: "SELF",
  counter: "time"
}

EVENT_CARD is a post-removal snapshot. Conditions can therefore test whether the event removed the last counter without being changed if counters are added to the permanent before the resulting trigger resolves:

condition: {
  count: { target: "EVENT_CARD", counters: "time" },
  comparison: "EQUAL",
  value: 0
}

Notes:

  • All turn hooks emit events and can be used as triggered ability hooks.
  • CAST_SPELL.sourceZone records where the spell was cast from when known.
  • A self-cast spell's stack object records manaSpent: the actual mana paid for its main and mandatory additional costs after reductions and Convoke. Free casts begin at zero, and mana paid to activate mana abilities is excluded. { event: { attribute: "MANA_SPENT" } } reads that value through the originating sourceSpell and returns zero when no recorded payment exists.
  • manaSpentFrom.filter matches when at least one mana source whose mana contributed to the spell payment matches the filter. The payment includes the main cost and mandatory additional mana costs. The stack object stores a last-known snapshot before a source is tapped, sacrificed, or otherwise changed, so a sacrificed Treasure still matches its printed name and types. A source that was merely available, or activated but contributed no mana, does not match. Convoke, Delve, cost reductions, and free-cast permissions are not mana sources. Mixed payments match once when any contributing source matches.
  • The same stack object records manaColorsSpent: the concrete colored mana types used by those payments. { source: "MANA_COLOURS_SPENT" } counts its distinct entries through the Count API. Spell copies reset both payment records because no mana was spent to cast the copy.
  • CAST_SPELL.requiredManaSourceId is an exact payment constraint used by a cast action. The engine accepts it only when that permanent can legally produce mana that contributes to the nonzero payment. An invalid constraint rejects the cast before moving the card or paying any cost.
  • Use sourceZone: { not: "hand" } for "casts from anywhere other than hand" triggers.
  • Unknown cast source zones do not satisfy negated source-zone triggers.
  • from is the zone the card moved from; to is the zone where it finished.
  • Omit from on an ENTERS trigger when the origin does not matter.
  • Use MOVE_CARD with from: "battlefield" for a permanent leaving the battlefield, and omit to when the destination does not matter.
  • Set another: true on MOVE_CARD to exclude the triggered ability's source.
  • ETB is ENTERS with to: "battlefield".
  • Death is ENTERS with from: "battlefield" and to: "graveyard".
  • Mill is ENTERS with from: "library" and to: "graveyard".
  • A discard that actually reaches the graveyard also emits ENTERS with from: "hand" and to: "graveyard"; the semantic discard event is DISCARDED, including when a replacement sends the card elsewhere.
  • Resolved instant/sorcery cards entering the graveyard use from: "stack".
  • Creature tokens that die emit the death-style ENTERS event even though they are not stored in the graveyard array; see Permanent Death Triggers.
  • CARD_PUT_INTO_GRAVEYARD and ENTER_BATTLEFIELD are not public authoring primitives.
  • CREATURE_DIED is legacy compatibility only; new card definitions should use ENTERS plus a filter.

Turn-Cycle Hooks

BEGIN_UNTAP_STEP

Fires during the tracked player's untap step and at the start of every simulated opponent untap step. Its abilities resolve before the corresponding upkeep event.

{
  trigger: {
    type: "BEGIN_UNTAP_STEP", /* New API */
    player: "OPPONENT"
  },
  effects: [{ type: "UNTAP_PERMANENT", target: "SELF" }]
}

Notes:

  • player can be SELF, OPPONENT, or EACH. An omitted player defaults to SELF.
  • Existing untapsEachUntapStep definitions continue to untap only their source permanent during simulated opponent untap steps.

BEGIN_UPKEEP

Fires at the beginning of upkeep.

{
  trigger: {
    type: "BEGIN_UPKEEP",
    player: "SELF"
  },
  effects: [
    { type: "DRAW_CARDS", count: 1 }
  ]
}

Notes:

  • player can be SELF, OPPONENT, or EACH where a card cares about whose upkeep it is. An omitted player defaults to SELF.

BEGIN_DRAW_STEP

Fires at the beginning of a draw step. Its abilities trigger as the step begins, but the normal turn-based draw occurs before those queued abilities resolve. The engine keeps the draw step active through that resolution and any draw-replacement choices, so first- and other-draw tracking remains correct. Use player: "SELF" for your draw step, player: "OPPONENT" for simulated opponent draw steps, and player: "EACH" for every player's draw step.

{
  trigger: { type: "BEGIN_DRAW_STEP", player: "EACH" }, /* Widened API */
  condition: { source: { tapped: true } }, /* New API */
  effects: [{ type: "DRAW_CARDS", amount: 1, player: "ACTIVE_PLAYER" }]
}

BEGIN_FIRST_MAIN

Fires at the beginning of the first main phase.

{
  trigger: {
    type: "BEGIN_FIRST_MAIN"
  },
  effects: [
    {
      type: "MILL",
      count: 3
    }
  ]
}

Notes:

  • Ripples of Undeath uses this phase hook for its start-of-main behavior.

BEGIN_COMBAT

Fires at the beginning of each combat. The event identifies whether the active player is SELF or OPPONENT; opponent events also carry the exact opponentId. A normal three-opponent turn cycle emits four events. Additional combats emit another event for their active player.

{
  trigger: {
    type: "BEGIN_COMBAT",
    player: "EACH" /* Optional: EACH | OPPONENT | SELF; defaults to SELF */
  },
  effects: [
    { type: "CREATE_TOKEN", count: 1, name: "Nymph" }
  ]
}

Notes:

  • Opponent beginning-of-combat triggers resolve before the abstract opponent attack and before a goaded opponent creature attacks.
  • TurnZero combat usually only matters when attacking produces modeled resources.

BEGIN_SECOND_MAIN

Fires at the beginning of the second main phase.

{
  trigger: {
    type: "BEGIN_SECOND_MAIN"
  },
  effects: [
    { type: "DRAW_CARDS", count: 1 }
  ]
}

Notes:

  • Use this only for cards that explicitly trigger at that phase.

BEGIN_END_STEP

Fires at the beginning of the end step.

{
  trigger: {
    type: "BEGIN_END_STEP",
    player: "SELF" /* New API */
  },
  effects: [
    {
      type: "SACRIFICE_PERMANENT",
      target: "SELF"
    }
  ]
}

Notes:

  • player: "SELF" means your end step, player: "OPPONENT" means an opponent's end step, and player: "EACH" means every end step. Omitting player retains the compatibility behavior of matching every end step.
  • Temporary "until end of turn" grants expire during cleanup after end-step triggers have resolved.
  • Additional end steps emit the same event again before cleanup. Until-end-of-turn effects remain active through all of them.

BEGIN_CLEANUP_STEP

Fires at the beginning of a cleanup step on the tracked player's turn and on every simulated opponent turn, after that step's turn-based actions (rule 514.3a): the hand-size discard has been made, and damage and "until end of turn" effects have already ended. Abilities that trigger here resolve before the next turn begins, and a fresh cleanup then ends anything they created "until end of turn".

{
  type: "CREATE_DELAYED_TRIGGER",
  source: { card: "SOURCE" },
  trigger: { type: "BEGIN_CLEANUP_STEP", player: "EACH" }, /* New API */
  effects: [{ type: "SACRIFICE_PERMANENT", target: "SELF" }]
}

Notes:

  • player can be SELF, OPPONENT, or EACH. An omitted player defaults to SELF.
  • A delayed trigger waiting for this event must not use until: "end of turn"; the cleanup discards such triggers before the event is emitted.
  • The engine emits the event only when a triggered ability, delayed trigger, or player-granted ability is waiting for it, so observers must not rely on seeing it every turn.
  • Because "until end of turn" effects have already ended, a permanent sacrificed here reports its unpumped last known power and toughness.

Engine Steps

Untap, draw, cleanup, and other turn engine steps exist, but they are not all general card-trigger hooks in the DSL. BEGIN_UNTAP_STEP is the untap-step hook and BEGIN_CLEANUP_STEP is the cleanup-step hook.

Useful distinctions:

  • Untap and cleanup maintain battlefield state, temporary grants, once-per-turn flags, and hand size.
  • Phased-out permanents phase in automatically at the start of untap, before permanents are untapped. This does not use the stack.
  • Floating mana empties when moving into combat, second main, and the end step; it cannot carry across a phase or step boundary.
  • Normal draw emits draw events; MOVE_CARD from library to hand does not count as drawing.
  • End-step triggers happen before cleanup removes "until end of turn" grants.
  • If a new card needs a hook that does not exist, add the trigger type with focused engine tests before adding the card.

MTG Mechanics

Earthbend

Earthbend is modeled as the EARTHBEND effect rather than as a printed CardKeyword, because it is an action performed by a resolving spell or ability. Use the effect directly and let the engine own the animation, counters, return triggers, zone-object checks, and EARTHBEND event. Do not rebuild those steps from separate GRANT, PUT_COUNTER, and MOVE_CARD effects in individual card definitions.

Cycling and Landcycling

Cycling and Landcycling are first-class parameterized keyword mechanics. Put their printed activation cost in the ordinary keywords array:

keywords: [
  {
    type: "Cycling",
    cost: { mana: { generic: 2 } }
  }
]

The engine creates an instant-speed activated ability available from hand. It adds the required self-discard cost and makes ordinary Cycling draw one card. Do not include discardCard in the keyword's cost, and do not repeat Cycling as an authored hand-zone activatedAbilities entry. Non-mana printed Cycling costs use the same Cost fields. X costs use the normal x plus { variable: "X" }; a fixed generic component such as {X}{1} is { variable: "X", offset: 1 }.

Landcycling uses either a land subtype or the basic-land marker. The engine creates the matching revealed-to-hand library search:

keywords: [
  {
    type: "Landcycling",
    cost: { mana: { generic: 1 } },
    subtype: "Swamp"
  }
]

keywords: [
  {
    type: "Landcycling",
    cost: { mana: { generic: 2 } },
    basic: true
  }
]

Landcycling is also treated as Cycling when matching keywords: ["Cycling"] and emits the same CYCLED event. Multiple Cycling or Landcycling entries create distinct generated activations named cycling, cycling-2, and so on. The activated ability uses the stack; CYCLED and discard triggers are queued after its cost is paid.

Ascend

Ascend is a parameterless keyword. Record it in the ordinary keywords array:

keywords: ["Ascend"] /* New API */

While the player controls a permanent with Ascend, controlling ten or more permanents grants the persistent CITYS_BLESSING player status. An instant or sorcery with Ascend performs the same check as that spell resolves, before its effects. The status remains for the rest of the game even if the Ascend source leaves or the permanent count falls below ten.

Read the blessing through the shared player-status count:

condition: {
  count: {
    source: "PLAYER_STATUS",
    status: "CITYS_BLESSING"
  },
  comparison: "AT_LEAST",
  value: 1
}

Storied

Storied is a parameterless keyword. Record it in the ordinary keywords array:

keywords: ["Storied"] /* New API */

While the player controls a permanent with Storied, the engine counts distinct controlled permanents that are artifacts, legendary, and/or Sagas. A permanent matching more than one quality contributes only once. At three or more, the player gains the ENDURING_STORY status for the rest of the game. Losing the qualifying permanents or the Storied permanent does not remove that status.

Dredge

Dredge is a first-class parameterized keyword. Record the printed mill count in the ordinary keywords array:

keywords: [
  {
    type: "Dredge", /* New API */
    count: 4
  }
]

The keyword is active only while its card is in its owner's graveyard. Before each individual draw, the engine offers one action for every Dredge card whose full count can be milled, plus the normal draw when no mandatory draw replacement still applies. Choosing one card mills exactly its count, returns that exact graveyard object to hand, and consumes the draw; another Dredge card cannot replace the same draw. Cards milled this way can replace later draws.

When Dredge and another draw replacement both apply, the player chooses which replacement to apply first. A replacement cannot apply to the same draw event more than once. Replaced draws do not increment draw counters or emit DRAW_CARD.

Discover

Discover is a first-class parameterized keyword. Card definitions and mana bonuses name the mechanic without reproducing its reveal implementation:

{
  type: "GRANT",
  kind: "keyword",
  target: "PAID_SPELL", /* New API */
  keyword: {
    type: "Discover",
    count: { event: { attribute: "MANA_VALUE" } }
  }
}

The engine resolves count when the spell is cast, including the triggering spell's chosen X. Each Discover keyword queues its own triggered ability above that spell, using the permanent that granted the keyword as the trigger source. When the trigger resolves, the engine exiles cards from the top of the library until it exiles a nonland card with mana value no greater than the Discover count. The player may cast that card without paying its mana cost; declining puts it into hand. The other exiled cards go on the bottom of the library in a random order before the discovered spell resolves.

PAID_SPELL is valid only for a structured Discover keyword grant made by a mana bonus. The grant is consumed by that spell's cast event and does not remain on the resulting permanent.

Miracle

Miracle is a first-class parameterized keyword. Record the printed alternate mana cost in the ordinary keywords array:

keywords: [
  {
    type: "Miracle", /* New API */
    cost: { generic: 1, black: 1 }
  }
]

When this exact card is the first card actually drawn in a turn, the engine queues its Miracle triggered ability. When that ability resolves, it offers a costed PLAY_CARD choice for the drawn card from hand. The choice bypasses normal card-type timing, including during an opponent turn, while still paying spell-cost modifiers and mandatory additional costs. Declining leaves the card in hand. Moving a card from the library to hand without drawing it, replacing the draw, or drawing the card later in the turn does not create this choice.

TurnZero does not separately track opponent knowledge of the reveal. Offering the Miracle cast-or-decline choice represents the reveal and its resulting trigger within the goldfish model.

Madness

Madness is a first-class parameterized keyword mechanic. Put the complete printed alternate cost in the ordinary keywords array:

keywords: [
  {
    type: "Madness",
    cost: { mana: { generic: 1, red: 1 } }
  }
]

The engine supplies both parts of Madness. A semantic discard moves the card to exile instead of the graveyard, then its generated trigger offers the card for the stated cost. Declining the offer, or being unable to cast the card, moves it from exile to the graveyard. A generic hand-to-graveyard MOVE_CARD is not a discard and does not invoke Madness.

The generated cast ignores normal card-type timing, applies spell-cost modifiers and mandatory additional costs, and marks the spell with alternateCostId: "madness". Use an ALTERNATE_COST_PAID condition with id: "madness" for text that checks whether the Madness cost was paid. If the discard paid a spell or ability cost, the discard event remains deferred until that object is on the stack.

Non-mana Madness costs use the ordinary Cost fields. An X cost uses x and { variable: "X" } in the cost's mana component; each payable X is offered as a distinct legal action and the chosen value is carried through the spell and its resulting triggers. Explicitly granted structured Madness keywords are functional from hand as well as printed ones. Do not repeat Madness as authored REPLACEMENT_EFFECT, DISCARDED, and PLAY_CARD entries.

Scry

Scry is a first-class mechanic effect. Its count accepts the shared EffectValue shape, and optional: true models an optional instruction:

effects: [
  { type: "SCRY", count: 2 },
  { type: "DRAW_CARDS", count: 1 }
]

Resolving Scry looks at up to the instructed number of cards from the top of the controller's library. The controller assigns every looked-at card to the top or bottom, then orders each destination group. Choices are staged one card at a time and remain bounded for large Scry counts. Cards kept on top become known library information until the top changes.

Use SCRY only when the rules instruction is Scry. Similar bespoke look-and-arrange instructions should continue to use LOOK_AT_LIBRARY and MOVE_CARD, so they do not acquire Scry's semantic identity.

Surveil

Surveil is a first-class mechanic effect. Its count accepts the shared EffectValue shape, and optional: true models an optional instruction:

effects: [
  { type: "SURVEIL", count: 2 },
  { type: "DRAW_CARDS", count: 1 }
]

Resolving Surveil looks at up to the instructed number of cards from the top of the controller's library. The controller assigns every looked-at card to the top of the library or the graveyard, then orders each destination group. Choices are staged one card at a time and remain bounded for large Surveil counts. Cards kept on top become known library information until the top changes.

Use SURVEIL only when the rules instruction is Surveil. Similar bespoke look-and-arrange instructions should continue to use LOOK_AT_LIBRARY and MOVE_CARD, so they do not acquire Surveil's semantic identity.

Backup

Backup is a first-class parameterized keyword mechanic. Record its counter count and the abilities printed beneath it in one structured keyword:

keywords: [
  {
    type: "Backup",
    count: 1,
    grants: [
      {
        kind: "activated ability",
        ability: {
          id: "sacrifice-to-draw",
          cost: {
            mana: { generic: 1 },
            sacrificePermanent: { source: "SELF" }
          },
          effects: [{ type: "DRAW_CARDS", amount: 1 }]
        }
      }
    ]
  }
]

The engine treats every entry in grants as a native ability of the creature with Backup. When that creature enters, the engine also generates the Backup trigger: choose a creature, put the declared number of +1/+1 counters on it, and, if it is another creature, grant those abilities until end of turn. Supported grant entries use kind: "keyword", "activated ability", "mana ability", or "triggered ability" and the corresponding ordinary DSL payload. Multiple Backup entries create separate triggers with independent targets.

Do not reproduce Backup with an authored ENTERS, PUT_COUNTER, and GRANT sequence. The grants payload is the single definition of the abilities below Backup; do not duplicate those abilities in the card's top-level keyword or ability arrays.

Riot

Riot is a first-class parameterless keyword mechanic:

keywords: ["Riot"] /* New API */

Each Riot instance creates a mandatory as-enters choice between one +1/+1 counter and haste. The counter uses the pending-entry counter path, including counter-placement modifiers and normal placement events. Haste uses the ordinary keyword-grant behavior and removes summoning sickness.

Multiple printed or separately granted Riot instances create independent choices. A battlefield static keyword grant is evaluated prospectively against the permanent as it will exist on the battlefield. The grant does not attach Riot to the card in its source zone, and a source entering in the same batch is not active soon enough to grant Riot to the other entries.

Use GRANT with kind: "keyword" for continuous effects that give Riot. Do not reproduce Riot in a card definition with authored CHOOSE_ONE, PUT_COUNTER, or GRANT effects.

Myriad

Myriad is a first-class keyword mechanic:

keywords: ["Myriad"] /* New API */

Each printed or granted Myriad instance supplies its own ATTACKS trigger for its source. On resolution, the trigger uses a source token copy with attacking: { defenderAssignment: "EACH_OTHER_OPPONENT" }, tapped: true, and exileAt: "end of combat". It creates one token copy for every configured opponent other than the opponent the source attacked. Copies enter attacking, so they are not declared attackers and do not trigger Myriad themselves.

Use GRANT with kind: "keyword" when an effect gives Myriad. Do not repeat Myriad as an authored ATTACKS trigger.

Prowess

Prowess is a first-class keyword mechanic. Record the keyword on the card or token definition:

keywords: ["Prowess"]

The engine supplies the triggered ability: whenever the controller casts a noncreature spell, that permanent gets +1/+1 until end of turn. The same definition works for printed creatures and creature-token templates.

Use GRANT with kind: "keyword" when an effect gives Prowess to another permanent. Granted Prowess is functional, disappears with the grant, and stacks with printed or separately granted instances. Do not repeat Prowess as an authored CAST_SPELL trigger.

Undying

Undying is a first-class keyword mechanic:

keywords: ["Undying"] /* New API */

When a creature with Undying dies without a +1/+1 counter, the engine queues one triggered ability for each printed or granted Undying instance. On resolution, the ability returns that exact card from its owner's graveyard under its owner's control only if it remains there, then puts one +1/+1 counter on the new battlefield object. Tokens cannot return. If another Undying instance or another effect moves the card first, later instances do nothing.

Use GRANT with kind: "keyword" for temporary or continuous Undying. Do not reproduce it with an authored death trigger.

Gravestorm

Gravestorm is a first-class parameterless keyword mechanic. Author it with the string literal, not an object or a card-defined trigger:

keywords: ["Gravestorm"] /* New API */

The engine supplies a CAST_SPELL self trigger. When that triggered ability resolves, it uses ZONE_CHANGES_THIS_TURN to count movements from battlefield to graveyard, then uses COPY_SPELL to create that many copies of its source spell. The count includes every permanent kind, tokens, and qualifying assumed movements already recorded in normal zone-movement history. Discard, mill, battlefield-to-exile movement, same-zone operations, and history from an earlier turn do not count. A replacement that moves the permanent somewhere other than the graveyard records its actual destination and does not count.

The cast trigger remains on the stack if the original spell is countered or otherwise leaves the stack. Each copy inherits the original target and other copiable choices, then independently offers every legal target choice. It may retain the inherited target by selecting it again or select a different legal target. Normal target revalidation and fizzle behavior still apply to the original and every copy.

Spell copies are separate stack objects. They are not cast and cannot trigger Gravestorm recursively. Do not reproduce Gravestorm with an authored CAST_SPELL, COPY_SPELL, or zone-history count in a card definition.

Storm

Storm is a first-class parameterless keyword mechanic. Author it with the string literal, not an object or a card-defined trigger:

keywords: ["Storm"] /* New API */

The engine supplies a CAST_SPELL self trigger. When that triggered ability resolves, it uses SPELLS_CAST_THIS_TURN with before: "SOURCE" to count every spell cast this turn before the source spell, by any player, then uses COPY_SPELL to create that many copies of its source spell. Spells cast after the source, the source itself, and history from an earlier turn do not count. Each keyword instance supplies its own trigger, so a spell with Storm twice copies itself twice per earlier spell.

The cast trigger remains on the stack if the original spell is countered or otherwise leaves the stack. Each copy inherits the original target and other copiable choices, then independently offers every legal target choice. It may retain the inherited target by selecting it again or select a different legal target. Normal target revalidation and fizzle behavior still apply to the original and every copy.

Spell copies are separate stack objects. They are not cast and cannot trigger Storm recursively. Use GRANT with kind: "keyword" and a stack target zone for "instant and sorcery spells you cast have storm". Do not reproduce Storm with an authored CAST_SPELL, COPY_SPELL, or spells-cast count in a card definition; reserve the longhand form for variants such as Thousand-Year Storm whose count differs from the keyword.

Persist

Persist is a first-class parameterless keyword mechanic:

keywords: ["Persist"] /* New API */

When a creature with Persist dies without a -1/-1 counter, the engine queues one triggered ability for each printed or granted Persist instance. On resolution, the ability returns that exact card from its owner's graveyard under its owner's control only if it remains there, then puts one -1/-1 counter on the new battlefield object. Tokens cannot return. If another Persist instance or another effect moves the card first, later instances do nothing.

Use GRANT with kind: "keyword" for temporary or continuous Persist. Granted Persist works the same as printed Persist and stacks with other instances. Do not reproduce it with an authored death trigger.

Enlist

Enlist is a first-class parameterless keyword mechanic:

keywords: ["Enlist"] /* New API */

After attackers are chosen, each attacking Enlist instance may be assigned one untapped, nonattacking creature controlled by the attacker. The enlisted creature must have haste or have been controlled since the turn began; in the engine this is represented by the same summoningSick state used for attacking and tap-cost legality. Finishing the staged choice taps every assigned creature and queues a normal triggered ability that gives its attacker +X/+0 until end of turn, where X is the enlisted creature's power when the trigger resolves. Last-known power is used if that creature has left the battlefield.

The choice exposes individual select and deselect actions plus an explicit finish action. A creature may be assigned only once, and legal actions are proportional to the Enlist entries and eligible creatures rather than every possible assignment set. Simultaneous Enlist and attack triggers use the staged triggered-ability ordering choice, so a power-dependent attack trigger may resolve before or after the Enlist boost as its controller chooses.

Do not reproduce Enlist with an authored attack trigger or tap effect. A keyword granted through the ordinary keyword-grant machinery participates in the same attacker-declaration choice.

Firebending

Firebending is a first-class parameterized keyword mechanic. Put its structured keyword object in the same keywords array as parameterless string keywords:

keywords: [
  "Haste",
  { type: "Firebending", amount: 2 }
]

amount is an EffectAmount, so it may be fixed or derived from the source's characteristics. Whenever the permanent attacks, the engine creates a normal triggered ability that adds that much red mana and retains that specific mana until end of combat. The trigger uses the stack and is not a mana ability. Multiple Firebending entries create separate triggers, and a dynamic amount is evaluated when each trigger resolves.

Magic normally empties unused mana as each step and phase ends. Firebending's retention therefore matters inside combat: its mana survives the declare attackers step and later combat steps, but empties when combat ends. It does not retain unrelated mana.

Use the same structured keyword with GRANT when an effect grants Firebending:

{
  type: "GRANT",
  kind: "keyword",
  permanents: {
    findCards: {
      zone: "battlefield",
      filter: { controller: "SELF", types: ["Creature"] }
    }
  },
  until: "end of turn",
  keyword: { type: "Firebending", amount: 5 }
}

The grant is functional and each affected permanent receives its own attack trigger. Do not author the equivalent ATTACKS, ADD_MANA, and RETAIN_MANA sequence on individual cards. Firebending-specific events such as "whenever you firebend" are not yet modeled.

First Strike and Double Strike

Combat uses a separate first-strike damage step whenever at least one attacking creature has First strike or Double strike as combat damage begins. Resolving that step sets combatStep to POST_FIRST_STRIKE_DAMAGE (/* New API */) and resolves its damage triggers before the regular combat-damage step. The engine then exposes another RESOLVE_COMBAT_DAMAGE action; instant-speed actions remain legal between the two steps.

Creatures with First strike deal damage only in the first step. Creatures with Double strike deal damage in both steps. The engine snapshots the IDs of creatures that participated in the first step and applies keyword changes between steps using Magic's sequencing:

  • A first-strike participant deals regular damage only if it currently has Double strike.
  • A creature that did not participate in the first step still deals regular damage even if it gains First strike afterward.
  • A double-strike creature that loses Double strike after first-strike damage does not deal regular damage.
  • A creature removed from the battlefield before the regular step does not deal regular damage.

When no attacker has First strike or Double strike, combat retains the single regular damage step. Blockers and defensive first-strike interactions remain outside TurnZero's goldfish combat model.

Planeswalkers

Planeswalkers are permanents with a printed starting loyalty value. They enter with that many loyalty counters. Mark each loyalty ability with loyalty: true; the engine then applies sorcery timing and allows only one loyalty ability from that planeswalker instance each turn.

"Quintorius, History Chaser": {
  name: "Quintorius, History Chaser",
  colorIdentity: ["red", "white"],
  types: ["Planeswalker"],
  subtypes: ["Quintorius"],
  legendary: true,
  loyalty: 5,
  manaCost: { generic: 2, red: 1, white: 1 },
  roles: ["Draw", "Payoff", "Synergy"],
  triggeredAbilities: [
    {
      trigger: { type: "MOVE_CARD", from: "graveyard" },
      effects: [
        { type: "CREATE_TOKEN", count: 1, name: "Quintorius Spirit" }
      ]
    }
  ],
  activatedAbilities: [
    {
      id: "discard-draw-mill",
      loyalty: true,
      cost: {
        putCounter: {
          source: "SELF",
          counter: { type: "loyalty", amount: 1 }
        }
      },
      effects: [
        {
          type: "DISCARD_CARDS",
          id: "quintorius-discarded",
          count: 1,
          choice: true,
          optional: true
        },
        {
          type: "DRAW_CARDS",
          count: 2,
          condition: { refExists: "quintorius-discarded" }
        },
        {
          type: "MILL",
          count: 1,
          condition: { refExists: "quintorius-discarded" }
        }
      ]
    },
    {
      id: "grant-spirit-keywords",
      loyalty: true,
      cost: {
        removeCounter: {
          source: "SELF",
          counter: { type: "loyalty", amount: 4 }
        }
      },
      effects: [
        {
          type: "GRANT",
          kind: "keyword",
          permanents: {
            findCards: {
              zone: "battlefield",
              filter: {
                controller: "SELF",
                types: ["Creature"],
                subtypes: ["Spirit"]
              }
            }
          },
          until: "end of turn",
          keyword: "Double strike"
        },
        {
          type: "GRANT",
          kind: "keyword",
          permanents: {
            findCards: {
              zone: "battlefield",
              filter: {
                controller: "SELF",
                types: ["Creature"],
                subtypes: ["Spirit"]
              }
            }
          },
          until: "end of turn",
          keyword: "Vigilance"
        }
      ]
    }
  ]
}

The normal cost rules still apply: an ability that removes loyalty is not legal without enough loyalty counters. A planeswalker with no loyalty counters is put into its owner's graveyard by state-based actions. Planeswalker damage and combat redirection are not modeled yet.

The MOVE_CARD trigger is a grouped movement event. It therefore creates one Quintorius Spirit for one completed operation that moves one or more cards out of the graveyard, as the card's Oracle text requires.

Modal Spells

Use the modal API that matches when the selection occurs.

Pawprint modal spells use a cast-time budget and may repeat modes. Put the card's maximum on pawprintBudget and the contribution of each mode on its quoted "🐾" field. The engine offers every mode multiset whose total is at most the budget, including choosing no modes, and resolves the selected modes in printed order:

{
  pawprintBudget: 5,
  spellModes: [
    { id: "one-paw", "🐾": 1, effects: [] },
    { id: "two-paw", "🐾": 2, effects: [] },
    { id: "three-paw", "🐾": 3, effects: [] }
  ]
}

pawprintBudget is mutually exclusive with chooseModeCount. Unlike ordinary modal selection, repeated mode indexes are legal. The emoji property is quoted because it is not a valid bare TypeScript identifier.

Modal casting is staged by the engine. The normal legal-action window exposes one BEGIN_MODAL_SPELL_CAST action for each available casting route. Choosing it exposes legal CHOOSE_SPELL_MODES actions, followed by CHOOSE_MODAL_SPELL_TARGETS when a selected mode requires targets. Only after those choices does the engine expose finalized CAST_SPELL cost alternatives. Card definitions opt into this flow automatically by using spellModes; no card-specific flag is required. Direct engine callers may still pass a complete modeIndexes selection to canCastSpell or castSpell.

Put costs that apply only to one mode on that mode's additionalCosts. They use the shared Cost[] primitive and are combined with spell-wide and alternate additional costs for legality, payment, mana-spent tracking, repeated pawprint modes, and free casts:

{
  chooseModeCount: 1,
  spellModes: [
    { id: "base", effects: [] },
    {
      id: "upgraded",
      additionalCosts: [{ mana: { generic: 1 } }],
      effects: []
    }
  ]
}

Set chooseAllModesWithCommander: true for "Choose one. If you control a commander as you cast this spell, you may choose both instead." Without a commander, the normal chooseModeCount choices are legal. While a commander is on your battlefield, the all-modes choice is added without removing those normal choices.

For a genuinely mutually exclusive resolution branch, put CHOOSE_ONE in effects. Do not use it as a generic wrapper for "you may" or "if you do"; prefer the choice-producing primitive, its stored result, and a conditioned follow-up. CHOOSE_ONE is for printed choices and branches whose alternatives are independently meaningful. Its labelled options become the manual tester's CHOOSE_EFFECT_OPTION actions; only the selected option's effects resolve. Targets declared by an option stay on that option's effects. The engine first filters only the unavailable options, then asks for the selected option's targets with CHOOSE_EFFECT_TARGET. An unavailable targeted option does not prevent another legal option from resolving, and unselected options never require targets. Borrowed Knowledge uses this shape:

"Borrowed Knowledge": {
  assumptions: {
    opponent: { handSize: 6 }
  },
  effects: [
    {
      type: "CHOOSE_ONE",
      options: [
        {
          id: "opponent-hand",
          label: "Discard your hand, then draw cards equal to an opponent's hand",
          effects: [
            {
              type: "DISCARD_CARDS",
              count: { source: "HAND_SIZE_AT_RESOLUTION" }
            },
            {
              type: "DRAW_CARDS",
              count: { source: "ESTIMATED_OPPONENT_HAND_SIZE" }
            }
          ]
        },
        {
          id: "discarded-hand",
          label: "Discard your hand, then draw that many cards",
          effects: [
            {
              type: "DISCARD_CARDS",
              count: { source: "HAND_SIZE_AT_RESOLUTION" }
            },
            {
              type: "DRAW_CARDS",
              count: { source: "HAND_SIZE_AT_RESOLUTION" }
            }
          ]
        }
      ]
    }
  ]
}

Give a CHOOSE_ONE effect an id and remember: "SOURCE" when option availability belongs to the source permanent. An option may use matchingCountThisTurn: 1 to remain legal only until that option has been chosen once during the current turn. Each option is counted independently, the counts reset as each player's turn begins, and a source that changes zones is a new object with fresh availability. Lifetime uses limits remain separate and do not reset each turn.

{
  type: "CHOOSE_ONE",
  id: "turn-modes",
  remember: "SOURCE",
  options: [
    {
      id: "draw-card",
      matchingCountThisTurn: 1, /* New API */
      effects: [{ type: "DRAW_CARDS", count: 1 }]
    }
  ]
}

For modes selected as the spell is cast, use spellModes with chooseModeCount. A number requires exactly that many distinct modes. The ranged form chooses any distinct count from minimum through maximum; omitting maximum allows every mode. The cast action records the chosen mode indexes in printed order, and only those mode effects resolve.

maximum may be an EffectValue. The engine resolves it from the current game state with the card being cast as the source while generating and validating the cast. The chosen mode indexes remain fixed after the spell is cast.

For modes chosen while an effect resolves, use CHOOSE_MODES. Each option is authored once. The engine exposes staged select and deselect actions plus a finish action after the minimum has been reached, so legal-action generation stays proportional to the number of modes instead of enumerating subsets. Selection reaching the maximum finishes automatically. Selected modes resolve only after selection is complete and always in printed order.

{
  type: "CHOOSE_MODES", /* New API */
  chooseModeCount: { minimum: 1, maximum: 3 },
  allModesLabel: "Full Send", /* New API: display only */
  options: [
    { id: "first", effects: [] },
    { id: "second", effects: [] },
    { id: "third", effects: [] }
  ]
}

allModesLabel has no rules meaning. Manual-play formatting uses it when the selection contains every available mode. allowRepeated: true retains the existing SpellModeCount meaning; otherwise each option can be selected only once. Set minimum: 0 for "choose any number." The initial legal actions then include one select action per mode and an immediate finish action for choosing none. A positive minimum withholds finish until that many modes are selected.

chooseModeCount: { /* New API */
  minimum: 1,
  maximum: 4
}

Set allowRepeated: true on the ranged form when the spell says the same mode may be chosen more than once. The engine enumerates canonical multisets in printed mode order and applies the minimum and maximum to the total number of chosen modes. Each repeated occurrence receives its own definition-local ids, so targeted occurrences may choose independently and stored results do not overwrite one another:

chooseModeCount: {
  minimum: 3,
  maximum: 3,
  allowRepeated: true /* New API */
}

Cryptic Command chooses exactly two distinct modes:

"Cryptic Command": {
  chooseModeCount: 2,
  spellModes: [
    {
      id: "counter-spell",
      effects: [{ type: "COUNTER_TARGET_SPELL" }]
    },
    {
      id: "return-permanent",
      effects: [
        {
          type: "MOVE_CARD",
          target: {
            id: "cryptic-command-permanent",
            zone: "battlefield"
          },
          to: "hand"
        }
      ]
    },
    {
      id: "tap-opponents-creatures",
      effects: []
    },
    {
      id: "draw-card",
      effects: [{ type: "DRAW_CARDS", count: 1 }]
    }
  ]
}

Kicker

Kicker and Multikicker are first-class structured keywords, not alternate mana costs or spell modes. Put only the printed payment in cost. Kicker supplies an optional one-time additional cost, offering the ordinary cast whether or not the Kicker payment is affordable and adding a kicked cast when it is:

keywords: [
  {
    type: "Kicker",
    cost: { mana: { generic: 5 } }
  }
]

cost is the ordinary Cost type, so Kicker may require mana, life, cards, or permanents as printed. Effects that say "if this spell was kicked" compose the semantic KICKER_COSTS_PAID count with an ordinary condition or conditional value:

count: {
  match: {
    count: { source: "KICKER_COSTS_PAID" },
    comparison: "EQUAL",
    value: 1
  },
  matched: 5,
  default: 1
}

For cards with two independent "and/or" Kicker costs, use two structured keywords with semantic ids. KICKER_COSTS_PAID without an id counts every paid Kicker cost; passing an id tests only that labeled cost:

keywords: [
  {
    type: "Kicker",
    id: "blue",
    cost: { mana: { generic: 1, blue: 1 } }
  },
  {
    type: "Kicker",
    id: "red",
    cost: { mana: { generic: 1, red: 1 } }
  }
]

condition: {
  count: { source: "KICKER_COSTS_PAID", id: "blue" },
  comparison: "EQUAL",
  value: 1
}

Multikicker uses the same structured shape, but the engine lowers it through the shared repeatable additional-cost facility. It offers zero through the maximum payable count, charging its cost once per selected count:

keywords: [
  {
    type: "Multikicker", /* New API */
    cost: { mana: { generic: 2 } }
  }
]

KICKER_COSTS_PAID is shared semantic accounting: each ordinary Kicker adds one, while each Multikicker adds its selected repeat count. An id reads that specific structured keyword; without one the count sums all Kicker and Multikicker payments. Free casts waive only the main mana cost, so a payable Multikicker remains offered and charged. Copies retain the selected count, and entry counters resolve with the resolving spell context through ordinary and as-enters battlefield-entry continuations.

Keep the printed resolution semantics intact. For "create five of those tokens instead," use one token-creation effect whose count is one or five. Do not model it as separate one-token and four-token effects, because token-creation replacement effects inspect each logical token group. Free casts waive only the spell's main mana cost, so the engine still offers and charges a payable kicker. Copies of a kicked spell retain the paid-cost marker.

Replicate

Replicate is a first-class structured keyword. Put the printed repeat payment in cost:

keywords: [
  {
    type: "Replicate", /* New API */
    cost: { mana: { generic: 2 } }
  }
]

The engine offers zero through the maximum payable repeat count and charges cost once for each selected repetition. A free cast waives only the spell's main mana cost, so every selected Replicate payment is still charged. The shared repeatable-cost bound keeps legal-action generation linear in the affordable count rather than enumerating payment subsets.

Casting the spell queues one Replicate triggered ability above it. When that ability resolves, it creates one spell copy for each Replicate payment, even if the original spell has already left the stack. The copies are created on the stack and are not cast, so they do not create cast events or Replicate again. Each copy initially retains the original spell's targets and offers its controller one complete legal target choice because Replicate permits new targets.

A copied permanent spell resolves as a token. This includes copied Aura spells: changing a copied Aura's target also changes its pending attachment, and the Aura token enters attached to that chosen legal target. A copy with no legal target does not resolve onto the battlefield.

Council's Dilemma

Council's dilemma first collects every vote, then resolves the card's vote-counted instructions in order. Fateful Tempest keeps the goldfish choice that opponents vote Present in metadata, while the effects remain its printed rules:

Fateful Tempest

"Fateful Tempest": {
  assumptions: {
    opponent: {
      choices: [
        { choiceId: "fateful-tempest-vote", optionId: "present" }
      ]
    }
  },
  effects: [
    {
      type: "VOTE",
      id: "fateful-tempest-vote",
      players: "EACH_PLAYER",
      startingWith: "SELF",
      options: [
        { id: "past", label: "Past — mill cards and deal damage" },
        { id: "present", label: "Present — exile cards for play access" }
      ]
    },
    {
      type: "MILL",
      id: "fateful-tempest-milled",
      count: {
        source: "VOTE_COUNT",
        voteId: "fateful-tempest-vote",
        optionId: "past"
      }
    },
    {
      type: "DEAL_DAMAGE",
      amount: {
        type: "SUM",
        attribute: "MANA_VALUE",
        source: { ref: "fateful-tempest-milled" }
      },
      target: "OPPONENT"
    }
  ]
}

Gift

gift is a singular optional cast choice. Its effect is ordinary effect DSL, while the engine owns the special Gift timing and recipient rules:

gift: { /* New API */
  effect: {
    type: "DRAW_CARDS",
    amount: 1,
    player: "GIFT_RECIPIENT" /* New API */
  }
}

For a spell with Gift, legal-action generation offers the unpromised cast and one promised cast for each simulated opponent. The selected opponent is stored on the spell. If the spell resolves, gift.effect resolves for that opponent before the spell's other effects. If the spell does not resolve, the gift is not given. A spell copy inherits the original promise and recipient. Promised and unpromised target branches are generated independently, including for free-cast and play-card choices, so either route can remain legal when the other has no matching target.

Ordinary effects can check whether the resolving spell's gift was promised:

{
  type: "GRANT",
  kind: "keyword",
  permanents: {
    findCards: {
      zone: "battlefield",
      filter: { controller: "SELF" }
    }
  },
  keyword: "Indestructible",
  until: "end of turn",
  condition: { type: "GIFT_PROMISED" } /* New API */
}

CONDITIONAL can also select different targeted effects for the promised and unpromised casts. Keep the target on the branch effect rather than on gift; legal-action generation chooses the applicable branch before offering targets:

{
  type: "CONDITIONAL",
  if: { type: "GIFT_PROMISED" },
  matched: {
    effects: [{
      type: "MOVE_CARD",
      target: {
        id: "gifted-target",
        choice: true,
        findCards: {
          zone: "battlefield",
          filter: {
            controller: "OPPONENT",
            not: { types: ["Land"] }
          }
        }
      },
      to: "hand"
    }]
  },
  rest: {
    effects: [{
      type: "MOVE_CARD",
      target: {
        id: "ungifted-target",
        choice: true,
        findCards: {
          zone: "battlefield",
          filter: {
            controller: "OPPONENT",
            types: ["Creature"]
          }
        }
      },
      to: "hand"
    }]
  }
}

GIFT_RECIPIENT is valid only while resolving a promised gift. Without a recipient it resolves to no player. Opponent draws are represented by the existing abstract opponent draw event and per-opponent count; opponent libraries and hands are not tracked.

Extort

Extort is composed from a self-cast trigger, an optional hybrid PAY_COST, non-targeted life loss for each opponent, and life gain equal to the opponent count:

triggeredAbilities: [
  {
    trigger: {
      type: "CAST_SPELL",
      player: "SELF"
    },
    effects: [
      {
        type: "PAY_COST",
        optional: true,
        cost: {
          mana: {
            hybrid: [
              {
                colours: ["white", "black"],
                count: 1
              }
            ]
          }
        },
        effects: [
          {
            type: "LOSE_LIFE",
            amount: 1,
            player: "EACH_OPPONENT" /* New API */
          },
          {
            type: "GAIN_LIFE",
            amount: {
              source: "OPPONENT_COUNT" /* New API */
            }
          }
        ]
      }
    ]
  }
]

Splice

Splice is a first-class structured keyword. filter describes the spell that can receive the copied rules text, and cost is the complete printed splice cost:

keywords: [
  {
    type: "Splice",
    filter: { subtypes: ["Arcane"] },
    cost: { mana: { generic: 1, red: 1 } }
  }
]

When a matching spell is cast, the engine stages the splice decision. Each eligible card in hand is offered individually, selected cards can be removed, and an explicit finish action completes the choice without enumerating every subset or permutation. Selected splice costs are added to the receiving spell's total cost. The splice cards remain in hand.

The selected cards' effects are appended in selection order and become part of the spell on the stack. Copied spells retain the appended effects. Effect ids and references are scoped to each physical splice card so multiple copies do not collide. Structured Splice granted through GRANT is functional.

Each extort ability creates one trigger and therefore offers at most one payment as that trigger resolves. The engine exposes pay only when the hybrid cost is legal and always exposes decline; the pilot chooses between those legal actions. The trigger is created when the spell is cast, so it resolves before that spell and is independent of whether the spell later resolves.

The goldfish engine does not track individual opponent life totals or life-loss prevention and replacement effects. It assumes every simulated opponent loses the instructed 1 life, making the life gained equal to OPPONENT_COUNT. A card definition using Extort should record that limitation precisely in unsupported.

Retrace

Retrace is a graveyard alternate mana cost that adds a land discard while retaining the card's normal mana cost. It is represented by a conditional alternateManaCosts entry with additionalCosts, not as a dedicated card field. Unlike Flashback, omit resolutionDestination, so the spell returns to the graveyard after resolving and can be cast again.

Embrace the Unknown

"Embrace the Unknown": {
  manaCost: { generic: 2, red: 1 },
  alternateManaCosts: [
    {
      id: "retrace",
      cost: { generic: 2, red: 1 },
      sourceZones: ["graveyard"],
      additionalCosts: [
        {
          discardCard: {
            id: "retrace-discard",
            count: 1,
            filter: { types: ["Land"] }
          }
        }
      ]
    }
  ]
}

Escape

Escape is a first-class parameterized keyword. The engine lowers each printed or granted Escape ability into a graveyard alternate-cost route. A fixed cost uses its authored mana cost. "MANA_COST" uses the receiving card instance's current mana cost and creates no route for a card without a mana cost. Unlike Flashback, Escape omits resolutionDestination, so normal spell-type resolution still applies.

keywords: [
  {
    type: "Escape",
    cost: { generic: 3, black: 2 },
    additionalCosts: [
      {
        moveCard: {
          id: "escape-exile",
          from: "graveyard",
          to: "exile",
          count: 4,
          another: true
        }
      }
    ],
    entersWithCounters: [
      { type: "+1/+1", amount: 2 }
    ]
  }
]

Generated routes use escape, escape-2, and later stable ids. They avoid ids already used by explicit alternate costs, so printed and granted Escape abilities remain separate legal choices.

Each legal action records the exact additional-cost selection. The escaping card cannot select itself, stale or duplicate selections invalidate the cast without partial payment, and all costs finish before another action window. alternateCostId: "escape" is provenance for the original spell while it is on the stack. If it resolves as a permanent, the route counters are prepared before entry and use normal counter modifiers. Normal casts, free casts, spell copies, token copies, and spells that leave the stack without resolving do not receive them.

Bargain

Bargain is a first-class keyword. Put "Bargain" in the ordinary keywords array. The engine offers the normal cast and a bargained cast that sacrifices one artifact, enchantment, or token through the shared optional additional-cost machinery. The bargained cast records additionalCostChoices.bargain, so later effects can use the generic ADDITIONAL_COST_PAID count with id "bargain".

keywords: ["Bargain"]

Buyback

Buyback is a first-class parameterized keyword. Put the complete printed additional cost in the ordinary keywords array:

keywords: [
  {
    type: "Buyback",
    cost: { mana: { generic: 2 } }
  }
]

The engine offers the normal cast and a cast that pays this optional additional cost. Buyback is not an alternate mana cost: the card's printed mana cost is paid once, the Buyback cost is added to the total cost, and ordinary spell-cost modifiers apply to that total. Non-mana components use the shared Cost shape. Multiple instances produce independent choices named buyback, buyback-2, and so on.

When any Buyback cost was paid, the engine attaches a one-shot replacement to the original spell. A successful post-resolution move to the graveyard becomes a move to hand. A countered or fizzled spell does not return, and spell copies do not pay Buyback or return to hand. Flashback and similar permissions still send the spell to exile because the Buyback replacement matches only a graveyard destination. Structured Buyback granted through GRANT is functional. Do not reproduce Buyback with authored alternateManaCosts, ALTERNATE_COST_PAID, and spell-resolution replacement triggers.

Flashback

Flashback is a first-class structured keyword. Put only the printed Flashback payment in cost; the engine supplies the graveyard cast permission and sends the spell to exile after it resolves. The internal legal action still uses alternateCostId: "flashback", but card definitions do not author that route.

Snort's complete definition:

Snort

Snort: {
  keywords: [
    {
      type: "Flashback",
      cost: { mana: { generic: 5, red: 1 } }
    }
  ],
  // ...ordinary spell effects...
}

Flashback uses the shared Cost vocabulary, not only mana. Prismatic Strands therefore records its printed tap payment directly:

keywords: [{
  type: "Flashback",
  cost: {
    tapPermanent: {
      id: "flashback-white-creature",
      filter: {
        colorIdentity: ["white"],
        controller: "SELF",
        types: ["Creature"]
      }
    }
  }
}]

When an effect says that cards gain Flashback with a cost equal to their mana cost, grant the same structured keyword with cost: "MANA_COST". This targets the cards currently in the named zone; cards entering that zone later are not included:

effects: [{
  type: "GRANT",
  kind: "keyword",
  zone: "graveyard",
  filter: { anyTypes: ["Instant", "Sorcery"] },
  keyword: { type: "Flashback", cost: "MANA_COST" },
  until: "end of turn"
}]

If a card has both printed and granted Flashback, the engine offers each distinct route. Free casts do not waive the Flashback payment because it is the chosen alternate cost, not an additional cost.

Amass

Amass is composed from CONDITIONAL, CREATE_TOKEN, PUT_COUNTER, and GRANT; it is not a dedicated effect. The conditional contains the two complete outcomes of the instruction. When no controlled Army exists, create the appropriate Army token and put counters on an Army. Otherwise, put the counters on an existing Army and add the named creature subtype if necessary:

{
  type: "CONDITIONAL",
  if: {
    count: {
      findCards: {
        zone: "battlefield",
        filter: {
          controller: "SELF",
          subtypes: ["Army"]
        }
      }
    },
    comparison: "EQUAL",
    value: 0
  },
  matched: {
    effects: [
      {
        type: "CREATE_TOKEN",
        count: 1,
        name: "Zombie Army"
      },
      {
        type: "PUT_COUNTER",
        choice: {
          id: "amassed-army",
          findCards: {
            zone: "battlefield",
            filter: {
              controller: "SELF",
              subtypes: ["Army"]
            }
          }
        },
        counter: {
          type: "+1/+1",
          amount: 1
        }
      }
    ]
  },
  rest: {
    effects: [
      {
        type: "PUT_COUNTER",
        choice: {
          id: "amassed-army",
          findCards: {
            zone: "battlefield",
            filter: {
              controller: "SELF",
              subtypes: ["Army"]
            }
          }
        },
        counter: {
          type: "+1/+1",
          amount: 1
        }
      },
      {
        type: "GRANT",
        kind: "subtype",
        target: { ref: "amassed-army" },
        mode: "add",
        subtypes: ["Zombie"],
        until: "leaves battlefield"
      }
    ]
  }
}

rest is the conditional's else branch, not a list of effects that always follows matched. Keeping counter placement inside both branches makes each branch a complete Amass outcome. The resolution-time PUT_COUNTER choice handles the rules case where the player controls multiple Armies. A newly created Zombie Army already has the Zombie subtype; an existing non-Zombie Army receives the persistent subtype grant.

The Zombie Army token template is a black 0/0 Zombie Army creature. The engine does not perform state-based actions in the middle of the resolving instruction, including while its counter-placement choice is pending, so the new token remains available to receive its counters.

Foretell

Foretell is a first-class card mechanic. Put the printed Foretell casting cost on the definition; the engine owns the fixed {2} setup action, own-turn timing, face-down exile state, next-turn delay, and persistent permission to cast for that mandatory cost:

foretellCost: { generic: 2, red: 1 } /* New API */

The engine exposes a semantic FORETELL_CARD special action when the card is in hand, it is the controller's turn, and {2} can be paid. Taking that action does not use the stack. It moves the card to exile face down, marks it as foretold, and makes it castable beginning with the next chronological turn, including an opponent's turn.

Normal spell timing still applies to the foretold card: an instant may be cast on that next opponent turn, while a sorcery waits for a legal sorcery-speed window. foretellCost is mandatory for a cast through the Foretell permission; the printed mana cost is not another option through that permission. Mandatory additional spell costs still apply. Foretold state and permission persist only while the card remains in exile. Foretell does not count as impulse draw.

Use specialActions, MOVE_CARD, and GRANT for bespoke instructions with similar movement or permissions that do not specifically name Foretell.

Plot

Plot is a first-class parameterized keyword. Put the complete printed Plot cost in the ordinary keywords array:

keywords: [
  {
    type: "Plot",
    cost: { mana: { generic: 1, red: 1 } }
  }
]

The engine generates a TAKE_SPECIAL_ACTION from hand. It is available only during the controller's first or second main phase with an empty stack, pays the shared Cost without using the stack, and exiles the card face up. Plot is not a spell cast, and spell-cost modifiers do not change its cost.

The exiled card is marked as plotted and becomes castable without paying its mana cost beginning with the next chronological turn. A plotted card may be cast only at sorcery speed even if it is an instant or has Flash. Mandatory additional costs still apply to the later free cast. Plotted state and its permission end when the card leaves exile.

Multiple structured Plot instances generate actions named plot, plot-2, and so on. Plot granted through GRANT is functional while the card is in hand. Do not reproduce Plot with authored specialActions, MOVE_CARD, and play-permission GRANT effects.

Morph

Morph is a first-class parameterized keyword. Record the printed turn-up cost using the shared Cost vocabulary:

keywords: [{
  type: "Morph", /* New API */
  cost: {
    sacrificePermanent: {
      id: "morph-sacrifice",
      another: true,
      filter: { controller: "SELF", types: ["Creature"] }
    }
  }
}]

The engine supplies the {3} alternate cast from hand. The spell and the resulting permanent have the normal face-down characteristics: a nameless, colorless 2/2 Creature with no mana cost, subtypes, keywords, or printed abilities. The cast uses the stack and does not use the face-up card's targets. If the spell is countered, its actual face is restored in the destination.

Turning a Morph permanent face up is a special action. The engine validates and pays the complete keyword cost before removing face-down state. It does not use the stack and does not make the permanent enter again. Any card-level asTurnedFaceUp effects happen immediately after payment. Choices created by those effects block other actions until the transition finishes.

Vivid

Sanar's Vivid ability is composed from a first-main trigger, a unique-color count, a collected repeated reveal, a distinct color-matched move choice, a shuffle, and temporary play permission. It is not a dedicated engine keyword:

{
  trigger: { type: "BEGIN_FIRST_MAIN" },
  effects: [
    {
      type: "REVEAL_TOP",
      from: "library",
      repeat: true,
      id: "vivid-revealed",
      untilMatchedCount: {
        type: "UNIQUE",
        attribute: "COLOR",
        zone: "battlefield",
        filter: { controller: "SELF" }
      },
      filter: { not: { types: ["Land"] } }
    },
    {
      type: "MOVE_CARD",
      source: { ref: "vivid-revealed" },
      from: "library",
      to: "exile",
      id: "vivid-exiled",
      count: {
        type: "UNIQUE",
        attribute: "COLOR",
        zone: "battlefield",
        filter: { controller: "SELF" }
      },
      choice: {
        minimum: 0,
        maximum: {
          type: "UNIQUE",
          attribute: "COLOR",
          zone: "battlefield",
          filter: { controller: "SELF" }
        },
        constraint: {
          type: "MATCH_DISTINCT_VALUES",
          attribute: "COLOR",
          values: {
            findCards: {
              zone: "battlefield",
              filter: { controller: "SELF" }
            }
          }
        }
      }
    },
    { type: "SHUFFLE_LIBRARY" },
    {
      type: "GRANT",
      kind: "play permission",
      card: { ref: "vivid-exiled" },
      zone: "exile",
      until: "end of turn"
    }
  ]
}

The color count and matching query use card color, not commander color identity. The move is optional down to zero cards. The distinct-value constraint permits one selected card per available color, including assigning a multicolored card to one otherwise-unused color. Normal mana costs and timing restrictions still apply to the granted casts.

Rebound

Rebound is modeled as explicit zone movement plus a delayed trigger, not as a keyword. The hand-cast spell moves itself from the stack to exile, captures that moved card, and creates a singular next-upkeep trigger. The trigger offers an optional free cast from exile with a fresh legal target choice.

effects: [
  // The spell's ordinary effects resolve first.
  {
    type: "MOVE_CARD",
    id: "rebound-card",
    card: "SOURCE",
    to: "exile",
    count: 1,
    condition: { sourceZone: "hand" } /* New API */
  },
  {
    type: "CREATE_DELAYED_TRIGGER", /* New API */
    source: { ref: "rebound-card", zone: "exile" },
    trigger: { type: "BEGIN_UPKEEP", player: "SELF" },
    effects: [{
      type: "CAST_SPELL",
      card: { ref: "rebound-card" },
      zone: "exile",
      free: true
    }],
    condition: { refExists: "rebound-card" }
  }
]

The sourceZone: "hand" condition is what prevents the rebound cast from exiling itself again. That cast came from exile, so it follows the spell's normal graveyard destination after resolving. If the original spell has no legal target at resolution, none of its effects resolve: it goes to the graveyard and never creates the delayed trigger.

Convoke

Every spell can be paid with mana by default. Add "Convoke" to keywords to give a spell creature-assisted payment. The engine uses a card's effective keywords—printed keywords plus any grants—to determine its payment methods. When casting a convoke spell, each untapped creature can pay one mana of one of its colours or one generic mana. The engine spends available mana before tapping creatures for convoke, and tapped creatures cannot help pay the cost. Convoke capacity is also included when the engine offers legal X values. The stack keeps the ordered identities of creatures tapped for Convoke. Only CONNIVE can consume that payment record through its narrow "CONVOKED_CREATURES" target; it is not a general card reference.

Example, Bennie Bracks, Zoologist:

Bennie Bracks, Zoologist

"Bennie Bracks, Zoologist": {
  name: "Bennie Bracks, Zoologist",
  colorIdentity: ["white"],
  types: ["Creature"],
  subtypes: ["Elf", "Druid"],
  manaCost: { generic: 3, white: 1 },
  keywords: ["Convoke"],
  power: 3,
  toughness: 2,
  roles: ["Draw", "Synergy"],
  triggeredAbilities: [
    {
      condition: { type: "TOKEN_CREATED_THIS_TURN" },
      trigger: { type: "BEGIN_END_STEP" },
      effects: [{ type: "DRAW_CARDS", count: 1 }]
    }
  ]
}

CONNIVE

CONNIVE is compatibility DSL. New definitions should normally compose draw, discard, and counter effects, but a definition may use this shape when it needs the existing connive resolver:

{
  type: "CONNIVE",
  count: EffectValue,
  target?: "EVENT_CARD" | "SOURCE" | "TARGET_PERMANENT" |
    "CONVOKED_CREATURES" /* Widened API */
}

"CONVOKED_CREATURES" resolves the creatures that paid for the current spell in their payment order. The engine snapshots contributors still on the battlefield when the spell resolves, then asks for one discard at a time. Each nonland discard places its +1/+1 counter on that same contributor. A creature that left before the spell resolves does not connive.

Class

Classes use classLevels. They begin at level 1, retain reached-level abilities, and expose only the next level's sorcery-speed upgrade. A level can define triggeredAbilities and staticAbilities. Model “when this Class becomes level N” with CLASS_LEVEL_REACHED; the level-up activation changes the level before emitting that event, so the newly active ability triggers and chooses its targets after the level-up ability resolves.

Wizard Class draws when its level-two ability triggers:

classLevels: [
  { level: 1, staticAbilities: [{ type: "NO_MAXIMUM_HAND_SIZE" }] },
  {
    id: "level-2",
    level: 2,
    cost: { mana: { generic: 2, blue: 1 } },
    triggeredAbilities: [{
      trigger: {
        type: "CLASS_LEVEL_REACHED", /* New API */
        level: 2,
        source: "SELF"
      },
      effects: [{ type: "DRAW_CARDS", amount: 2 }]
    }]
  }
]

Artist's Talent uses an optional discard and a refExists condition, so its draw occurs only if the player discarded a card:

classLevels: [
  {
    level: 1,
    triggeredAbilities: [{
      trigger: {
        type: "CAST_SPELL",
        player: "SELF",
        filter: { not: { types: ["Creature"] } }
      },
      effects: [
        {
          type: "DISCARD_CARDS",
          id: "artist-talent-discard",
          count: 1,
          choice: true,
          optional: true
        },
        {
          type: "DRAW_CARDS",
          count: 1,
          condition: { refExists: "artist-talent-discard" }
        }
      ]
    }]
  },
  {
    id: "level-2",
    level: 2,
    cost: { mana: { generic: 2, red: 1 } },
    staticAbilities: [{
      type: "COST_MODIFIER",
      appliesTo: {
        action: "CAST_SPELL",
        filter: { not: { types: ["Creature"] } }
      },
      reduction: { generic: 1 }
    }]
  }
]

Room

Rooms use doors. Each door carries its own name, mana cost, and the staticAbilities and triggeredAbilities that function only while that door is unlocked. The card is cast as either door: the engine exposes every door as a named spell under its door id, and the resolved permanent enters with only the cast door unlocked. A locked door on the battlefield generates the sorcery-speed special action unlock-<doorId>, which pays the door's mana cost without using the stack. Model "when you unlock this door" with DOOR_UNLOCKED naming that door; it fires both when the door unlocks on the battlefield and when it unlocks as the cast half enters. Doors never re-lock, and a Room that leaves the battlefield returns as a new object with every door locked.

Leave manaCost off the card. A Room's mana value is the sum of its doors outside the battlefield and the sum of its unlocked doors on it; manaValue may still record the printed combined total.

"Walk-In Closet // Forgotten Cellar": {
  name: "Walk-In Closet // Forgotten Cellar",
  types: ["Enchantment"],
  subtypes: ["Room"],
  doors: [ /* New API */
    {
      id: "walk-in-closet",
      name: "Walk-In Closet",
      manaCost: { generic: 2, green: 1 },
      staticAbilities: [{
        type: "GRANT",
        kind: "play permission",
        target: { zone: "graveyard", filter: { types: ["Land"] } }
      }]
    },
    {
      id: "forgotten-cellar",
      name: "Forgotten Cellar",
      manaCost: { generic: 3, green: 2 },
      triggeredAbilities: [{
        trigger: {
          type: "DOOR_UNLOCKED", /* New API */
          door: "forgotten-cellar",
          source: "SELF"
        },
        effects: [ /* ... */ ]
      }]
    }
  ]
}

Harness

Harness is a battlefield designation represented by harnessed: true on the permanent. HARNESS_SOURCE applies the designation idempotently, so resolving it more than once does not duplicate any abilities. The designation is cleared when the permanent leaves the battlefield.

Abilities that become active after harnessing belong in harnessedAbilities, not in a durationless GRANT. Effects such as MOVE_CARD own their target declaration so target selection and zone movement remain coupled.

activatedAbilities: [{
  id: "harness",
  cost: {
    mana: { generic: 6, black: 1 },
    tap: true,
    moveCard: {
      id: "harness-exiled-creature",
      from: "battlefield",
      to: "exile",
      count: 1,
      filter: { controller: "SELF", types: ["Creature"] }
    }
  },
  effects: [{ type: "HARNESS_SOURCE" }]
}],
harnessedAbilities: {
  triggeredAbilities: [{
    trigger: { type: "BEGIN_UPKEEP", player: "SELF" },
    effects: [{
      type: "MOVE_CARD",
      target: {
        id: "harness-graveyard-creature",
        choice: true,
        findCards: {
          zone: "graveyard",
          filter: { types: ["Creature"] }
        }
      },
      to: "battlefield",
      optional: true
    }]
  }]
}

Mentor

Mentor is composed from an ATTACKS trigger, a singular battlefield target, an attacking-state filter, and a relative-power match. FILTER_CARD is the candidate target currently being checked, while SOURCE is the creature with Mentor. Target selection belongs to the triggered ability because the target must be legal both when chosen and when the ability resolves.

Danny Pink models Mentor together with his granted counter-draw ability:

Danny Pink

"Danny Pink": {
  name: "Danny Pink",
  colorIdentity: ["blue"],
  types: ["Creature"],
  subtypes: ["Human", "Soldier", "Advisor"],
  legendary: true,
  manaCost: { generic: 3, blue: 1 },
  power: 4,
  toughness: 3,
  roles: ["Draw", "Synergy"],
  triggeredAbilities: [{
    trigger: { type: "ATTACKS", source: "SELF" },
    target: {
      zone: "battlefield",
      count: 1,
      filter: {
        types: ["Creature"],
        isAttacking: true,
        match: [{
          left: "FILTER_CARD",
          comparison: "LESS_THAN",
          right: "SOURCE",
          attribute: "POWER"
        }]
      }
    },
    effects: [{
      type: "PUT_COUNTER",
      target: "TARGET_PERMANENT",
      counter: { type: "+1/+1", amount: 1 }
    }]
  }],
  staticAbilities: [{
    type: "GRANT",
    kind: "triggered ability",
    target: {
      zones: ["battlefield"],
      filter: { types: ["Creature"], controller: "SELF" }
    },
    ability: {
      trigger: {
        type: "PUT_COUNTER",
        source: "SELF",
        matchingCountThisTurn: { count: 1 }
      },
      effects: [{ type: "DRAW_CARDS", count: 1 }]
    }
  }]
}

The target filter excludes Danny naturally: FILTER_CARD must have power less than Danny's current power. Equal- or greater-power attackers and creatures that were not declared as attackers are also illegal. If several creatures are eligible, the engine exposes a target choice to the pilot. The resulting counter placement can then trigger the ability Danny grants to that creature.

Adapt

Adapt is composed from a normal activated ability, PUT_COUNTER, and a count-backed effect condition; it does not need a dedicated engine effect. The ability may always be activated when its cost can be paid. When it resolves, it puts counters on the source only if that creature still has no +1/+1 counters.

Incubation Druid is the canonical example:

Incubation Druid

activatedAbilities: [
  {
    id: "adapt-3",
    cost: {
      mana: {
        generic: 3,
        green: 2
      }
    },
    effects: [
      {
        type: "PUT_COUNTER",
        target: "SOURCE",
        counter: {
          type: "+1/+1",
          amount: 3
        },
        condition: {
          count: {
            target: "SELF",
            counters: "+1/+1"
          },
          comparison: "EQUAL",
          value: 0
        }
      }
    ]
  }
]

The condition belongs to the resolving PUT_COUNTER effect, not to the activated ability. This distinction preserves Magic's timing: the player can activate Adapt and pay its costs even when the creature already has a counter, or another effect can add a counter while Adapt is on the stack. In either case, the ability resolves normally but the conditional counter effect does nothing.

Vanishing

Vanishing is composed from intrinsic entry counters, a conditional upkeep trigger, and a REMOVE_COUNTER trigger. The removal event's post-removal EVENT_CARD snapshot identifies the last time counter without rechecking the permanent's later counter state.

Dreamtide Whale

entersWithCounters: [
  { type: "time", amount: 2 }
],
triggeredAbilities: [
  {
    trigger: { type: "BEGIN_UPKEEP", player: "SELF" },
    condition: {
      count: { target: "SELF", counters: "time" },
      comparison: "AT_LEAST",
      value: 1
    },
    effects: [{
      type: "REMOVE_COUNTER",
      target: "SOURCE",
      counter: { type: "time", amount: 1 }
    }]
  },
  {
    trigger: {
      type: "REMOVE_COUNTER",
      source: "SELF",
      counter: "time"
    },
    condition: {
      count: { target: "EVENT_CARD", counters: "time" },
      comparison: "EQUAL",
      value: 0
    },
    effects: [{
      type: "SACRIFICE_PERMANENT",
      target: "SOURCE"
    }]
  }
]

Evolve

Evolve is represented as an ENTERS triggered ability with an intervening condition. The trigger watches another creature entering under your control, then condition.match compares that creature's current power and toughness with the evolve creature.

A match list uses any-match semantics: evolve succeeds if either the entering creature's power is greater or its toughness is greater. Because this is an intervening-if condition, the engine evaluates it when the ability would trigger and again when it resolves.

Gyre Sage is the canonical example:

Gyre Sage

"Gyre Sage": {
  name: "Gyre Sage",
  colorIdentity: ["green"],
  types: ["Creature"],
  subtypes: ["Elf", "Druid"],
  manaCost: { generic: 1, green: 1 },
  power: 1,
  toughness: 2,
  roles: ["Ramp", "Synergy"],
  triggeredAbilities: [
    {
      trigger: {
        type: "ENTERS",
        to: "battlefield",
        another: true,
        filter: {
          types: ["Creature"],
          controller: "SELF"
        }
      },
      condition: {
        match: [
          {
            left: "EVENT_CARD",
            comparison: "GREATER_THAN",
            right: "SOURCE",
            attribute: "POWER"
          },
          {
            left: "EVENT_CARD",
            comparison: "GREATER_THAN",
            right: "SOURCE",
            attribute: "TOUGHNESS"
          }
        ]
      },
      effects: [
        {
          type: "PUT_COUNTER",
          target: "SOURCE",
          counter: { type: "+1/+1", amount: 1 }
        }
      ]
    }
  ]
}

EVENT_CARD is the entering creature and SOURCE is the permanent with evolve. Power and toughness are current values, including counters and other modifiers; equality does not satisfy GREATER_THAN.

EXPLORE

Models the semantic Magic action "explore" as one engine effect. The target is the creature that explores:

{
  type: "EXPLORE",
  target:
    | "SOURCE"
    | "EVENT_CARD"
    | PermanentEffectTarget
}

The engine reveals the top library card. A land moves to hand. Otherwise, the exploring creature gets a +1/+1 counter and the pilot chooses whether to keep the revealed card on top or put it into the graveyard. If the library is empty, the creature still gets the counter and later effects continue.

The nonland choice is semantically tagged as Explore for terminal output; a generic optional library-to-graveyard MOVE_CARD is not treated as Explore. Land movement uses the normal zone machinery and is counted automatically as effect-driven card access. Definitions do not need a separate mechanic tag.

Path of Discovery

"Path of Discovery": {
  triggeredAbilities: [
    {
      trigger: {
        type: "ENTERS",
        to: "battlefield",
        filter: { types: ["Creature"] }
      },
      effects: [
        {
          type: "EXPLORE",
          target: "EVENT_CARD"
        }
      ]
    }
  ]
}

Map token

Map: {
  types: ["Artifact"],
  subtypes: ["Map"],
  activatedAbilities: [
    {
      id: "explore-creature",
      timing: "sorcery",
      cost: {
        mana: { generic: 1 },
        sacrificePermanent: { source: "SELF" },
        tap: true
      },
      effects: [
        {
          type: "EXPLORE",
          target: {
            id: "map-explore-creature",
            zone: "battlefield",
            filter: {
              controller: "SELF",
              types: ["Creature"]
            }
          }
        }
      ]
    }
  ]
}

Path consumes the entering creature from EVENT_CARD. Map declares its target directly on EXPLORE, so target enumeration and resolution use the same semantic effect regardless of whether the top card is a land.

Prepared

A permanent that enters prepared uses entersPrepared: true and embeds its prepared spell in preparedSpell. The embedded definition is the source of truth for the copied spell's name, type line, mana cost, targets, and effects; the engine does not look up a separate card definition with the same name.

"Blazing Firesinger": {
  name: "Blazing Firesinger",
  colorIdentity: ["red"],
  types: ["Creature"],
  subtypes: ["Dwarf", "Bard"],
  manaCost: { generic: 2, red: 1 },
  power: 2,
  toughness: 3,
  roles: ["Ramp", "Synergy"],
  entersPrepared: true,
  preparedSpell: {
    name: "Seething Song",
    types: ["Instant"],
    manaCost: { generic: 2, red: 1 },
    effects: [
      {
        type: "ADD_MANA",
        mana: { red: 5 }
      }
    ]
  }
}

Casting the prepared copy pays its embedded mana cost, unprepares the source permanent, and casts the copy from exile as a normal self-cast spell. The copy uses the stack, records its actual mana payment, increments spell-cast history, and satisfies matching cast triggers before resolving from its embedded definition. After resolving or failing to resolve, the copy ceases to exist instead of moving to another tracked zone.

Use a PREPARE_SOURCE effect for abilities that prepare a permanent after it is already on the battlefield. Prepared sorceries follow the same timing restrictions as ordinary sorceries.

Adventure

Adventure cards are represented as one normal card definition with an adventure face. The Adventure face supplies its own name, type line, mana cost, targets, and effects. The Adventure face should explicitly move the resolving source card to exile and mark it castable later as the non-Adventure face.

Kellan, Inquisitive Prodigy // Tail the Suspect

"Kellan, Inquisitive Prodigy": {
  name: "Kellan, Inquisitive Prodigy",
  mechanic: "Adventure",
  colorIdentity: ["blue", "green"],
  types: ["Creature"],
  subtypes: ["Human", "Faerie", "Detective"],
  legendary: true,
  manaCost: { generic: 2, green: 1, blue: 1 },
  keywords: ["Flying", "Vigilance"],
  power: 3,
  toughness: 4,
  adventure: {
    id: "tail-the-suspect",
    name: "Tail the Suspect",
    types: ["Sorcery"],
    subtypes: ["Adventure"],
    manaCost: { green: 1, blue: 1 },
    effects: [
      { type: "INVESTIGATE", count: 1 },
      { type: "ADDITIONAL_LAND_PLAY", amount: 1 },
      {
        type: "MOVE_CARD",
        card: "SOURCE",
        count: 1,
        to: "exile",
        playableAs: "NON_ADVENTURE"
      }
    ]
  }
}

Named Spells

Cards with multiple castable spell faces use a spells collection. Each entry has a stable identifier and supplies the face's printed name, type line, mana cost, and effects while the card instance keeps its combined card name in every zone.

An entry without sourceZones follows ordinary hand casting permissions. Explicit sourceZones restricts the named spell to those zones and grants its intrinsic permission there. A separate condition reuses the dynamic count-condition shape from alternateManaCosts.

{
  name: "Split Example",
  types: ["Sorcery"],
  spells: {
    front: {
      name: "Front",
      types: ["Sorcery"],
      manaCost: { generic: 1, white: 1 },
      effects: []
    },
    back: {
      name: "Back",
      types: ["Sorcery"],
      manaCost: { generic: 2, white: 1 },
      sourceZones: ["graveyard"],
      resolutionDestination: "exile",
      effects: []
    }
  }
}

MDFCs

MDFCs are represented as one combined hand entry plus standalone face entries. The combined entry and front-face entry name the other face with backFaceName. When that definition is a land, the engine offers a land play; otherwise, it offers a spell cast using the back face's own definition and mana cost. The selected face remains visible on the stack and battlefield, then handName and nonHandName restore the combined or front-face identity in other zones.

"Witch Enchanter // Witch-Blessed Meadow": {
  name: "Witch Enchanter // Witch-Blessed Meadow",
  colorIdentity: ["white"],
  handName: "Witch Enchanter // Witch-Blessed Meadow",
  nonHandName: "Witch Enchanter",
  backFaceName: "Witch-Blessed Meadow",
  types: ["Creature"],
  subtypes: ["Human", "Warlock"],
  manaCost: {
    generic: 3,
    white: 1
  },
  power: 2,
  toughness: 2,
  roles: ["Interaction"],
  triggeredAbilities: [
    {
      trigger: {
        type: "ENTERS",
        to: "battlefield",
        filter: {
          name: "Witch Enchanter"
        }
      },
      effects: [
        {
          type: "DESTROY_PERMANENT",
          target: {
            id: "artifact-or-enchantment",
            zone: "battlefield",
            optional: true,
            filter: {
              anyTypes: ["Artifact", "Enchantment"]
            }
          }
        }
      ]
    }
  ]
}

Witch Enchanter // Witch-Blessed Meadow

"Witch Enchanter": {
  name: "Witch Enchanter",
  colorIdentity: ["white"],
  handName: "Witch Enchanter // Witch-Blessed Meadow",
  nonHandName: "Witch Enchanter",
  types: ["Creature"],
  subtypes: ["Human", "Warlock"],
  manaCost: {
    generic: 3,
    white: 1
  },
  power: 2,
  toughness: 2,
  roles: ["Interaction"],
  triggeredAbilities: [
    {
      trigger: {
        type: "ENTERS",
        to: "battlefield",
        filter: {
          name: "Witch Enchanter"
        }
      },
      effects: [
        {
          type: "DESTROY_PERMANENT",
          target: {
            id: "artifact-or-enchantment",
            zone: "battlefield",
            optional: true,
            filter: {
              anyTypes: ["Artifact", "Enchantment"]
            }
          }
        }
      ]
    }
  ]
}

Witch-Blessed Meadow

"Witch-Blessed Meadow": {
  name: "Witch-Blessed Meadow",
  colorIdentity: ["white"],
  handName: "Witch Enchanter // Witch-Blessed Meadow",
  nonHandName: "Witch Enchanter",
  types: ["Land"],
  entersBattlefieldTapped: true,
  manaProduction: {
    white: 1
  },
  triggeredAbilities: [
    {
      trigger: {
        type: "ENTERS",
        to: "battlefield",
        filter: {
          name: "Witch-Blessed Meadow"
        }
      },
      effects: [
        {
          type: "PAY_COST",
          optional: true,
          cost: {
            loseLife: { amount: 3 }
          },
          effects: [
            {
              type: "UNTAP_PERMANENT",
              target: "SELF"
            }
          ]
        }
      ]
    }
  ]
}

Kazuul's Fury // Kazuul's Cliffs

"Kazuul's Fury // Kazuul's Cliffs": {
  name: "Kazuul's Fury // Kazuul's Cliffs",
  colorIdentity: ["red"],
  handName: "Kazuul's Fury // Kazuul's Cliffs",
  nonHandName: "Kazuul's Fury",
  backFaceName: "Kazuul's Cliffs",
  types: ["Instant"],
  manaCost: {
    generic: 2,
    red: 1
  },
  additionalCosts: [
    {
      sacrificePermanent: {
        id: "sacrificed-creature",
        filter: { types: ["Creature"] }
      }
    }
  ],
  roles: ["Interaction", "Synergy"],
  effects: [
    {
      type: "DEAL_DAMAGE",
      amount: {
        type: "SUM",
        attribute: "POWER",
        source: { ref: "sacrificed-creature" }
      },
      target: {
        type: "creature_or_player",
        controller: "any",
        player: "any"
      }
    }
  ],
  unsupported: ["Planeswalker targeting is not modeled."]
}

Kazuul's Fury

"Kazuul's Fury": {
  name: "Kazuul's Fury",
  colorIdentity: ["red"],
  handName: "Kazuul's Fury // Kazuul's Cliffs",
  types: ["Instant"],
  manaCost: {
    generic: 2,
    red: 1
  },
  additionalCosts: [
    {
      sacrificePermanent: {
        id: "sacrificed-creature",
        filter: { types: ["Creature"] }
      }
    }
  ],
  roles: ["Interaction", "Synergy"],
  effects: [
    {
      type: "DEAL_DAMAGE",
      amount: {
        type: "SUM",
        attribute: "POWER",
        source: { ref: "sacrificed-creature" }
      },
      target: {
        type: "creature_or_player",
        controller: "any",
        player: "any"
      }
    }
  ],
  unsupported: ["Planeswalker targeting is not modeled."]
}

Kazuul's Cliffs

"Kazuul's Cliffs": {
  name: "Kazuul's Cliffs",
  colorIdentity: ["red"],
  handName: "Kazuul's Fury // Kazuul's Cliffs",
  nonHandName: "Kazuul's Fury",
  types: ["Land"],
  entersBattlefieldTapped: true,
  manaProduction: {
    red: 1
  }
}

Blitz

Blitz is a first-class structured keyword. Record its printed cost and let the engine supply the mechanic's alternate-cast route, haste, delayed sacrifice, and draw-on-death behavior.

The cost is a full Cost, so printed non-mana components belong beside mana. Use "MANA_COST" when an effect grants Blitz for the card's mana cost. sourceZones defaults to the normal hand casting route. When an effect adds another route, list every permitted zone, for example sourceZones: ["hand", "graveyard"].

Jaxis, the Troublemaker

"Jaxis, the Troublemaker": {
  keywords: [
    {
      type: "Blitz",
      cost: {
        mana: {
          generic: 1,
          red: 1
        }
      }
    }
  ]
}

The generated cast route uses alternateCostId: "blitz" internally. Card definitions should not repeat the generated alternate cost or its three abilities.

Dash

Dash is a first-class structured keyword. Record the complete printed cost in the shared Cost shape:

keywords: [
  {
    type: "Dash",
    cost: {
      mana: {
        generic: 1,
        black: 1
      }
    }
  }
]

The engine keeps the normal cast route and adds a hand-only alternate route. The first Dash ability uses alternateCostId: "dash"; later printed or granted instances use dash-2, dash-3, and so on. The cost accepts the full Cost vocabulary, so any non-mana payment is authored beside mana and becomes an additional casting cost.

When a spell cast through a Dash route resolves as a permanent, it gains haste until end of turn. The engine also creates a singular delayed trigger for the next beginning of an end step. That trigger captures the exact battlefield object and returns it to its owner's hand. It remains functional if the permanent later loses its abilities. If the permanent leaves before the end step, or leaves after the trigger is queued and returns before it resolves, the new object is not returned by the stale trigger.

A normal cast does not gain haste or create the delayed return. A Dash spell that is countered never enters, so it creates neither effect. Structured Dash granted through the ordinary keyword GRANT is functional. Card definitions should not repeat Dash with authored alternateManaCosts, an ENTERS recipe, or a granted end-step ability.

Warp

Warp is a first-class structured keyword. Record its complete printed cost and let the engine supply its alternate cast route, delayed exile, and later cast permission:

keywords: [
  {
    type: "Warp",
    cost: {
      mana: {
        generic: 1,
        white: 1
      }
    }
  }
]

The cost uses the shared Cost vocabulary, so printed non-mana components sit beside mana. sourceZones defaults to ["hand"]. List every printed zone for an exception such as a card that may Warp from its graveyard:

{
  type: "Warp",
  cost: {
    mana: { black: 1 },
    loseLife: { amount: 2 }
  },
  sourceZones: ["hand", "graveyard"]
}

The generated route uses alternateCostId: "warp". When the warped permanent enters, the engine creates a one-shot delayed trigger for the next end step. If that same permanent is still on the battlefield, it moves to exile and becomes castable beginning on a later turn for its normal mana cost. Ordinary card-type timing and mandatory additional costs still apply to the later cast, and Warp is not offered from exile unless its own sourceZones explicitly say so. A permanent that leaves and returns before the delayed trigger resolves is a new object and is not exiled by the old Warp trigger.

Structured Warp granted through the ordinary keyword GRANT is functional. Card definitions should not repeat Warp as an authored alternateManaCosts, ENTERS, CREATE_DELAYED_TRIGGER, MOVE_CARD, and play-permission sequence.