GitHub ↗

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:

PartValuesMeaning
Timing“at the end of” · “before”When the condition is evaluated (see Timing semantics below).
Phaseuntap, upkeep, draw, precombat main, …, end stepThe phase of the trigger moment.
Turn1, 2, 3…The turn of the trigger moment.
Conditionplain EnglishCompiled 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”

T1 mainT1 end T2 mainT2 end T3 upkeepT3 main T3 end “before precombat main of turn 3” — every checkpoint in here “at the end of” — exactly here

Two more rules complete the picture:

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