GitHub β†—

The search

How one run turns a deck into a success rate β€” by exhaustively exploring each game's decision tree.

How a simulation runs

A run plays N independent games. Each game is one random shuffle of the deck (seeded, so runs are reproducible). For each game the engine then searches the whole decision tree:

  1. Mulligans. If you allow mulligans, every way of choosing which cards to bottom from the opening 7 becomes a separate root of the search.
  2. Lines of play. At every point the player has priority, every legal action is a branch: in a main phase, playing a distinct land (with its enter modes β€” a shockland can enter tapped or cost 2 life), casting a distinct affordable spell, activating an ability, or passing. In the other steps (upkeep, combat sub-steps, end step) an instant-speed window opens whenever an instant, a flash spell, or an instant-speed ability is actually available β€” so lines like cracking a fetchland on the end step, or holding up a counterspell, are explored too. Fetch targets, tutor targets, discards, the value chosen for an {X} spell, modal choices and attack declarations branch as well.
  3. Mana is not a branch point. Tapping sources to pay a cost is solved deterministically by a payment planner, keeping the tree focused on genuinely meaningful decisions.
  4. Checkpoints. The game state is checked against your properties at every phase entry and after every action at a priority window.

A game is a success if some single line satisfies all properties at their respective trigger moments. Independently, per-property statistics count the games where each property was satisfiable in any line.

keep hand7 cards play Forest play Bayou pass cast Sol Ring fetch β†’ Taiga cast commander pass attack βœ“ all properties satisfied βœ— property no longer verifiable β†’ pruned
Every state created during the search is recorded; the UI can open the full explored tree of any game, with the winning line highlighted in gold.

Pruning & bounds

Search strategies

All strategies are exhaustive β€” they visit the same states, only the order differs, so winning lines are found sooner and timeouts bite later:

ModeOrder
best_first default Greedy best-first on a board-progress score (satisfied properties, then permanents in play, cards seen, turn) β€” heads for the most developed states first.
bfsBreadth-first β€” shallowest lines first.

Parallel tree exploration

Games run one at a time, in order β€” but within each game the search tree is explored across CPU cores (one worker process per core, keeping one core free for the UI). The trick is that no game state ever crosses a process boundary: everything about a game derives deterministically from its seed, so the master process expands the tree just deep enough to find a level with enough subtrees, and each worker rebuilds the same game from the seed, replays that same shallow expansion, and exhaustively explores only its share of the subtrees. The pieces are then grafted back into one tree β€” identical, state for state, to what a sequential search would have produced. The first worker to satisfy every property makes the others stop, and stopping a run reaches into every worker within a fraction of a second.

Fake shuffling

Distinct lines of play can shuffle the library differently (a fetch land cracked at different moments, say) β€” then each line effectively gets a fresh top of library, which can over-evaluate β€œfind X” probabilities. The Fake shuffling option (in the Simulation box) keeps the library near-constant instead: a β€œshuffle” never reorders it, except that the cards whose position the player actually knows β€” put on top by a Brainstorm or a tutor, bottomed by a mulligan or a scry β€” are pulled out and reinserted at random places (after a real shuffle you wouldn't know where they went). Enabled by default; disable it to get real seeded shuffles.

Crash-safe runs & resume

A run's entry is persisted after every completed game, so if the app dies mid-run the games already searched are kept β€” the run shows as interrupted in β€œPrevious runs”. Loading a stopped or interrupted run offers a Resume button next to Run/Stop: exactly the games that never completed are (re)run with their original seeds and settings, appending to the same entry.

The stack β€” how casting, activating and triggering are modelled step by step β€” is covered on the Architecture page.