-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathjerk_board.py
347 lines (244 loc) · 11.5 KB
/
jerk_board.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
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import RegularPolygon
class JerkBoard():
""" This object acts as the environemnt for a Minesweeper game.
An agent can interface with the game using the following functions:
board.user_select(i, j)
Right click a cell. Returns -1 if mine or minecount of cell
board.user_flag(i, j)
Toggles mine on a cell
board.check_gameover_conditions()
Returns boolean to check if game is over. If true, board.score
is populated
board.score
Get score of the game following gameover conditions are met
The following blog post was used as reference for the visualization commponent
of this object.
https://jakevdp.github.io/blog/2012/12/06/minesweeper-in-matplotlib/
"""
count_colors = ['none', 'blue', 'green', 'red', 'darkblue',
'darkred', 'darkgreen', 'black', 'black']
flag_vertices = np.array([[0.25, 0.2], [0.25, 0.8],
[0.75, 0.65], [0.25, 0.5]])
def __init__(self, dim=15, num_mines=45, fog_probability=0.2):
self.dim = dim
self.num_mines = num_mines
# fog probability is the chance that the board will not return the hint
# to the agent. It will instead return a integer value of -2, signifying that
# a hint was not provided.
self.fog_probability = fog_probability
# grid of cells of the board. Shows mines (-1) and minecounts
self.cells = np.zeros((dim, dim))
# grid of bools. True if agent has excavated the cell at i, j
self.excavated = np.zeros((dim, dim), dtype=bool)
# grid of bools. True if agent has placed a flag at i, j
self.flags = np.zeros((dim, dim), dtype=object)
# Boolean marking if game is complete
self.gameover = False
# Score value will be populated to this attribute upon gameover
self.score = None
# Create the figure and axes
self.fig = plt.figure(figsize=((dim + 2) / 3., (dim + 2) / 3.))
self.ax = self.fig.add_axes((0.05, 0.05, 0.9, 0.9),
aspect='equal', frameon=False,
xlim=(-0.05, dim + 0.05),
ylim=(-0.05, dim + 0.05))
for axis in (self.ax.xaxis, self.ax.yaxis):
axis.set_major_formatter(plt.NullFormatter())
axis.set_major_locator(plt.NullLocator())
# Create the grid of squares
self.squares = np.array([[RegularPolygon((i + 0.5, j + 0.5),
numVertices=4,
radius=0.5 * np.sqrt(2),
orientation=np.pi / 4,
ec='black',
fc='lightgray')
for j in range(dim)]
for i in range(dim)])
[self.ax.add_patch(sq) for sq in self.squares.flat]
self.place_mines()
self.assign_mine_counts()
# Event hook for mouse clicks
self.fig.canvas.mpl_connect('button_press_event', self._button_press)
def place_mines(self):
"""Randomly places self.num_mines across the grid.
Mines are denoted by (-1)
"""
for mine_idx in range(self.num_mines):
not_placed=True
while(not_placed):
i, j = np.random.randint(0, self.dim, 2)
if self.cells[i, j] != -1:
# Place mine
self.cells[i, j] = -1
not_placed=False
return
def assign_mine_counts(self):
"""Assigns adjacentcy mine counts too all non-mine cells of the grid.
Mine counts can be [0, 8] and are written to self.cells
"""
for i in range(self.dim):
for j in range(self.dim):
mine_count = 0
# if (i,j) is not mine
if self.cells[i, j] != -1:
# Neighbors in 8 directions (cardinal plus diagonal)
neighbors = [(i+1, j),
(i, j+1),
(i-1, j),
(i, j-1),
(i+1, j+1),
(i+1, j-1),
(i-1, j+1),
(i-1, j-1),
]
for neighbor in neighbors:
# Check if neighbor is on the board
if (0 <= neighbor[0] < self.dim) and (0 <= neighbor[1] < self.dim):
# Increment mine count for (i,j) if mine exists in neighbor
if self.cells[neighbor[0], neighbor[1]] == -1:
mine_count += 1
# Assign minecount to cell[i, j]
self.cells[i, j] = mine_count
return
def user_select(self, i, j):
""" User function for selecting cell (i, j).
If the cell has already been excavated: do nothing
If the cell has a flag on it: do nothing
If the cell has a mine: reveal the mine
Otherwise: reveal the cell's mine count
Returns -1 on mine or mine count integer value
"""
# If the cell is excavated, do nothing
if self.excavated[i, j]:
return None
# If the cell is flagged, do nothing
if self.flags[i, j]:
return None
# If the cell is a mine, essplode
if self.cells[i, j] == -1:
self.excavated[i, j] = True
self._draw_exploded_mine(i, j)
return -1
# Otherwise, this is a regular safe cell.
self.excavated[i, j] = True
# Sample for to see if the fog clouds the hint for the agent
roll = np.random.uniform()
if roll <= self.fog_probability:
# The fog wins. Agent is not given the hint
self._draw_mine_count_value(i, j, fog=True)
return -2
else:
# The agent is given the hint
self._draw_mine_count_value(i, j)
return int(self.cells[i, j])
def user_flag(self, i, j):
""" User function for placing flag at cell (i, j)
"""
# If the cell has already been excavated, do nothing
if self.excavated[i, j]:
return
else:
# Otherwise, toggle flag. The helper function applies changes
# to self.flags and adds/removes flag to self.ax + applies draw()
self._toggle_flag(i, j)
def check_gameover_conditions(self):
""" Checks if end-game conditions are met.
flag_count + excavated_count == total cells on board
"""
flag_count = sum([bool(cell) for cell in self.flags.flatten()])
excavated_count = sum(sum(self.excavated))
if self.dim**2 == (flag_count + excavated_count):
self.gameover = True
# Calculate score
self._calculate_score()
return True
else:
return False
def _reveal_board(self):
""" Reveals all cells of the board. WARNING: flag state is removed
"""
for i in range(self.dim):
for j in range(self.dim):
# remove flag if present
if self.flags[i, j]:
self._toggle_flag(i, j)
# if mine, draw mine
if self.cells[i, j] == -1:
self._draw_mine(i, j)
else:
self._draw_mine_count_value(i, j)
return
def _button_press(self, event):
""" Event hook for catching mouse clicks
Pipes left and right click actions to user_select() and user_flag()
"""
# Get coordinates of cell clicked on
i, j = map(int, (event.xdata, event.ydata))
if (i < 0 or j < 0 or i >= self.dim or j >= self.dim):
return
# Left Mouse Click. button == 1
# Pipe to user_select
if event.button == 1:
self.user_select(i, j)
# Right Mouse Click. button == 3
# Pipe to user_place_flag
if event.button == 3:
self.user_flag(i, j)
# Redraw canvas
self.fig.canvas.draw()
def _calculate_score(self):
""" Calculates the current point value according to the state of the board
For each correctly placed flag, one point is earned.
Uses self.flag and self.cells as global truth of the state of the game
"""
correct = 0
incorrect = 0
for i in range(self.dim):
for j in range(self.dim):
# cell (i, j) has a flag and has a mine
if (bool(self.flags[i, j])) and (self.cells[i, j] == -1):
correct += 1
# cell has a flag and no mine
if (bool(self.flags[i, j])) and (self.cells[i, j] != -1):
incorrect += 1
self.score = (correct - incorrect) / self.num_mines
def _draw_mine_count_value(self, i, j, fog=False):
"""Draws colored mine count value at cell @ i, j
"""
self.squares[i, j].set_facecolor('white')
# If fog applies, then print a '?' to represent that the agent dont know
if fog:
self.ax.text(i + 0.5, j + 0.5, '?',
color='purple',
ha='center', va='center', fontsize=18,
fontweight='bold')
return
self.ax.text(i + 0.5, j + 0.5, str(int(self.cells[i, j])),
color=self.count_colors[int(self.cells[i, j])],
ha='center', va='center', fontsize=18,
fontweight='bold')
def _draw_mine(self, i, j):
""" Draws mine at cell @ i, j. Mine is black and gray
"""
self.squares[i, j].set_facecolor('white')
self.ax.add_patch(plt.Circle((i + 0.5, j + 0.5), radius=0.25,
ec='black', fc='gray'))
def _draw_exploded_mine(self, i, j):
""" Draws exploded mine at cell @ i, j. Mine is black and red.
"""
self.squares[i, j].set_facecolor('white')
self.ax.add_patch(plt.Circle((i + 0.5, j + 0.5), radius=0.25,
ec='black', fc='orangered'))
def _toggle_flag(self, i, j):
""" Toggles flag image on cell at i, j
"""
if self.flags[i, j]:
self.ax.patches.remove(self.flags[i, j])
self.flags[i, j] = None
else:
self.flags[i, j] = plt.Polygon(self.flag_vertices + [i, j],
fc='red', ec='black', lw=2)
self.ax.add_patch(self.flags[i, j])
self.fig.canvas.draw()