-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtermine.py
executable file
·400 lines (342 loc) · 11.6 KB
/
termine.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
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
#!/usr/bin/python
# -*- coding: utf-8 -*-
# termine, a terminal minesweeper clone
# Copyright (C) 2017-2019 Damien Picard dam.pic AT free.fr
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import print_function
import argparse
import re
import sys
import os
import random
import time
from itertools import product
# from http://stackoverflow.com/a/21659588
# input getter
def _find_getch():
try:
import termios
except ImportError:
# Non-POSIX. Return msvcrt's (Windows') getch.
import msvcrt
return msvcrt.getch
# POSIX system. Create and return a getch that manipulates the tty.
import sys
import tty
def _getch():
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(fd)
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return ch
return _getch
getch = _find_getch()
class Cell(object):
TEXTS = {
0: 'o',
1: '\033[1m\033[34m1\033[0m',
2: '\033[1m\033[32m2\033[0m',
3: '\033[1m\033[31m3\033[0m',
4: '\033[94m4\033[0m',
5: '\033[91m5\033[0m',
6: '\033[34m6\033[0m',
7: '7',
8: '\033[1m\033[97m8\033[0m',
'F': u'\033[1m\033[31m\u2691\033[0m',
}
def __init__(self, coords, grid, is_mine):
self.grid = grid
self.coords = coords
self.is_mine = is_mine
self.is_flagged = False
self.is_opened = False
def compute_adjacent(self):
self.adjacent = 0
if self.is_mine:
return
for dx in range(-1, 2):
for dy in range(-1, 2):
x = dx + self.coords[0]
y = dy + self.coords[1]
if x < 0 or y < 0 or x >= width or y >= height:
continue
if grid[x][y].is_mine:
self.adjacent += 1
def get_output(self):
output = ' '
if self.is_opened:
if self.is_mine:
output = u'\u2737'
else:
output = self.TEXTS[self.adjacent]
elif self.is_flagged:
output = self.TEXTS['F']
return output
def toggle_flag(self):
self.is_flagged = not self.is_flagged
def generate_grid(width, height, mines):
grid = []
mines = [True if (n < mines) else False for n in range(width*height)]
random.shuffle(mines)
for x in range(width):
row = []
for y in range(height):
row.append(Cell((x, y), grid, mines[y*width+x]))
grid.append(row)
return grid
def compute_grid(grid):
for x in range(width):
for y in range(height):
grid[x][y].compute_adjacent()
def move_mine_to_first_empty_cell(cell):
for (x, y) in product(range(width), range(height)):
if not grid[x][y].is_mine:
grid[x][y].is_mine = True
break
cell.is_mine = False
compute_grid(grid)
def flag():
coords = current_coords
cell = grid[coords[0]][coords[1]]
if not cell.is_opened:
cell.toggle_flag()
def evaluate_flags(coords):
flags = 0
for dx in range(-1, 2):
for dy in range(-1, 2):
if dx == dy == 0:
continue
x = coords[0] + dx
y = coords[1] + dy
if x < 0 or y < 0 or x >= width or y >= height:
continue
if grid[x][y].is_flagged:
flags += 1
return flags
def open_cell(coords=None, visited_cells=None):
do_flagging = False
if coords is None: # values for first level of recursion
coords = current_coords
visited_cells = [coords]
cell = grid[coords[0]][coords[1]]
if cell.is_flagged:
return
if cell.is_mine:
return -1
flags = evaluate_flags(coords)
do_flagging = (
cell.adjacent == flags
and cell.is_opened
)
# avoid border
x, y = coords
if x < 0 or y < 0 or x >= width or y >= height:
return visited_cells
else:
cell = grid[coords[0]][coords[1]]
if do_flagging:
if cell.is_flagged:
return visited_cells
elif cell.is_mine and not cell.is_flagged:
return -1
else:
if cell.is_opened or cell.is_flagged:
return visited_cells
else:
if cell.is_mine:
return -1
else:
cell.is_opened = True
cell.is_flagged = False
if cell.adjacent != 0 and not do_flagging:
return visited_cells
visited_cells.append(coords)
# check all neighbouring cells which have not yet been visited
for dx in range(-1, 2):
for dy in range(-1, 2):
if dx == dy == 0: # avoid current cell
continue
x = coords[0] + dx
y = coords[1] + dy
if [x, y] not in visited_cells:
result = open_cell([x, y], visited_cells)
if result == -1: # found mine
return -1
visited_cells = result
return visited_cells
def step(next_move):
global start_time
if next_move in ('F', 'f', ' ') and not start_time:
start_time = time.time()
if next_move in ('Q', 'q', u'\003'):
sys.exit()
elif next_move == ' ':
cell = grid[current_coords[0]][current_coords[1]]
global first_cell_opened
if not first_cell_opened:
first_cell_opened = True
if cell.is_mine:
cell = grid[current_coords[0]][current_coords[1]]
move_mine_to_first_empty_cell(cell)
return open_cell()
elif next_move in ('F', 'f'):
flag()
elif (next_move) == 'A' and current_coords[1] > 0:
# up
current_coords[1] -= 1
elif (next_move) == 'B' and current_coords[1] < height - 1:
# down
current_coords[1] += 1
elif (next_move) == 'C' and current_coords[0] < width - 1:
# right
current_coords[0] += 1
elif (next_move) == 'D' and current_coords[0] > 0:
# left
current_coords[0] -= 1
def evaluate_cell_attribute(attr):
attr_value = 0
for x in range(width):
for y in range(height):
if getattr(grid[x][y], attr):
attr_value += 1
return attr_value
def clear_screen():
sys.stdout.write(u"\u001b[" + str(5 + height*2) + "A") # Move up
print(u"\u001b[1000D")
def print_grid(grid):
# header
print('\n ', end='')
# draw horizontal coordinates
for x in range(width):
print('% 4i' % (x+1), end='')
print('\n ╔═══' + '╤═══' * (width-1) + '╗')
for y in range(height):
if y > 0:
print(' ╟───' + '┼───' * (width-1) + '╢')
# draw vertical coordinates
print(' ' + ROWS[y] + ' ║', end='')
for x in range(width):
if x:
print('│', end='')
output = grid[x][y].get_output()
output = ' %s ' % output
if current_coords[0] == x and current_coords[1] == y:
output = '\033[47m' + output + '\033[49m'
# is_mine = 'X' if grid[x][y].is_mine else ' '
print(output, end='')
print('║')
print(' ╚═══' + '╧═══' * (width-1) + '╝')
def end_game():
for x in range(width):
for y in range(height):
cell = grid[x][y]
if cell.is_mine:
cell.is_opened = True
def save_highscores(date, game_time, size, mines):
home = os.environ['HOME']
if '.termine' not in os.listdir(home):
os.makedirs(os.path.join(home, '.termine'))
filepath = os.path.join(home, '.termine', 'highscores')
with open(filepath, 'a') as f:
f.write("{} {} {} {}\n".format(date, game_time, size, mines))
def print_highscores():
home = os.environ['HOME']
if (
'.termine' not in os.listdir(home)
and 'highscores' not in os.path.join(home, '.termine')
):
print('No highscores yet.')
else:
filepath = os.path.join(home, '.termine', 'highscores')
with open(filepath, 'r') as f:
highs = list(f) #.readlines()
# header
print("\n{:<26} {:<8} {:<8} {:<8}\n".format("Date",
"Score",
"Size",
"Mines"))
# scores
highs = [line.split(' ') for line in highs]
for line in sorted(highs, key=lambda l: (l[2], l[3], l[1])):
# print(line)
date, game_time, size, mines = line
date = time.strftime("%a %x %X", time.localtime(float(date)))
print("{:<26} {:<8.2f} {:<8} {}".format(date, float(game_time), size, mines), end='')
sys.exit()
if __name__ == '__main__':
ROWS = 'abcdefghjklmnopqrstuvwxyz'
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description='A terminal minesweeper clone.\n'
' Controls:\n'
' Spacebar: open a cell\n'
' F: flag\n'
' Q or ^C: quit'
)
parser.add_argument('-hs', '--highscores', action='store_true',
help='Print highscores')
parser.add_argument('-gs', '--gridsize', metavar='WxH',
type=str, default='8x8',
help='Size of the grid')
parser.add_argument('-m', '--mines', metavar='N',
type=int, default=10,
help='Number of mines')
args = parser.parse_args()
if args.highscores:
print_highscores()
mines = args.mines
match = re.match('([0-9]+)x([0-9]+)', args.gridsize)
try:
width, height = (int(g) for g in match.groups())
except:
print(' Please specify a grid size of form WidthxHeight')
sys.exit(1)
if mines >= width * height:
print(' Error: Number of mines too high')
sys.exit(1)
if mines < 1:
print(' Error: Number of mines too low')
sys.exit(1)
grid = generate_grid(width, height, mines)
compute_grid(grid)
current_coords = [0, 0]
start_time = 0
game_ended = False
first_move = True
first_cell_opened = False
while not game_ended:
if not first_move:
clear_screen()
print_grid(grid)
print('Remaining:',
mines - evaluate_cell_attribute('is_flagged'),
' ' * 10)
next_move = getch()
if step(next_move) == -1:
game_ended = "lose"
if evaluate_cell_attribute('is_opened') == width * height - mines:
game_ended = 'win'
first_move = False
end_game()
print_grid(grid)
game_time = time.time() - start_time
if game_ended == "lose":
print("Game over. Time:", "{:.2f}".format(game_time))
if game_ended == "win":
print("You win! Time:", "{:.2f}".format(game_time))
save_highscores(start_time, game_time, args.gridsize, mines)