-
Notifications
You must be signed in to change notification settings - Fork 3
/
mine.py
321 lines (252 loc) · 12 KB
/
mine.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
import numpy as np
from nurses import ScreenManager, Widget, colors
from nurses.keys import DOWN, LEFT, RIGHT, UP
from nurses.widgets import ArrayWin
def playMinesweeper() -> None:
"""Wrapper function englobing all MineSweeper code."""
# Keybindings
SPACE_KEY, RESET_KEY = ord(' '), ord('r')
FORFEIT_KEY = ord('g')
FLAG_KEY = ord('f')
# Miscs
OFFSET_TOP, OFFSET_LEFT = 5, 25
DELTA = .1
# Symbols
COVERED_SYMBOL = '❑'
EMPTY_SYMBOL = '⯀'
MINE_SYMBOL = '☠'
FLAG_SYMBOL = '☢'
HAPPYFACE_SYMBOL = '☺'
SADFACE_SYMBOL = '☹'
CURSOR_SYMBOL = 'ᐁ'
BOXEDCHECK_SYMBOL = '☑'
BOXEDCROSS_SYMBOL = '☒'
# States
COVERED_STATE = 0
UNCOVERED_STATE = 1
FLAGGED_STATE = 2
class Cursor(Widget):
"""Movable cursor to point to a land location."""
move_up = UP
move_down = DOWN
move_left = LEFT
move_right = RIGHT
lr_step = 1
ud_step = 1
wrap_height = None
wrap_width = None
offset_top = OFFSET_TOP
offset_left = OFFSET_LEFT
def __init__(self, wrap_height: int, wrap_width: int, *args, **kwargs):
self.wrap_height = wrap_height
self.wrap_width = wrap_width
super().__init__(*args, **kwargs)
def on_press(self, key: int) -> bool:
"""Handle keys for Cursor movement."""
top, left = self.top, self.left
height, width = self.height, self.width
if key == self.move_up:
if top > 0:
self.top -= self.ud_step
elif key == self.move_down:
if top + height < self.parent.height:
self.top += self.ud_step
elif key == self.move_left:
if left > 0:
self.left -= self.lr_step
elif key == self.move_right:
if left + width < self.parent.width:
self.left += self.lr_step
else:
return super().on_press(key)
if self.wrap_height:
self.top = (self.top - self.offset_top) % self.wrap_height + self.offset_top
if self.wrap_width:
self.left = (self.left - self.offset_left) % self.wrap_width + self.offset_left
return True
class Lawn(ArrayWin):
"""MineSweeper Game Board."""
def __init__(self, rows: int, cols: int, num_mines: int,
scoreboard: ArrayWin, gsm: ScreenManager, *args, **kwargs) -> None:
# Initialize display from ArrayWin
super().__init__(OFFSET_TOP, OFFSET_LEFT, rows, cols, *args, **kwargs)
self._shape = rows, cols
self._num_mines = num_mines
# Game board records the state of each cell
self._state_map = np.zeros((rows, cols)).astype(int)
# Initialize with given mine amount
self._mine_map = np.r_[np.full(rows * cols - num_mines, False), np.full(num_mines, True)]
self._solution_map = np.zeros((rows, cols)).astype(int)
# Scoreboard display scores and shoutout banner
self.scoreboard = scoreboard
# Game ScreenManager, useful for scheduling animation task
self._gsm = gsm
self._marching_task = None
def schedule_marching(self, delay: int = .3) -> None:
"""Use ScreenManager handle to animate scoreboard text."""
def marching_scoreboard() -> None:
"""Animate scoreboard with marching texts."""
head = self.scoreboard[1, 0]
tail = self.scoreboard[1, 1:]
self.scoreboard[1, :-1] = tail
self.scoreboard[1, -1] = head
self._marching_task = self._gsm.schedule(marching_scoreboard, delay=delay, n=120)
def init_lawn(self) -> None:
"""Initialize game board for the next game."""
self[:, :] = COVERED_SYMBOL
# Display the game board
self.revealed = False
# Clear the game board
self._state_map[:, :] = COVERED_STATE
# Randomize mine locations
self._mine_map = self._mine_map.reshape(-1)
np.random.shuffle(self._mine_map)
self._mine_map = self._mine_map.reshape(self._shape)
self._solution_map = self.build_solution()
# Erase shoutout text
self.scoreboard[:, :] = ' '
self.scoreboard[1, :8] = "Welcome!"
self.schedule_marching()
# Unset timer
self.timer = None
def build_solution(self) -> np.ndarray:
"""Count mines near each land in adjacent lands."""
rows, cols = self._shape
solution_map = np.zeros((rows, cols)).astype(int)
for r in range(rows):
for c in range(cols):
solution_map[r, c] = self._mine_map[
max(0, r - 1):min(r + 2, rows),
max(0, c - 1):min(c + 2, cols)
].sum()
solution_map = solution_map.astype(str)
solution_map[solution_map == '0'] = EMPTY_SYMBOL
return solution_map
def reveal_mines(self) -> None:
"""Reveal all mine locations."""
if not self.revealed:
self.revealed = True
self[:, :] = np.where(self._mine_map, MINE_SYMBOL, self[:, :])
def refresh(self) -> None:
"""Handle terminal display refresh."""
if self.timer and not self.revealed:
# Timer on the right of scoreboard
self.scoreboard[0, -3:] = str(int(self.timer)).rjust(3, '0')
self.timer += DELTA
# Flagging count on the left
self.scoreboard[0, :3] = str(self._num_mines - (self._state_map == FLAGGED_STATE).sum()).rjust(3, '0')
return super().refresh()
def on_press(self, key: int) -> bool:
"""Handle key press events."""
# Initialize timer
if not self.timer:
self.timer = DELTA
self.scoreboard[1, :] = ' '
self._marching_task.cancel()
if key == FORFEIT_KEY:
self.reveal_mines()
elif key == RESET_KEY:
self.init_lawn()
elif not self.revealed:
if key == SPACE_KEY:
self.poke()
if key == FLAG_KEY:
self.flag()
else:
return super().on_press(key)
return True
def flag(self) -> None:
"""Flag the location for potential mine."""
row, col = cursor.top - OFFSET_TOP, cursor.left - OFFSET_LEFT
if self._state_map[row, col] == COVERED_STATE:
self._state_map[row, col] = FLAGGED_STATE
self[row, col] = FLAG_SYMBOL
elif self._state_map[row, col] == FLAGGED_STATE:
self._state_map[row, col] = COVERED_STATE
self[row, col] = COVERED_SYMBOL
def poke(self) -> None:
"""Uncover the pointed location."""
row, col = cursor.top - OFFSET_TOP, cursor.left - OFFSET_LEFT
rows, cols = self._shape
def uncover_land(r: int, c: int) -> None:
"""Uncover adjacent locations when the adjacent mine count is 0."""
if not (0 <= r < rows and 0 <= c < cols) or state_map[r, c] == UNCOVERED_STATE:
return
state_map[r, c] = UNCOVERED_STATE
# Uncover the 8 adjacent locations
if self._solution_map[r, c] == EMPTY_SYMBOL:
for rr in range(r - 1, r + 2):
for cc in range(c - 1, c + 2):
if not (rr == r and cc == c):
uncover_land(rr, cc)
if self._state_map[row, col] != COVERED_STATE:
return
if self._mine_map[row, col]:
self.lose(row, col)
else:
state_map = self._state_map.copy()
uncover_land(row, col)
self._state_map = state_map
self[:, :] = np.where(self._state_map == UNCOVERED_STATE, self._solution_map, self[:, :])
self.evaluate()
def win(self) -> None:
"""Handle winning."""
# Replace good flags with boxed check marks, and all other locations with solutions
self[:, :] = np.where(self._mine_map,
BOXEDCHECK_SYMBOL, self._solution_map)
# Put up smiley and winning shoutout
self.scoreboard[0, len(self.scoreboard[0]) // 2] = HAPPYFACE_SYMBOL
self.scoreboard[1, :8] = "You win!"
self.schedule_marching(.1)
self.revealed = True
def lose(self, r: int, c: int) -> None:
"""Handle losing."""
# Replace good flags with boxed check marks, bad flags with boxed crosses,
# and all other locations with solutions
self[:, :] = np.where(self._mine_map,
np.where(self._state_map == FLAGGED_STATE, BOXEDCHECK_SYMBOL, MINE_SYMBOL),
np.where(self._state_map == FLAGGED_STATE, BOXEDCROSS_SYMBOL, self._solution_map))
# Color exploded mine
colors_copy = np.full(self._shape, self.color)
colors_copy[r, c] = colors.WHITE_ON_RED
self.colors = colors_copy
# Put up sad face and losing callout
self.scoreboard[0, len(self.scoreboard[0]) // 2] = SADFACE_SYMBOL
self.scoreboard[1, :8] = "You die!"
self.schedule_marching(.8)
self.revealed = True
def evaluate(self) -> None:
"""Evaluate winning or losing."""
# If all and only all mines are covered, win
if np.all(self._mine_map == (self._state_map != UNCOVERED_STATE)):
self.win()
# Otherwise, if the number of covered cells matches the number of mines, lose
elif (self._state_map != UNCOVERED_STATE).sum() == self._num_mines:
row, col = cursor.top - OFFSET_TOP, cursor.left - OFFSET_LEFT
self.lose(row, col)
with ScreenManager() as gsm:
num_mines = 10
rows, cols = 8, 8
text_len = 20
# Draw the scoreboard on the bottom
scoreboard = gsm.root.new_widget(OFFSET_TOP + rows + 2, OFFSET_LEFT, height=2, width=cols,
color=colors.RED_ON_BLACK, create_with="ArrayWin")
# Draw instructions on the side
instructions = gsm.root.new_widget(OFFSET_TOP, OFFSET_LEFT + cols + 2, height=6, width=text_len,
color=colors.YELLOW_ON_BLACK, create_with="ArrayWin")
instructions[0, :] = 'r: reset game'.ljust(text_len, ' ')
instructions[1, :] = 'g: give up game'.ljust(text_len, ' ')
instructions[2, :] = '␣: uncover location'.ljust(text_len, ' ')
instructions[3, :] = 'f: flag mine'.ljust(text_len, ' ')
instructions[4, :] = 'arrows: move pointer'.ljust(text_len, ' ')
instructions[5, :] = 'esc: leave game'.ljust(text_len, ' ')
# Draw board
lawn = gsm.root.new_widget(rows=rows, cols=cols, num_mines=num_mines,
scoreboard=scoreboard, gsm=gsm, create_with=Lawn)
lawn.init_lawn()
# Draw Cursor
cursor = gsm.root.new_widget(rows, cols, OFFSET_TOP, OFFSET_LEFT, 1, 1, transparent=True, create_with=Cursor)
cursor.window.addstr(0, 0, CURSOR_SYMBOL)
# Schedule refreshing task
gsm.schedule(gsm.root.refresh, delay=DELTA)
gsm.run()