FunHub-minesweeper 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: FunHub-minesweeper
3
+ Version: 1.0.0
4
+ Summary: CLI minesweeper game for FunHud app-launcher
5
+ Description-Content-Type: text/markdown
6
+ Requires-Dist: pynput>=1.7.6
7
+ Requires-Dist: colorama>=0.4.6
8
+ Requires-Dist: numpy>=1.24.0
9
+ Dynamic: description
10
+ Dynamic: description-content-type
11
+ Dynamic: requires-dist
12
+ Dynamic: summary
13
+
14
+ A simply CLI minesweeper game for [FunHud app-launcher](https://pypi.org/project/FunHub-app-launcher/)
15
+
16
+ ## Install
17
+ ```bash
18
+ pip install funhub-tic-tac-toe
19
+ ```
20
+ ## Quick start
21
+ Write in terminal this command to start game:
22
+ ```bash
23
+ funhub-minesweeper
24
+ ```
@@ -0,0 +1,9 @@
1
+ README.md
2
+ minesweeper.py
3
+ setup.py
4
+ FunHub_minesweeper.egg-info/PKG-INFO
5
+ FunHub_minesweeper.egg-info/SOURCES.txt
6
+ FunHub_minesweeper.egg-info/dependency_links.txt
7
+ FunHub_minesweeper.egg-info/entry_points.txt
8
+ FunHub_minesweeper.egg-info/requires.txt
9
+ FunHub_minesweeper.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ funhub-minesweeper = minesweeper:main
@@ -0,0 +1,3 @@
1
+ pynput>=1.7.6
2
+ colorama>=0.4.6
3
+ numpy>=1.24.0
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: FunHub-minesweeper
3
+ Version: 1.0.0
4
+ Summary: CLI minesweeper game for FunHud app-launcher
5
+ Description-Content-Type: text/markdown
6
+ Requires-Dist: pynput>=1.7.6
7
+ Requires-Dist: colorama>=0.4.6
8
+ Requires-Dist: numpy>=1.24.0
9
+ Dynamic: description
10
+ Dynamic: description-content-type
11
+ Dynamic: requires-dist
12
+ Dynamic: summary
13
+
14
+ A simply CLI minesweeper game for [FunHud app-launcher](https://pypi.org/project/FunHub-app-launcher/)
15
+
16
+ ## Install
17
+ ```bash
18
+ pip install funhub-tic-tac-toe
19
+ ```
20
+ ## Quick start
21
+ Write in terminal this command to start game:
22
+ ```bash
23
+ funhub-minesweeper
24
+ ```
@@ -0,0 +1,11 @@
1
+ A simply CLI minesweeper game for [FunHud app-launcher](https://pypi.org/project/FunHub-app-launcher/)
2
+
3
+ ## Install
4
+ ```bash
5
+ pip install funhub-tic-tac-toe
6
+ ```
7
+ ## Quick start
8
+ Write in terminal this command to start game:
9
+ ```bash
10
+ funhub-minesweeper
11
+ ```
@@ -0,0 +1,483 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+ import random
6
+ import shutil
7
+ import threading
8
+ import time
9
+ from pynput import keyboard
10
+ import sys
11
+ from colorama import init, Cursor, Fore, Back, Style
12
+ import ctypes
13
+ import numpy as np
14
+
15
+ init(autoreset=True)
16
+
17
+ if sys.platform == 'win32':
18
+ import msvcrt
19
+ kernel32 = ctypes.windll.kernel32
20
+ kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
21
+ else:
22
+ import termios
23
+ import tty
24
+ fd = sys.stdin.fileno()
25
+ oldSettings = termios.tcgetattr(fd)
26
+ tty.setcbreak(fd)
27
+
28
+ stop_event = threading.Event()
29
+ print_lock = threading.Lock()
30
+
31
+ MIN_TERMINAL_COLS, MIN_TERMINAL_ROWS = 150, 60
32
+ EASY_FIELD_SIZE, MEDIUM_FIELD_SIZE, HARD_FIELD_SIZE, VERY_HARD_FIELD_SIZE = 9, 16, 30, 49
33
+
34
+ backgroundColor = 250
35
+ numbersColors = [12, 10, 9, 4, 94, 14, 0,15]
36
+
37
+ userStatsPath = Path.home() / ".funhub" / "minesweeper" / "userStats.json"
38
+
39
+ os.makedirs(os.path.dirname(userStatsPath), exist_ok=True)
40
+
41
+ if not os.path.exists(userStatsPath):
42
+ with open(userStatsPath, 'w') as f:
43
+ json.dump({"easy":0, "medium":0, "hard":0, "very hard":0}, f, ensure_ascii=False, indent=4)
44
+
45
+ gameField = []
46
+ gameFieldSize = 0
47
+ mines = 0
48
+ selectedCell = 0, 0
49
+
50
+ gameField = np.array([])
51
+
52
+ logo = r'''
53
+ __ __ _
54
+ | \/ (_)_ _ ___ ____ __ _____ ___ _ __ ___ _ _
55
+ | |\/| | | ' \/ -_|_-< V V / -_) -_) '_ \/ -_) '_|
56
+ |_| |_|_|_||_\___/__/\_/\_/\___\___| .__/\___|_|
57
+ |_|
58
+
59
+ '''
60
+
61
+ class cellObject():
62
+ def __init__(self):
63
+ self.isMine = False
64
+ self.isFlag = False
65
+ self.isOpen = False
66
+ self.isSelected = False
67
+ self.neighborMines = 0
68
+
69
+ def getStr(self):
70
+ if not self.isOpen and not self.isFlag:
71
+ return f"\x1b[38;5;{backgroundColor};48;5;{254}m{' # ' if not self.isSelected else '[#]'}\x1b[0m"
72
+ elif not self.isOpen and self.isFlag:
73
+ return f"\x1b[38;5;{11};48;5;{backgroundColor}m{' F ' if not self.isSelected else '[F]'}\x1b[0m"
74
+ elif self.isOpen and not self.isMine and self.neighborMines > 0:
75
+ return f"\x1b[38;5;{numbersColors[self.neighborMines - 1]};48;5;{backgroundColor}m{f' {self.neighborMines} ' if not self.isSelected else f'[{self.neighborMines}]'}\x1b[0m"
76
+ elif self.isOpen and not self.isMine and self.neighborMines == 0:
77
+ return f"\x1b[38;5;{0};48;5;{backgroundColor}m{' ' if not self.isSelected else '[ ]'}\x1b[0m"
78
+ elif self.isOpen and self.isMine:
79
+ return f"\x1b[38;5;{0};48;5;{9}m{' * ' if not self.isSelected else '[*]'}\x1b[0m"
80
+
81
+ def openCell(self):
82
+ if not self.isOpen and not self.isFlag:
83
+ self.isOpen = True
84
+ return True
85
+ return False
86
+
87
+ def placeFlag(self):
88
+ if not self.isOpen:
89
+ if self.isFlag:
90
+ self.isFlag = False
91
+ else:
92
+ self.isFlag = True
93
+
94
+ def printField(startedTime=0, isWin = False, isDefeat = False):
95
+ with print_lock:
96
+ global gameFieldSize, logo, mines
97
+ termCols, termRows = os.get_terminal_size()
98
+ resulStr = ''
99
+ logo = logo if gameFieldSize < 40 else 'Minesweeper'
100
+ centeredLogo = ''
101
+
102
+ for i in logo.splitlines():
103
+ centeredLogo += ' ' * int((os.get_terminal_size().columns - len(i))//2) + i + '\n'
104
+
105
+ for r in range(gameFieldSize):
106
+ row_str = ''
107
+ for c in range(gameFieldSize):
108
+ cell = gameField[r][c]
109
+ char = cell.getStr()
110
+ row_str += char
111
+ resulStr += row_str + '\n'
112
+
113
+ centeredResulStr = ''
114
+
115
+ for i in resulStr.splitlines():
116
+ centeredResulStr += ' ' * int((termCols - (gameFieldSize * 3))//2) + i + '\n'
117
+
118
+ centeredResulStr = centeredLogo + '\n' + centeredResulStr
119
+ upperSpace = (termRows - len(centeredResulStr.splitlines()))//2
120
+
121
+ info = "time: " + getTime(startedTime, getStr=True) + ' ' + f'mines:{mines}'
122
+ info = " " * ((os.get_terminal_size().columns - len(info))//2) + info
123
+ centeredResulStr += '\n' + info
124
+
125
+ endGameText = ''
126
+ if isWin:
127
+ endGameText = 'You win!'
128
+ elif isDefeat:
129
+ endGameText = 'You lose!'
130
+
131
+ if not endGameText:
132
+ centeredResulStr = '\n' * upperSpace + centeredResulStr + '\n' * (termRows - upperSpace - len(centeredResulStr.splitlines()) - 1) + '↑/↓/←/→ – move, Enter – enter, R - restart, F - place flag, Ctrl + C – quit'
133
+ else:
134
+ endGameText = ' ' * int((termCols - len(endGameText))//2) + endGameText
135
+ centeredResulStr = '\n' * upperSpace + centeredResulStr + '\n\n' + endGameText +'\n' * (termRows - upperSpace - len(centeredResulStr.splitlines()) - 3) + '↑/↓/←/→ – move, Enter – enter, R - restart, F - place flag, Ctrl + C – quit'
136
+
137
+
138
+ sys.stdout.write(Cursor.POS(1, 1))
139
+ sys.stdout.write(centeredResulStr)
140
+ sys.stdout.flush()
141
+
142
+ def changeSelection(direction):
143
+ global selectedCell
144
+ cols, rows = gameField.shape
145
+
146
+ gameField[selectedCell[0]][selectedCell[1]].isSelected = False
147
+
148
+ if direction == 'up':
149
+ selectedCell = (selectedCell[0] - 1) % cols, selectedCell[1]
150
+ elif direction == 'down':
151
+ selectedCell = (selectedCell[0] + 1) % cols, selectedCell[1]
152
+ elif direction == 'right':
153
+ selectedCell = selectedCell[0], (selectedCell[1] + 1) % rows
154
+ elif direction == 'left':
155
+ selectedCell = selectedCell[0], (selectedCell[1] - 1) % rows
156
+
157
+ gameField[selectedCell[0]][selectedCell[1]].isSelected = True
158
+
159
+ def selectDificulti():
160
+ global gameField, gameFieldSize, mines, selectedCell
161
+ try:
162
+ difilculties = ['Easy', 'Medium', 'Hard', 'Very hard']
163
+ selectedDiff = 0
164
+
165
+ prew_cols, prew_rows = 0, 0
166
+
167
+ with open(userStatsPath, 'r', encoding='utf-8') as f:
168
+ userStats = json.load(f)
169
+
170
+ def printMenu():
171
+ menuToPrint = ''
172
+ termCols, termRows = os.get_terminal_size()
173
+ centeredStr = ''
174
+ nonlocal prew_cols, prew_rows, selectedDiff
175
+
176
+ if termCols != prew_cols or termRows != prew_rows:
177
+ prew_cols, prew_rows = termCols, termRows
178
+ sys.stdout.write('\033[2J\033[H')
179
+ sys.stdout.flush()
180
+
181
+ for i in logo.splitlines():
182
+ centeredStr += ' ' * int((termCols - len(i))//2) + i + '\n'
183
+
184
+ menuToPrint += centeredStr
185
+
186
+ difList = ''
187
+ for i in difilculties:
188
+ if i == difilculties[selectedDiff]:
189
+ difList += f'*{i}' + '\n'
190
+ else:
191
+ difList += f' {i}' + '\n'
192
+
193
+ centeredStr = ''
194
+
195
+ for i in difList.splitlines():
196
+ centeredStr += ' ' * int((termCols - len(i))//2) + i + '\n'
197
+
198
+ menuToPrint += centeredStr
199
+
200
+ upperSpace = (termRows - len(menuToPrint.splitlines()))//2
201
+
202
+ menuToPrint = '\n' * upperSpace + menuToPrint + '\n' * (termRows - upperSpace - len(menuToPrint.splitlines()) - 1) + '↑/↓/ – move, Enter – select, Ctrl + C – quit'
203
+
204
+ sys.stdout.write(Cursor.POS(1, 1))
205
+ sys.stdout.write(menuToPrint)
206
+ sys.stdout.flush()
207
+
208
+ def setDif():
209
+ global gameField, gameFieldSize, mines, gameField
210
+ gameField = [[cellObject() for _ in range(gameFieldSize)] for _ in range(gameFieldSize)]
211
+ gameField = np.array(gameField)
212
+
213
+
214
+ sys.stdout.write('\033[2J\033[H')
215
+ sys.stdout.flush()
216
+ printMenu()
217
+ with keyboard.Events() as events:
218
+ while True:
219
+
220
+ event = events.get(1.0)
221
+ if event is None:
222
+ continue
223
+
224
+ if isinstance(event, keyboard.Events.Press):
225
+ key = event.key
226
+ if key == keyboard.Key.up:
227
+ selectedDiff = (selectedDiff - 1) % len(difilculties)
228
+ printMenu()
229
+ elif key == keyboard.Key.down:
230
+ selectedDiff = (selectedDiff + 1) % len(difilculties)
231
+ printMenu()
232
+ elif key == keyboard.Key.enter:
233
+ if difilculties[selectedDiff] == 'Easy':
234
+ gameFieldSize = EASY_FIELD_SIZE
235
+ mines = 10
236
+ elif difilculties[selectedDiff] == 'Medium':
237
+ gameFieldSize = MEDIUM_FIELD_SIZE
238
+ mines = 40
239
+ elif difilculties[selectedDiff] == 'Hard':
240
+ gameFieldSize = HARD_FIELD_SIZE
241
+ mines = 99
242
+ elif difilculties[selectedDiff] == 'Very hard':
243
+ gameFieldSize = VERY_HARD_FIELD_SIZE
244
+ mines = 150
245
+ setDif()
246
+ gameField[gameFieldSize//2][gameFieldSize//2].isSelected = True
247
+ selectedCell = gameFieldSize//2, gameFieldSize//2
248
+ return True
249
+
250
+ except KeyboardInterrupt:
251
+ os._exit(0)
252
+ except Exception as e:
253
+ print(e)
254
+ os._exit(1)
255
+
256
+ def forceTerminalSize(cols, rows):
257
+ if sys.platform == 'win32':
258
+ os.system(f'mode con: cols={cols} lines={rows}')
259
+
260
+ kernel32 = ctypes.windll.kernel32
261
+ hOut = kernel32.GetStdHandle(-11)
262
+ res = ctypes.wintypes._COORD(cols, rows)
263
+ kernel32.SetConsoleScreenBufferSize(hOut, res)
264
+ else:
265
+ sys.stdout.write(f"\x1b[8;{rows};{cols}t")
266
+ sys.stdout.flush()
267
+
268
+ def checkTerminalSize():
269
+ cols, rows = shutil.get_terminal_size()
270
+ if cols < MIN_TERMINAL_COLS or rows < MIN_TERMINAL_ROWS:
271
+ forceTerminalSize(MIN_TERMINAL_COLS, MIN_TERMINAL_ROWS)
272
+ cols, rows = shutil.get_terminal_size()
273
+ if cols < MIN_TERMINAL_COLS or rows < MIN_TERMINAL_ROWS:
274
+ return False
275
+ return True
276
+
277
+ def placeMines():
278
+ global gameField, mines, gameFieldSize, selectedCell
279
+ placedMines = []
280
+ coordinates = [(y, x) for y in range(gameFieldSize) for x in range(gameFieldSize)]
281
+ coordinates = random.shuffle(coordinates)
282
+ for i in range(mines):
283
+ mine = coordinates[i] if coordinates[i] != selectedCell and coordinates[i] not in placedMines else coordinates[mines + i]
284
+ gameField[mine].isMine = True
285
+ placedMines.append(mine)
286
+
287
+ def checkDefeat():
288
+ global gameField
289
+ for i in gameField:
290
+ for j in i:
291
+ if j.isOpen and j.isMine:
292
+ return True
293
+ return False
294
+
295
+ def checkWin():
296
+ global gameField, mines
297
+ openedCell = 0
298
+ for i in gameField:
299
+ for j in i:
300
+ if j.isOpen and j.isMine:
301
+ return False
302
+ if j.isOpen:
303
+ openedCell += 1
304
+ if openedCell >= (gameField.size - mines):
305
+ return True
306
+ return False
307
+
308
+ def checkNeighbors(tuplePlace):
309
+ global gameField, gameFieldSize
310
+ y, x = tuplePlace
311
+ curCell = gameField[y, x]
312
+ neighborsCells = [(y-1, x), (y-1, x+1), (y, x+1), (y+1, x+1), (y+1, x), (y+1, x-1), (y, x-1), (y-1, x-1)]
313
+ flagedMines = 0
314
+
315
+ if not curCell.isMine:
316
+ if curCell.neighborMines == 0 and not curCell.isMine:
317
+ for i in neighborsCells:
318
+ if (i[0] >= 0 and i[1] >= 0) and (i[0] < gameFieldSize and i[1] < gameFieldSize):
319
+ if gameField[i].isMine:
320
+ curCell.neighborMines += 1
321
+
322
+ for i in neighborsCells:
323
+ if (i[0] >= 0 and i[1] >= 0) and (i[0] < gameFieldSize and i[1] < gameFieldSize):
324
+ if gameField[i].isMine:
325
+ if gameField[i].isFlag:
326
+ flagedMines += 1
327
+
328
+ if curCell.neighborMines == 0:
329
+ for i in neighborsCells:
330
+ if (i[0] >= 0 and i[1] >= 0) and (i[0] < gameFieldSize and i[1] < gameFieldSize):
331
+ if not gameField[i].isMine:
332
+ if gameField[i].openCell() and gameField[i].neighborMines == 0 :
333
+ checkNeighbors(i)
334
+
335
+ if curCell.neighborMines == flagedMines:
336
+ for i in neighborsCells:
337
+ if (i[0] >= 0 and i[1] >= 0) and (i[0] < gameFieldSize and i[1] < gameFieldSize):
338
+ if gameField[i].openCell():
339
+ checkNeighbors(i)
340
+
341
+ def restartProcess():
342
+ global gameField, selectedCell
343
+ for j in gameField:
344
+ for i in j:
345
+ i.isMine = False
346
+ i.isFlag = False
347
+ i.isOpen = False
348
+ i.isSelected = False
349
+ i.neighborMines = 0
350
+ sys.stdout.write('\033[2J\033[H')
351
+ sys.stdout.flush()
352
+ gameField[selectedCell].isSelected = False
353
+ selectedCell = gameFieldSize//2, gameFieldSize//2
354
+ gameField[selectedCell].isSelected = True
355
+ printField()
356
+
357
+ def getTime(gettedTime, getStr=False):
358
+ if gettedTime != 0:
359
+ if not getStr:
360
+ return time.monotonic() - gettedTime
361
+ else:
362
+ return timeToStr(time.monotonic() - gettedTime)
363
+ else:
364
+ return '00:00'
365
+
366
+ def timeToStr(gettedTime):
367
+ minuts = int((gettedTime % 3600) // 60)
368
+ seconds = int(gettedTime % 60)
369
+ return f'{minuts:02d}:{seconds:02d}'
370
+
371
+ def updateView(gameTime):
372
+ while not stop_event.is_set():
373
+ printField(startedTime=gameTime)
374
+ time.sleep(0.1)
375
+
376
+ def flush_input():
377
+ try:
378
+ import termios
379
+ termios.tcflush(sys.stdin, termios.TCIFLUSH)
380
+ except ImportError:
381
+ while msvcrt.kbhit():
382
+ msvcrt.getch()
383
+
384
+ def main():
385
+ global selectedCell, gameField
386
+ moves = 0
387
+ isGameEnd = False
388
+ timeStarted = 0
389
+ try:
390
+ sys.stdout.write('\033[2J\033[H')
391
+ sys.stdout.flush()
392
+ checkTerminalSize()
393
+ selectDificulti()
394
+ sys.stdout.write('\033[2J\033[H')
395
+ sys.stdout.flush()
396
+ printField()
397
+ with keyboard.Events() as events:
398
+ while True:
399
+ event = events.get(1.0)
400
+ if event is None:
401
+ continue
402
+
403
+ if isinstance(event, keyboard.Events.Press):
404
+ key = event.key
405
+
406
+ if not isGameEnd:
407
+ if key == keyboard.Key.up:
408
+ changeSelection('up')
409
+ printField()
410
+ elif key == keyboard.Key.down:
411
+ changeSelection('down')
412
+ printField()
413
+ elif key == keyboard.Key.right:
414
+ changeSelection('right')
415
+ printField()
416
+ elif key == keyboard.Key.left:
417
+ changeSelection('left')
418
+ printField()
419
+
420
+ elif key == keyboard.Key.enter:
421
+ if moves <= 0:
422
+ placeMines()
423
+ timeStarted = time.monotonic()
424
+ timerUpdateThread = threading.Thread(target=updateView, args=(timeStarted,), daemon=True)
425
+ stop_event.clear()
426
+ timerUpdateThread.start()
427
+
428
+ moves += 1
429
+ gameField[selectedCell].openCell()
430
+ checkNeighbors(selectedCell)
431
+
432
+ if checkDefeat():
433
+ printField(isDefeat=True)
434
+ isGameEnd = True
435
+ stop_event.set()
436
+
437
+ elif checkWin():
438
+ printField(isWin=True)
439
+ with open(userStatsPath, 'r', encoding='utf-8') as f:
440
+ records = json.load(f)
441
+
442
+ newTime = getTime(timeStarted)
443
+ if gameFieldSize == EASY_FIELD_SIZE:
444
+ records['easy'] = newTime if records['easy'] > newTime else records['easy']
445
+ elif gameFieldSize == MEDIUM_FIELD_SIZE:
446
+ records['medium'] = newTime if records['medium'] > newTime else records['medium']
447
+ elif gameFieldSize == HARD_FIELD_SIZE:
448
+ records['hard'] = newTime if records['hard'] > newTime else records['hard']
449
+ elif gameFieldSize == VERY_HARD_FIELD_SIZE:
450
+ records['very hard'] = newTime if records['very hard'] > newTime else records['very hard']
451
+
452
+ with open(userStatsPath, 'w') as f:
453
+ json.dump(records, f, ensure_ascii=False, indent=4)
454
+
455
+ isGameEnd = True
456
+ stop_event.set()
457
+
458
+ if (hasattr(key, 'vk') and key.vk == 82) or key == keyboard.KeyCode.from_char('r') or (hasattr(key, 'char') and key.char == 'r'):
459
+ restartProcess()
460
+ timeStarted = 0
461
+ moves = 0
462
+ isGameEnd = False
463
+ stop_event.set()
464
+ printField()
465
+
466
+ elif (hasattr(key, 'vk') and key.vk == 70) or key == keyboard.KeyCode.from_char('f') or (hasattr(key, 'char') and key.char == 'f'):
467
+ gameField[selectedCell].placeFlag()
468
+
469
+ except KeyboardInterrupt:
470
+ flush_input()
471
+ os._exit(0)
472
+
473
+ except Exception as e:
474
+ flush_input()
475
+ print(e)
476
+ os._exit(1)
477
+ finally:
478
+ if sys.platform != 'win32':
479
+ termios.tcsetattr(fd,termios.TCSADRAIN, oldSettings)
480
+ flush_input()
481
+
482
+ if __name__ == "__main__":
483
+ main()
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,24 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ with open("README.md", "r", encoding="utf-8") as fh:
4
+ long_description = fh.read()
5
+
6
+ setup(
7
+ name="FunHub-minesweeper",
8
+ version="1.0.0",
9
+ py_modules=["minesweeper"],
10
+ description="CLI minesweeper game for FunHud app-launcher",
11
+ long_description=open("README.md", encoding="utf-8").read(),
12
+ long_description_content_type="text/markdown",
13
+ packages=find_packages(),
14
+ install_requires=[
15
+ "pynput>=1.7.6",
16
+ "colorama>=0.4.6",
17
+ "numpy>=1.24.0",
18
+ ],
19
+ entry_points={
20
+ 'console_scripts': [
21
+ 'funhub-minesweeper=minesweeper:main',
22
+ ],
23
+ },
24
+ )