Titan Breach Simulator
./driver.py N DECKNAME [DECKNAMES...]
N is the number of trials you want to conduct and
DECKNAME is the name of the deck list you want to use. Deck lists are defined in a dictionary near the top of
[ 26-baseline 0] D 3 B 0 k states / 0.05 s = 6 k states/s  initial  drawing 7 drawing: Mountain ; Bolt ; Breach ; Search ; Bolt ; Forest ; Breach  passing turn TURN 1 drawing: Forest HAND: Mountain ; Bolt ; Breach ; Search ; Bolt ; Forest ; Breach ; Forest  playing Forest  suspending Search  passing turn HAND: Mountain ; Bolt ; Breach ; Bolt ; Breach ; Forest BOARD: ((Forest)) ; (Forest) TURN 2 drawing: Elder HAND: Mountain ; Bolt ; Breach ; Bolt ; Breach ; Forest ; Elder BOARD: (Forest) ; Forest  playing Mountain  casting and cracking Elder  passing turn HAND: Bolt ; Breach ; Bolt ; Breach ; Forest BOARD: (Forest) ; (Forest) ; (Forest) ; (Mountain) TURN 3 drawing: Pact HAND: Bolt ; Breach ; Bolt ; Breach ; Forest ; Pact BOARD: Forest ; Forest ; Forest ; Mountain  playing Forest  casting Breach with Pact 16401 PLAY; 16371 DRAW turn : play ; draw ; mean 1 : 0.0% ± 0.0% ; 0.0% ± 0.0% ; 0.0% ± 0.0% 2 : 0.1% ± 0.0% ; 0.3% ± 0.0% ; 0.2% ± 0.0% 3 : 25.0% ± 0.4% ; 47.6% ± 0.5% ; 36.2% ± 0.3% 4 : 84.9% ± 0.7% ; 94.6% ± 0.8% ; 89.7% ± 0.5%
The functions of all the cards in the deck have been coded up in
manager.py, as have several plausible additions. If you want to simulate anything else, you'll have to code up the card's effects yourself. This has three steps. For example, to add a cantrip:
cast_cantripmethods for the
GameStateobject. The first makes sure the operation makes sense (for example, that the cantrip is in our hand and we can afford it). If it is, it clones the game state then pays the mana, moves the cantrip to the graveyard, and draws a card.
- Add a call to
next_statesfunction of the
GameStateobject. This function figures out all possible plays from the present game state, clones the state that many times, and tries them all.
- If applicable, update
is_colorlessat the bottom of
Cantripinteracts correctly with Oath of Nissa, Ancient Stirrings, etc.
Shuffling is a problem for the simulation, so we don't do it. When the computer pops Sakura-Tribe Elder for a Mountain, it doesn't pull that Mountain out of our deck; it just creates a new one out of thin air. This means we don't capture the (marginal) effects of deck thinning, and slightly over-estimate the odds of drawing lands.
The problem is a bit subtle, so let's look at an example:
It's T3. We've got plenty of land and a Through the Breach, but we haven't drawn Primeval Titan yet. We play a Wooded Foothills and crack it. The options are Cinder Glade, Forest, Mountain, and Stomping Ground.
Strategically, there's little difference between them. No matter what we fetch, we'll be able to Breach a Titan as soon as we draw it. But the computer sees a choice, so it tries all the options. It makes 4 copies of the current game state (including the order of the deck). Then game state #1 fetches a Cinder Glade and shuffles, game state #2 fetches a Forest and shuffles, etc.
There are about 48 cards left in the deck at this point, including 4 Primeval Titans and 4 Summoner's Pacts, so each game state has a one-in-six chance to hit. If one of them does, it stops the simulation, saying "I found a line that gets T4 Titan!" But -- because the states all shuffled independently -- that happens way more often than one-in-six.
When game states shuffle independently, it essentially lets us double-dip on luck. It's like rolling four dice and keeping the best result. We can make the problem smaller by always shuffling in the same way, or by fetching the land but leaving the rest of the deck alone, but it doesn't go away. The only way to completely solve the problem is to make it impossible for the order of our deck to be affected by a "free" choice (like what to fetch).