-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
223 lines (193 loc) · 5.46 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
package main
// This is a Monte Carlo simulation for how fast a 2 card combo can be
// drawn into in Magic: The Gathering. It simplifies the game down to
// just lands and non-lands, with non-lands being the only cards capable
// of being combo pieces. This simulation assumes 2 combo cards in hand
// is a win-con and doesn't attempt to discern if the combo was castable.
import (
"fmt"
"log"
"math/rand"
"sync"
)
// Card holds the information for a card in the game
type Card struct {
keyword string // denotes land or non-land
combo bool // denotes a combo piece
}
// Results collates the simulations of a scenario run
type Results struct {
attempts int64
averageDrawsToWin float64
openingHandWins int64
averageOpeningHandWins float64
averageOpeningLands float64
}
// Simulation holds the results of the sim's run
type Simulation struct {
// drawsToWinCon is the number of draws to find the required
// number of combo pieces
drawsToWinCon int64
// openingHandWinCon is true if the first 7 cards drawn
// contained the required number of combo pieces
openingHandWin bool
// openingHandLands records the number of lands drawn in the
// opening hand
openingHandLands int
}
// this first scenario models a 37 land deck with 62 permanents and
// 2 combo pieces. this deck is then shuffled and run until it hits
// both combo pieces snd records the turn count that happened.
func main() {
fmt.Println("🔮 mtg-sim booting up")
var numSimulations = 10_000_000
var input = make(chan Simulation, 10_000)
results, err := runScenario(input, numSimulations)
if err != nil {
log.Fatalf("error: %+v", err)
}
fmt.Printf("📊 results:\n%+v\n", results)
}
// runScenario runs a deck simulations a given number of times.
func runScenario(input chan Simulation, numSimulations int) (Results, error) {
var results = Results{}
wg := &sync.WaitGroup{}
wg.Add(numSimulations)
go func(input chan Simulation) {
for i := 0; i < numSimulations; i++ {
deck := createDeck()
input <- runSimulation(deck)
}
}(input)
var drawCount = []int64{}
var openingLandCount = []int64{}
var openingWinCount = 0
go func() {
for {
select {
case sim := <-input:
results.attempts++
// record an opening hand win
if sim.openingHandWin {
openingWinCount++
}
// record draws to required win
drawCount = append(drawCount, sim.drawsToWinCon)
// record opening hand lands
openingLandCount = append(openingLandCount, int64(sim.openingHandLands))
wg.Done()
}
}
}()
wg.Wait()
// calculate the drawSum and average of draw counts
var drawSum int64 = 0
for _, value := range drawCount {
drawSum += value
}
// calculate opening land averages
var landSum int64 = 0
for _, val := range openingLandCount {
landSum += val
}
// calculate averages for each category
results.averageDrawsToWin = float64(drawSum) / float64(len(drawCount))
results.averageOpeningHandWins = float64(openingWinCount) / float64(results.attempts)
results.openingHandWins = int64(openingWinCount)
results.averageOpeningLands = float64(landSum) / float64(results.attempts)
return results, nil
}
// createDeck creates a deck with the default setup of lands,
// non-lands, and combo pieces.
func createDeck() []Card {
// setup the distribution of cards for our simulation
var numLands = 37
// set the number of non-lands to the rest of the deck
var numNonLands = 99 - numLands
// assumes the commander is not a part of the combo strategy
var numComboPieces = 4
// create a deck
var deck []Card
// add lands to the deck
for i := 0; i < numLands; i++ {
deck = append(deck, Card{
keyword: "land",
})
}
// add non-combo permanents
for i := 0; i < numNonLands-numComboPieces; i++ {
deck = append(deck, Card{
keyword: "non-land",
combo: false,
})
}
// finally, add the appropriate number of combo pieces to the deck.
// it is assumed that all combo pieces must be drawn to trigger
// the win condition.
for i := 0; i < numComboPieces; i++ {
deck = append(deck, Card{
keyword: "non-land",
combo: true,
})
}
return shuffleDeck(deck)
}
// shuffleDeck shuffles a slice of Cards and returns the shuffled slice
func shuffleDeck(deck []Card) []Card {
rand.Shuffle(len(deck), func(i, j int) {
deck[i], deck[j] = deck[j], deck[i]
})
return deck
}
// runSimulation starts drawing down until it hits a win con and
// then records the results of the simulation for later analysis
func runSimulation(deck []Card) Simulation {
var drawCount int64 = 0
hand, deck := deck[:6], deck[7:]
openingLands := 0
// check lands in opening hand
for _, c := range hand {
if c.keyword == "land" {
openingLands++
}
}
if checkComboWin(hand, 2) {
return Simulation{
drawsToWinCon: drawCount,
openingHandWin: true,
}
}
for i := 0; i < len(deck)-len(hand); i++ {
drawCount++
// draw
drawn := deck[0]
deck = deck[1:]
hand = append(hand, drawn)
// check if enough combo pieces have been hit
if checkComboWin(hand, 2) {
return Simulation{
drawsToWinCon: drawCount,
openingHandWin: false,
}
}
}
return Simulation{
drawsToWinCon: drawCount,
openingHandWin: false,
openingHandLands: openingLands,
}
}
// checks if the required number of combo cards has been drawn
// into hand for a naive win-con check
func checkComboWin(hand []Card, required int64) bool {
var count int64 = 0
for i := 0; i < len(hand); i++ {
if hand[i].combo {
count++
if count == required {
return true
}
}
}
return false
}