Properties & states
A property (or constraint) is an assertion about the game, checked at a specific moment: “at the end of the precombat main phase of turn 3, the commander is in play and at least 4 lands are on the battlefield.” The simulator measures how often your deck can make all of your properties true at once.
Anatomy of a property
Each property has four parts:
| Part | Values | Meaning |
|---|---|---|
| Timing | “at the end of” · “before” | When the condition is evaluated (see Timing semantics below). |
| Phase | untap, upkeep, draw, precombat main, …, end step | The phase of the trigger moment. |
| Turn | 1, 2, 3… | The turn of the trigger moment. |
| Condition | plain English | Compiled to a Python check(state) function you can review. |
What is a “state”?
A state is a full photograph of the game at one instant on one line of play: all zones (library, hand, battlefield, graveyard, exile, command zone, stack), whose turn and phase it is, life totals, the mana pool, per-turn tallies (spells cast, lands played, cards drawn…), and per-turn history (what entered play or hit the graveyard on every earlier turn).
The search creates a new state for every decision it explores — the same turn-3 moment exists in many states, one per line of play that reaches it. Your condition is evaluated against each state reached at the property's trigger moment; the game succeeds if one single line makes all properties true.
Conditions are evaluated at checkpoints: on entering every phase, and after every individual action at a priority window — a main phase, an instant-speed window, or the attack step (so “4 spells cast this turn” can be caught between two casts of the same phase).
Timing semantics: “at the end of” vs “before”
- “at the end of” phase P of turn N — checked at every checkpoint where the game is exactly at that phase of that turn (including after each action of a main phase). It reads as “at the end of” because the last such checkpoint is usually the one that matters.
- “before” phase P of turn N — checked at every checkpoint strictly earlier than that moment: any earlier phase, any earlier turn. Use it for “this happens at some point in the first N turns”.
Two more rules complete the picture:
- Sticky satisfaction. Once a property holds at one of its checkpoints on a line, it stays satisfied on that line — it does not need to remain true afterwards.
- Viability pruning. A line is abandoned as soon as an unsatisfied property can no longer be verified on it (its “at the end of” moment is past, or its “before” deadline reached). The search tree viewer shows exactly where each dead line died, and why, with one status circle per property.
Writing conditions
Write the condition in plain English, then hit Compile → review code. The selected
LLM turns it into a Python function check(state), displayed under the property so
you can verify it matches your intent before running. (Without an API key or local model, an
offline stub compiles simple numeric conditions with regex heuristics.)
The compiler is given your deck's card list, so it resolves approximate card names
to the exact full name — write “Nick Fury” and it targets
"Nick Fury, Agent of S.H.I.E.L.D.", or “Bolt” → "Lightning Bolt".
It also reports a confidence next to Generated code
(high
medium
low) and, when the wording is
ambiguous or names a card that isn't in the deck, a short note saying what extra detail
it needs. If confidence is low, rephrase and recompile.
The code box is editable: you can hand-tune the generated function (or fix a low-confidence translation) directly. Once you edit it, the label switches from Generated code (with the model's confidence) to Manual code, marked valid or invalid. Clicking Run does not recompile — it uses the code as-is and only checks that every property has valid, runnable code, refusing to start (with a warning) if any property has no code or code that doesn't compile.
The generated code runs in a restricted sandbox against a documented, stable
state API:
Counts and booleans
state.commander_in_play() -> bool # a commander is on the battlefield
state.lands_in_play() -> int
state.creatures_in_play() -> int
state.permanents_in_play() -> int
state.cards_in_hand() -> int
state.cards_in_graveyard() -> int
state.has_permanent_named(name: str) -> bool # case-insensitive
state.count_on_battlefield(pred) -> int # pred: fn(card_data) -> bool
Creature stats (on the battlefield)
state.total_power() -> int
state.total_toughness() -> int
state.max_power() -> int
state.max_toughness() -> int
state.creatures_with_power_at_least(n: int) -> int
Per-turn tallies (reset each turn)
state.spells_cast_this_turn -> int
state.creature_spells_cast_this_turn -> int
state.noncreature_spells_cast_this_turn -> int
state.lands_played_this_turn -> int
state.cards_drawn_this_turn -> int
Game-long
state.cards_drawn -> int # total cards drawn this game
state.storm_count -> int # accumulates across the game
state.life -> int
state.turn -> int
Zones (lists of names)
state.hand_names() -> list[str]
state.battlefield_names() -> list[str]
state.graveyard_names() -> list[str]
What happened during the game (event history)
Every game records an event log: each spell cast, land played, permanent that entered or left the battlefield, card drawn, ability activated, and each spell/trigger that resolved — tagged with the turn and with what caused it. This lets a property test anything that happened at any earlier moment, not just the current board:
state.played_on(name, turn=None, min_turn=None, max_turn=None) -> bool # you cast a spell OR played a land
state.cast_on(name, turn=None, min_turn=None, max_turn=None) -> bool # a spell was cast (even if later countered)
state.entered_battlefield(name, turn=None, min_turn=None, max_turn=None, # a permanent entered play, however it got there
token=None, via_kind=None) -> bool
state.spell_resolved(name, turn=None) -> bool # cast AND resolved (not countered)
state.permanents_put_by(source, via_kind=None, land=None, creature=None,
token=None, turn=None) -> int # permanents a spell/ability of `source` put into play
state.cards_put_by(source, via_kind=None, turn=None) -> list[card] # the card objects it put into play
state.cards_drawn_by(source, turn=None) -> int # cards drawn because `source` resolved
state.ability_activated(source, turn=None) -> bool # an activated ability of `source` was activated
state.ability_succeeded(source, turn=None) -> bool # …and it achieved its purpose (found/put something)
state.trigger_resolved(source, turn=None) -> bool # a triggered ability of `source` resolved
state.commander_name() -> str # the deck's commander (stable name)
state.count_events(kind=None, name=None, via=None, via_kind=None, turn=None, pred=None) -> int
state.events_matching(...same filters...) -> list[dict] # the raw events, to test anything else
via_kind narrows the cause of an effect:
"spell" · "land_drop" · "triggered" · "activated".
Event kinds include cast, play_land,
enter_battlefield/leave_battlefield, draw,
activated, trigger_resolved, spell_resolved.
Past states / per-turn history
These describe what happened on earlier turns of the same line — the state carries its own history:
state.turns_played() -> int # current turn number
state.graveyard_added_on(turn: int) -> list[str] # cards put in GY that turn
state.permanents_entered_on(turn: int) -> list[str]
state.creatures_entered_on(turn: int) -> list[str]
state.lands_entered_on(turn: int) -> list[str]
state.each_turn(pred) -> bool # pred(turn)->bool holds for EVERY turn 1..now
state.some_turn(pred) -> bool # … for at least one turn
Cards exposed to count_on_battlefield's predicate have:
.name, .cmc, .type_line, .is_land,
.is_creature, .colors (a list like ["R"]).
Worked examples
“The commander is in play and at least 4 lands on the battlefield”
def check(state):
return state.commander_in_play() and state.lands_in_play() >= 4
“4 non-creature spells have been cast this turn”
def check(state):
return state.noncreature_spells_cast_this_turn >= 4
“Urza's Saga is on the battlefield and there are at least 8 cards in the graveyard”
def check(state):
return state.has_permanent_named("Urza's Saga") and state.cards_in_graveyard() >= 8
“A card was put in the graveyard on each turn since the beginning”
def check(state):
return state.each_turn(lambda t: len(state.graveyard_added_on(t)) >= 1)
“At least two creatures with power 4 or more”
def check(state):
return state.creatures_with_power_at_least(4) >= 2
“At least 3 artifacts on the battlefield”
def check(state):
return state.count_on_battlefield(lambda c: "artifact" in c.type_line.lower()) >= 3
“Nick Fury was cast by turn 4” (the action — not just present in play)
def check(state):
return state.cast_on("Nick Fury, Agent of S.H.I.E.L.D.", max_turn=4)
“The commander’s triggered ability put at least 2 lands into play”
def check(state):
return state.permanents_put_by(state.commander_name(), via_kind="triggered", land=True) >= 2
“A land entered the battlefield on turn 1 without being played” (a fetch)
def check(state):
# entered via a triggered/activated fetch, not a land drop
return state.count_events(kind="enter_battlefield", turn=1,
pred=lambda e: e.get("is_land") and e["via_kind"] != "land_drop") >= 1
Tips & pitfalls
- Pick the right timing. “I can have it by turn 3” is usually
before end step of turn 3(any moment counts), not “at the end of” — with “at the end of”, the condition must hold at that exact phase. - Approximate card names are fine. The compiler resolves them against your deck (“Nick Fury” → the full name), so you rarely need to type the exact string. If a name is ambiguous or not in the deck it lowers confidence and says so — check the note.
- Each property is checked independently. Two properties don't need to hold at the same instant — they each have their own trigger moment; the game succeeds if one line satisfies all of them at their respective moments.
- Review the generated code. The English → code step is an LLM; the code shown under the property is what actually runs. If it doesn't match your intent, rephrase and recompile.
- Mind the search budget. Late trigger moments (turn 5+) explode the tree. Raise the per-game timeout, lower the number of games, or use properties on earlier turns to keep runs fast.
- Errors count as false. If the compiled code raises at a checkpoint, the property simply doesn't hold there — the run continues.