-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathpandemic.py
351 lines (309 loc) · 10.1 KB
/
pandemic.py
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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
#!/usr/bin/env python
import copy
import readline
import sys
import re
import os
import pickle
special_commands = ['EPIDEMIC', 'REPIDEMIC', 'READ', 'LEVEL', 'VACCINATE', 'UNDO']
class SimpleCompleter(object):
def __init__(self, options):
self.options = sorted(options)
return
def add_to_options(self, option):
self.options.append(option)
self.options = sorted(self.options)
def complete(self, text, state):
response = None
if state == 0:
# This is the first time for this text, so build a match list.
if text:
self.matches = [s for s in self.options if s and s.startswith(text)]
else:
self.matches = self.options[:]
# Return the state'th item from the match list, if we have that many.
try:
response = self.matches[state]
except IndexError:
response = None
return response
class PandemicInfections(object):
def __init__(self, cities_filename, state_filename='state.txt'):
self.cities = []
self.stack = []
self.cards_drawn = []
self.commented_cities = []
self.cities_filename = cities_filename
self.state_filename = state_filename
self.level = 2
self.backups = []
self.completer = None
self.setup()
def setup(self):
self.read_cities()
self.stack = [copy.copy(self.cities)]
# Register the completer function and bind tab
options = copy.copy(self.cities)
options = sorted(set(options), key=options.index)
options.extend(special_commands)
self.completer = SimpleCompleter(options)
readline.set_completer(self.completer.complete)
readline.parse_and_bind('tab: complete')
def set_level(self):
question = 'Set infection level to? '
impossible = 'Invalid input! Please enter a number.'
line = input(question)
while not re.match('^\d+$', line):
print(impossible)
line = input(question)
self.level = int(line)
def read_cities(self):
# Read input file with city names.
self.cities = []
with open(self.cities_filename, 'r') as f:
for line in f:
line = line.strip('\n')
if line.startswith('#'):
self.commented_cities.append(line)
if not line or line.startswith('#'):
continue
if '*' in line:
n, city = line.split('*')
self.cities += int(n) * [city]
else:
self.cities += [line]
def write_cities(self):
# Read input file with city names.
with open(self.cities_filename, 'w') as f:
for city in sorted(set(self.cities), key=lambda v: self.cities.count(v), reverse=True):
print('%d*%s' % (self.cities.count(city), city), file=f)
for l in self.commented_cities:
print(l, file=f)
def draw_card(self, line):
# Draw card from the top of the stack and add it to the discard pile.
self.cards_drawn.append(line)
assert(self.stack[-1])
self.stack[-1].remove(line)
if not self.stack[-1]:
self.stack.pop()
def vaccinate(self):
question='Which city was vaccinated? '
line = input(question)
while line not in self.cards_drawn:
line = input(question)
assert(line in self.cards_drawn)
assert(line in self.cities)
self.cards_drawn.remove(line)
self.cities.remove(line)
self.write_cities()
def epidemic(self, line=None):
question = 'Which city was drawn from the bottom in the Epidemic? '
impossible = 'This is impossible!'
# Draw card from front of the stack ("the bottom")
assert(self.stack)
front_pile = self.stack[0]
if not line:
line = input(question)
while not line in front_pile:
print(impossible)
line = input(question)
assert(line in front_pile)
self.cards_drawn.append(line)
# Remove card from front pile and remove it from the stack if it is now empty.
front_pile.remove(line)
if not front_pile:
del self.stack[0]
# Push discard pile on stack and reset it.
self.stack.append(sorted(self.cards_drawn))
self.cards_drawn = []
def repidemic(self):
question='Which city was drawn from box 6? '
line = input(question)
self.stack[0].append(line)
self.cities.append(line)
self.completer.add_to_options(line)
self.write_cities()
self.epidemic(line)
def print_state(self, f=sys.stdout):
# Print the draw deck with sections
i = 0
print('', file=f)
print('############################', file=f)
print('### The Deck ###', file=f)
for x in self.stack:
for city in sorted(set(x), key=lambda v: x.count(v), reverse=True):
print('%d * %s' % (x.count(city), city), file=f)
i += 1
if i != len(self.stack):
print('----------------------------', file=f)
print('############################', file=f)
# Print the discard pile
print('', file=f)
print('############################', file=f)
print('### Discard ###', file=f)
for city in sorted(set(self.cards_drawn), key=lambda v: self.cards_drawn.count(v), reverse=True):
print('%d * %s' % (self.cards_drawn.count(city), city), file=f)
print('############################', file=f)
def write_state(self):
# Write the current state to disk
with open(self.state_filename, 'w') as f:
self.print_state(f=f)
def read_state(self):
# Read the current state from disk
self.stack = []
self.cards_drawn = []
phase = ''
with open(self.state_filename, 'r') as f:
for line in f:
line = line.strip('\n')
if 'The Deck' in line:
self.stack = [[]]
self.cards_drawn = []
phase = 'deck'
elif 'Discard' in line:
phase = 'discard'
if phase == 'deck' and line.startswith('-----'):
self.stack.append([])
if not re.search('^\d+ \* \w+$', line):
continue
occurences, _, city = line.split(' ')
for k in range(int(occurences)):
if phase == 'deck':
self.stack[-1].append(city)
elif phase == 'discard':
self.cards_drawn.append(city)
else:
assert(False)
def calculate_probability(self, city, M, N, stack=None):
if stack is None:
stack = copy.deepcopy(self.stack)
N_cards = sum([len(x) for x in stack])
N = min(N, N_cards)
# Stop conditions
if M == 0:
return 1.0
if M > N:
return 0.0
assert(M >= 1)
assert(N >= 1)
assert(stack)
pile = stack.pop()
count = pile.count(city)
total = len(pile)
assert(total > 0)
p_city = count / total
# If there was only one card to draw: This is the leaf probability
if N == 1:
return p_city
# Prepare two new piles, one with the city removed and one with some other city removed (if any)
pile1 = copy.copy(pile)
if city in pile1:
pile1.remove(city)
pile2 = copy.copy(pile)
for x in pile2:
if x != city:
pile2.remove(x)
break
# Add the two new piles to two stacks
stack1 = copy.copy(stack)
stack2 = copy.copy(stack)
if pile1:
stack1.append(pile1)
if pile2:
stack2.append(pile2)
# Add the two branch probabilities
p1 = (p_city * self.calculate_probability(city, M-1, N-1, stack=stack1)) if p_city > 0.0 else 0.0
p2 = ((1 - p_city) * self.calculate_probability(city, M, N-1, stack=stack2)) if p_city < 1.0 else 0.0
return p1 + p2
def print_probabilities(self, f=sys.stdout):
print('', file=f)
header = '%-15s' % 'Name'
for i in range(1, self.level + 1):
header += ' %6s' % ("N>=%d" % i)
print(header, file=f)
print(len(header)*'-', file=f)
probabilities = dict()
for x in set(self.cities):
probabilities[x] = []
for M in range(1, self.level + 1):
probabilities[x].append(self.calculate_probability(x, M, self.level))
for x, p in sorted(probabilities.items(), key=lambda x: x[1][0], reverse=True):
line = '%-15s ' % x
for px in p:
line += "%5.1f%% " % (100.0 * px)
print(line, file=f)
def write_probabilities(self):
# Write the current state to disk
with open(self.state_filename, 'a') as f:
self.print_probabilities(f=f)
def print(self):
self.print_state()
self.print_probabilities()
def write(self):
self.write_state()
self.write_probabilities()
def __getstate__(self):
attributes = self.__dict__.copy()
del attributes['backups']
return attributes
def store_backup(self):
self.backups.append(pickle.dumps(self))
def undo(self):
if len(self.backups) < 2:
print('Not enough backups...')
return
b = pickle.loads(self.backups[-2])
self.cities = b.cities
self.stack = b.stack
self.cards_drawn = b.cards_drawn
self.commented_cities = b.commented_cities
self.cities_filename = b.cities_filename
self.state_filename = b.state_filename
self.level = b.level
del self.backups[-2:]
def initialize(self):
if not os.path.exists(self.state_filename):
self.level = 9
self.print()
self.write()
self.level = 2
else:
self.read_state()
self.print()
self.store_backup()
def run(self):
# The main input loop
self.initialize()
question = 'Please enter the name of the city which was drawn or "%s": ' % ('/'.join(special_commands))
impossible = 'This is impossible!'
while True:
# Get new input
print()
line = input(question)
while line not in special_commands and not line in self.stack[-1]:
print(impossible)
line = input(question)
# Process
if line == 'LEVEL':
self.set_level()
elif line == 'READ':
self.read_state()
elif line == 'EPIDEMIC':
self.epidemic()
elif line == 'REPIDEMIC':
self.repidemic()
elif line == 'VACCINATE':
self.vaccinate()
elif line == 'UNDO':
self.undo()
else:
self.draw_card(line)
# Print current state and probabilities, write state to disk
self.print()
self.write()
self.store_backup()
# Start the input loop
cities_file = sys.argv[1]
output_file = sys.argv[2] if len(sys.argv) > 2 else 'state.txt'
p = PandemicInfections(cities_filename=cities_file, state_filename=output_file)
p.run()