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
Interactioncard. 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 }
]
}

Important fields:
types,subtypes,keywords,power, andtoughnessdescribe 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. Usecolorsonly for a printed color indicator or another characteristic that overrides that derivation.colorIdentityis commander-deck metadata and does not make a card colored; a land can have a colored identity while remaining colorless. powerandtoughnessaccept either a fixed number or anEffectAmountfor a characteristic-defining value that is recalculated in every zone. Dynamic printed stats resolve before base-stat settings, counters, and continuous modifiers.manaCost,alternateManaCosts, andmanaCostReductiondescribe casting costs. Ordinary mana payment is implicit.additionalCostsaccepts ordinaryCostentries pluschooseOneclauses. Ordinary entries are all mandatory; achooseOneclause requires exactly one named cast-time payment option.manaAbilitiesis the canonical API for non-stack mana abilities.asEntersis the canonical API for choices and copy modifications described by Oracle text as a permanent enters.asTurnedFaceUpcontains immediate transition effects performed while a permanent is turning face up. These effects are not triggered abilities and do not use the stack.entersWithCountersdescribes counters the permanent's own rules give it as it enters, including X-derived amounts.manaProductionis deprecated syntactic sugar for simple tap mana abilities.effectsis for spell resolution.pregameAbilitiescontains effects a card may apply from the kept opening hand before the game begins. Each effect resolves once with that card asSOURCE; choosingBEGIN_GAMEdeclines any remaining pregame abilities.activatedAbilitiesis for abilities with costs.triggeredAbilitiesis for event-driven abilities.staticAbilitiesis for continuous battlefield state.rolesis used by the pilot and metrics.assumptionsmakes any goldfish simulation policy explicit and separate from the card's rules effects.unsupportedrecords 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:
- Resolve every
asEnterschoice for every permanent in the entering batch. - Apply intrinsic entry state such as
entersWithCounters, loyalty, tapped state, and summoning sickness. - Put the complete batch onto the battlefield simultaneously.
- Emit counter-placement events, then
ENTERSevents.
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. EffectCountqueries that count or aggregate game objects and history.ContextValueexpressions 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_TYPEfor the different card types among matching cards. Multi-type cards contribute each type. TurnZero's in-scope set isArtifact,Battle,Creature,Enchantment,Instant,Kindred,Land,Planeswalker, andSorcery; supertypes and subtypes do not contribute.COLORfor the different Magic colors among matching cards. Colorless does not contribute a value, and a multicolored card contributes each of its colors.POWERfor current power among matching cards or permanents.MANA_VALUEfor different mana values among matching cards.COUNTERSfor 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:
targetdeclares a rules target or consumes an explicit reference.permanents.findCardsselects a non-targeted permanent set for an effect.count.findCardsreturns a number for anEffectValue.to.findCardsselects 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.choicessupplies 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 betweenmatchedandrest.sacrificeOutletFormatches 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.graveyardCardsupplies 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'sidso later effects can inspect it.opponent.handSizesupplies the hand size of an opponent a card refers to, such as Borrowed Knowledge's target opponent.opponent.weightedCountssupplies 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.permanentGroupsdescribes possible opposing permanents for an effect such asGAIN_CONTROL. The effect names its group withassumptionId; the engine rolls and caches one candidate per source instance and group ID.triggeredAbilityOccurrencessamples one weighted occurrence count for the named ability per full turn cycle.OPPONENT_COMBATqueues 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:

{
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:

{
type: "MOVE_CARD",
from: "hand",
to: "battlefield",
count: 1,
choice: true,
optional: true,
filter: { types: ["Land"] }
}
Notes:
library_topandlibrary_bottomare destination aliases, not long-lived zones.choice: truechooses exactlycountcards. The object form chooses fromminimumthroughmaximum; omitmaximumto make every current candidate eligible. Setorder: truewhen 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: truemoves every matching candidate. Withorder: true, it presents a staged destination-order choice when two or more cards remain.optional: trueexposes 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:

{
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:
idrecords 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. targetandpermanentsare mutually exclusive. Apermanents.findCardsrecipient 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: falsebypasses prevention. The engine records attempted, prevented, and actual damage separately; only actual damage emitsDAMAGE_DEALT, changes life or loyalty, marks damage, or contributes to lifelink. optional: trueis 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 whencount.minis zero. - Every counted-damage target must be distinct. When
divisionis omitted, the fullamountis 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:

{
type: "DESTROY_PERMANENT",
target: {
id: "creature-to-destroy",
zone: "battlefield",
filter: { types: ["Creature"] }
}
}
Example, 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,
idstores 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 withtype: "SUM"andsource: { ref: id }. SELFcan be used for source self-destruction where supported by the target type.targetandpermanentsare mutually exclusive.permanents.findCardsdestroys 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.idis present, the reference stores last-known snapshots of only the permanents actually destroyed. Later effects can aggregate that group withtype: "COUNT"ortype: "SUM"andsource: { 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: truealso 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
countaccepts 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.
targetandpermanentsare mutually exclusive.exemptdeclares 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.findCardssnapshots every matching tracked permanent and exiles that group simultaneously. When the query has anid, the stored reference contains only the permanents actually exiled after applyingexempt.- 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_PERMANENTremains as a compatibility effect for existing definitions. New definitions should useEXILE_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:

{
trigger: {
type: "ENTERS",
from: "battlefield",
to: "graveyard",
source: "SELF"
},
effects: [
{ type: "DRAW_CARDS", amount: 1 }
]
}
Example, 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:

{
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:

{
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_COSTinstead of trigger-level optional costs for new definitions. optional: trueexposes both pay and decline choices.player: "OPPONENT"is limited to an optionalloseLifecost and requiresopponentChoiceId. It resolves through the namedassumptions.opponent.choicesanswer,PAYorDECLINE, 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 explicitDECLINEassumption is the default goldfish answer unless an injected policy overrides it. Opponent life totals are not tracked, soPAYemits a normal opponentLOSE_LIFEevent 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
costis treated as an empty cost. Withoptional: true, the engine still exposes both accept and decline choices; withoutoptional, the nested effects resolve immediately. cost.xenumerates payablexValuechoices and makes{ variable: "X" }available to nested effects. A namedrefalso 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
targetorpermanents. 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
countrequires exactly that many distinct targets. The ranged form allows every distinct target count fromminthroughmax. 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_PERMANENTandBECOMES_TAPPEDexactly 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:

{
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"] } }targetandpermanentsare mutually exclusive.choicemakes a resolution-time, non-target choice among matching battlefield permanents. It is mutually exclusive withtargetandpermanents.permanents.findCardsuntaps every matching tapped permanent without target selection. Optionalcountlimits 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
skipsUntapStepflag 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_INDSL 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:

[
{
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_TOPwhen Oracle says reveal. LOOK_AT_LIBRARYmust 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
idfrom later movement assource: { ref: id }. - Optional looks defer following effects. Declining leaves no referenced cards, so subsequent moves from that reference do nothing.
LOOK_AT_LIBRARYmay also be used as astaticAbilitiesentry 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:

{
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:
amountdefaults to1. The single-reference shape stores one card object whenamountis 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: trueoffers accept and decline actions before revealing any cards. Declining leaves the library unchanged and resumes later effects.rest.randomOrder: truerandomizes, 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:

{
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.
idstores the complete created-token array for later{ ref: id }effects.optional: trueoffers 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.createdByqueries. This also holds for named replacement outputs, investigate, token copies, Afterlife, Offspring, and legacy token creation paths. playerdefaults toSELF. 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 battlefieldENTERSevent 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: truecreates 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 itsENTERSevent, but it was not declared as an attacker and does not emitATTACKorATTACKS.- Use
count: { source: "OPPONENT_COUNT" }withattacking.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:

{
type: "COPY",
resultId: "jaxis-copy",
target: {
id: "creature-to-copy",
zone: "battlefield",
controller: "self",
excludeSource: true,
filter: { types: ["Creature"] }
}
}
Notes:
countdefaults 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.resultIdlets later effects target the created token with{ ref: "jaxis-copy" }.applyTo: "SOURCE", mode: "BECOME_COPY"makes the resolving battlefield object copy a selected permanent. Withuntil: "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.resultIdrecords 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.effectsrun against that token after its copied characteristics are established but before it enters the battlefield. UseGRANT kind: "characteristics"for fieldwise copy exceptions and keep eachSET,ADD, orREMOVEoperation 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.effectsmechanism 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 emitATTACKorATTACKSor 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.refreads the stored card group instead of searching a zone. One referenced member is selected throughCHOOSE_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
untilfrom a battlefield keywordGRANTcreates 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: nullmeans the copy has no mana cost and therefore has mana value zero. Omitting an override retains the copied characteristic.manaValueoverrides the value derived frommanaCost. 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 andtypes: ["ACTIVATED", "TRIGGERED"]for abilities.sourceapplies the ordinaryCardFiltervocabulary to the spell card or ability source.anyOfcomposes alternatives without introducing a card-specific copy effect. - Stack copies retain the original object's targets by default. Set
mayChooseNewTargets: trueonly 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_SPELLandCOPY_ABILITYremain supported for existing definitions, but new effects that can copy more than one stack-object kind should useCOPYwith 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:

{
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:

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:

{
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:

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:

{
type: "GRANT",
kind: "keyword",
target: "SELF",
until: "end of turn",
keyword: "Haste"
}
Type example, 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_ABILITYandGRANT_KEYWORDstill exist as compatibility aliases in current definitions.- Prefer
GRANTfor 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 inmanaColorsabsent 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
untilon static grants; the source remaining in a declaredsourceZoneszone 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"orzone: "graveyard". Exile useschoice: { count: 1 }for a referenced card group; graveyard useschoice: truewith a filter. Static play permission supports dynamictarget: { zone: "library", position: "top" }andtarget: { zone: "graveyard", filter: CardFilter }shapes. - Filtered graveyard permission without
usesapplies to cards matching when the effect resolves; cards that enter the graveyard later are not included. A limitedusespermission 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
whilecondition 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:

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.sourceZonesdefaults to battlefield. A hand-zone ability can usediscardCard.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.xexposes one legal activation for every payable value frommin(default zero) through the resolvedmax. Whenmaxis 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" }. Whenrefis 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:

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
manaAbilitiesfor new mana-source definitions.manaProductionremains supported as deprecated sugar for simple{ tap: true }producers.Mana abilities are not exposed as normal
ACTIVATE_ABILITYlegal actions.Cost primitives resolve immediately when the payment engine uses the mana ability. Selectable
moveCardcosts 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, andmoveCardfor text before the colon.cost.effectsremains available for legacy effect-shaped costs that do not yet have a primitive cost field.SACRIFICE_PERMANENTwithtarget: "SELF"is supported incost.effectsfor mana abilities that sacrifice their own source.REMOVE_COUNTERwithtarget: "SOURCE"is supported incost.effects. Availability requires the full resolved amount, and payment emits the normal removal event.Top-level
effectsresolve 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 inCREATE_REFLEXIVE_TRIGGERso 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
constraintfilter is checked against the spell being paid for. Use the structuredanyOfform 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, andpayment: "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 dedicatedpayment: "CLASS_LEVEL"alternative. A mana ability's own activation cost is a separate payment path and matches neither.conditionuses 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
bonusresolves only when that mana ability is activated as part of paying for a spell.appliesTocan narrow the bonus independently ofconstraint; 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:

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
ENTERSwithto: "battlefield"andsource: "SELF"for "when this permanent enters". - Use
ENTERSwithfrom: "battlefield",to: "graveyard", andsource: "SELF"for "when this creature dies". - Use
ENTERS.idwhen the effects need the event card as a reusable card ref. - Use
ENTERS.turnContextfor zone-change text limited to your turn or an opponent's turn. - Trigger conditions such as
ALTERNATE_COST_PAIDlive beside the trigger, not inside the effect list. - Saga timing abilities put a lore counter on themselves. Chapters use
PUT_COUNTERtriggered abilities with exact lore-counter conditions. An exact array such asamount: [1, 2]shares one chapter body across discrete totals; arrays requirecomparison: "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_TAPPEDevent and retains the existing singleTAPPED_FOR_MANAmana-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.sourceZonerecords 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 originatingsourceSpelland returns zero when no recorded payment exists. manaSpentFrom.filtermatches 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.requiredManaSourceIdis 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.
fromis the zone the card moved from;tois the zone where it finished.- Omit
fromon anENTERStrigger when the origin does not matter. - Use
MOVE_CARDwithfrom: "battlefield"for a permanent leaving the battlefield, and omittowhen the destination does not matter. - Set
another: trueonMOVE_CARDto exclude the triggered ability's source. - ETB is
ENTERSwithto: "battlefield". - Death is
ENTERSwithfrom: "battlefield"andto: "graveyard". - Mill is
ENTERSwithfrom: "library"andto: "graveyard". - A discard that actually reaches the graveyard also emits
ENTERSwithfrom: "hand"andto: "graveyard"; the semantic discard event isDISCARDED, 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
ENTERSevent even though they are not stored in the graveyard array; see Permanent Death Triggers. CARD_PUT_INTO_GRAVEYARDandENTER_BATTLEFIELDare not public authoring primitives.CREATURE_DIEDis legacy compatibility only; new card definitions should useENTERSplus 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:
playercan beSELF,OPPONENT, orEACH. An omitted player defaults toSELF.- Existing
untapsEachUntapStepdefinitions 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:
playercan beSELF,OPPONENT, orEACHwhere a card cares about whose upkeep it is. An omitted player defaults toSELF.
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, andplayer: "EACH"means every end step. Omittingplayerretains 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:
playercan beSELF,OPPONENT, orEACH. An omitted player defaults toSELF.- 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_CARDfrom 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": {
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": {
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: {
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": {
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": {
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:

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.

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": {
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": {
triggeredAbilities: [
{
trigger: {
type: "ENTERS",
to: "battlefield",
filter: { types: ["Creature"] }
},
effects: [
{
type: "EXPLORE",
target: "EVENT_CARD"
}
]
}
]
}

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": {
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": {
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": {
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": {
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": {
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": {
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": {
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.