-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.py
543 lines (458 loc) · 24.7 KB
/
main.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
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
from tkinter import messagebox
import tkinter as tk
import random
import struct
import math
import time
CELL_SIZE = 36 # px. scales well with font rn
BG_COLOR = "#222222" # darker gray
UNCLICKED_COLOR = "#d77f37" # orange
CLICKED_COLOR = "#222222" # clicked/empty spaces background
NUMBER_COLORS = "#e3e3e3" # white for numbers
TEMP_BLANK_COLOR = "#aaaaaa" # temporary blank color for visualization
IMPOSSIBLE_COLOR = "#503333" # bright red for impossible moves
class Minesweeper:
def __init__(self, master):
self.master = master
self.start_time = None
self.game_active = False
self.buttons = []
self.scheduled_tasks = [] # Add this line
self.mines = set()
self.revealed = set()
self.flags = set()
self.temp_blanks = set()
self.first_click = True
self.master.configure(bg=BG_COLOR)
self.show_menu()
def recenter_window(self): # fix for higher grid count boards not being centered properly
self.master.update_idletasks() # update the window to get the latest size
window_width = self.master.winfo_width()
window_height = self.master.winfo_height()
screen_width = self.master.winfo_screenwidth()
screen_height = self.master.winfo_screenheight()
center_x = int((screen_width - window_width) / 2)
center_y = int((screen_height - window_height) / 2)
self.master.geometry(f'+{center_x}+{center_y}')
def show_menu(self):
self.menu_frame = tk.Frame(self.master, bg=BG_COLOR)
self.menu_frame.pack(padx=10, pady=10, fill="both", expand=True)
title_label = tk.Label(self.menu_frame, text="Select Difficulty", bg=BG_COLOR, fg=NUMBER_COLORS, font=("Arial", 14, "bold"))
title_label.pack(pady=(0, 20))
modes = [
# name width height mines
("Beginner", 9, 9, 10),
("Intermediate", 16, 16, 40),
("Expert", 30, 16, 99)
]
buttons_frame = tk.Frame(self.menu_frame, bg=BG_COLOR)
buttons_frame.pack()
for index, mode in enumerate(modes):
btn = tk.Button(buttons_frame, text=mode[0], bg=UNCLICKED_COLOR, fg=NUMBER_COLORS,
font=("Arial", 12, "bold"), relief="flat",
command=lambda m=mode: self.start_game(*m[1:]))
btn.grid(row=0, column=index, padx=5, pady=5)
# other title
other_label = tk.Label(self.menu_frame, text="Other", bg=BG_COLOR, fg=NUMBER_COLORS, font=("Arial", 12, "bold"))
other_label.pack(pady=(20, 5))
highscores_btn = tk.Button(self.menu_frame, text="Highscores", bg=UNCLICKED_COLOR, fg=NUMBER_COLORS,
font=("Arial", 12, "bold"), relief="flat",
command=self.show_highscores)
highscores_btn.pack(pady=(5, 5))
def format_time(self, seconds):
"""Converts time in seconds to a formatted string."""
total_seconds = float(seconds)
hours = int(total_seconds) // 3600
minutes = (int(total_seconds) % 3600) // 60
seconds = total_seconds % 60
formatted_time = ""
if hours > 0:
formatted_time += f"{hours}h "
if minutes > 0 or hours > 0: # if hours, include minutes
formatted_time += f"{minutes}m "
formatted_time += f"{seconds:.2f}s"
return formatted_time
def show_highscores(self):
highscores_window = tk.Toplevel(self.master, bg=BG_COLOR)
highscores_window.title("Highscores")
highscores_window.resizable(False, False)
difficulties = {
"Beginner": "9x9 - 10 Mines",
"Intermediate": "16x16 - 40 Mines",
"Expert": "30x16 - 99 Mines"
}
highscores_frame = tk.Frame(highscores_window, bg=BG_COLOR)
highscores_frame.pack(pady=(10, 5))
for index, difficulty in enumerate(difficulties.keys()):
tk.Label(highscores_frame, text=difficulty, bg=BG_COLOR, fg=NUMBER_COLORS, font=("Arial", 16, "bold")).grid(row=0, column=index, padx=20)
records = []
try:
with open("minesweeper.wins", "rb") as file:
while True:
length_bytes = file.read(4) # 4 bytes for unsigned int
if not length_bytes:
break # End of file
length = struct.unpack('I', length_bytes)[0]
mode_bytes = file.read(length)
mode = mode_bytes.decode('utf-8')
time_taken_bytes = file.read(4) # 4 bytes for float
time_taken = struct.unpack('f', time_taken_bytes)[0]
records.append((mode, time_taken))
except FileNotFoundError:
pass
records.sort(key=lambda x: float(x[1]))
records_by_difficulty = {difficulty: [] for difficulty in difficulties.keys()}
for record in records:
for difficulty, format in difficulties.items():
if format in record[0]:
records_by_difficulty[difficulty].append(record)
break
for index, (difficulty, records) in enumerate(records_by_difficulty.items()):
if not records: # Check if there are no records for the difficulty
tk.Label(highscores_frame, text="No scores set!", bg=BG_COLOR, fg=NUMBER_COLORS).grid(row=1, column=index, padx=20)
continue # Skip to the next difficulty
top_records = records[:5]
for row, record in enumerate(top_records, start=1):
formatted_time = self.format_time(record[1])
tk.Label(highscores_frame, text=formatted_time, bg=BG_COLOR, fg=NUMBER_COLORS).grid(row=row, column=index, padx=20)
highscores_window.update_idletasks()
window_width = highscores_window.winfo_width()
window_height = highscores_window.winfo_height()
screen_width = highscores_window.winfo_screenwidth()
screen_height = highscores_window.winfo_screenheight()
# we're moving it 450 pixels to the right here because on a 1920x1080 monitor,
# the highscores interface will be pretty close next to the menu, just makes stuff cleaner :)
center_x = int((screen_width - window_width) / 2) + 450
center_y = int((screen_height - window_height) / 2)
highscores_window.geometry(f'+{center_x}+{center_y}')
def start_game(self, width, height, mines):
global GRID_WIDTH, GRID_HEIGHT, MINES_COUNT
GRID_WIDTH, GRID_HEIGHT, MINES_COUNT = width, height, mines
self.menu_frame.destroy() # remove menu after starting
self.buttons = [[None for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
self.create_widgets()
self.recenter_window() # fix for higher grid count boards not being centered properly
def create_widgets(self):
# Flag Counter Frame and Label
self.info_frame = tk.Frame(self.master, bg=BG_COLOR, height=CELL_SIZE)
self.info_frame.grid(row=0, column=0, columnspan=GRID_WIDTH, sticky="nsew")
self.flag_counter_label = tk.Label(self.info_frame, text=f"Flagged: 0/{MINES_COUNT}", bg=BG_COLOR, fg=NUMBER_COLORS, font=("Arial", int(CELL_SIZE/2.5), "bold"))
self.flag_counter_label.pack(side="left", padx=(10, 0))
# Restart Button
self.restart_button = tk.Button(self.info_frame, text="Restart", bg=UNCLICKED_COLOR, fg=NUMBER_COLORS, font=("Arial", int(CELL_SIZE/2.5), "bold"), relief="flat", command=self.restart_game)
self.restart_button.pack(side="left", expand=True, padx=10) # Added padding for visual separation
# Time Elapsed Label
self.time_elapsed_label = tk.Label(self.info_frame, text="Time: 0s", bg=BG_COLOR, fg=NUMBER_COLORS, font=("Arial", int(CELL_SIZE/2.5), "bold"))
self.time_elapsed_label.pack(side="right", padx=(0, 10))
for row in range(1, GRID_HEIGHT + 1): # grid placement for buttons from row 1
for col in range(GRID_WIDTH):
button = tk.Canvas(self.master, width=CELL_SIZE, height=CELL_SIZE, bg=UNCLICKED_COLOR, highlightthickness=0)
button.grid(row=row, column=col, sticky="nsew")
button.bind("<Button-1>", lambda e, r=row, c=col: self.cell_click(r-1, c, e))
button.bind("<ButtonRelease-1>", lambda e, r=row, c=col: self.hide_temporary_blanks(r-1, c, e))
button.bind("<Button-3>", lambda e, r=row, c=col: self.place_flag(r-1, c))
button.bind("<Enter>", lambda e, r=row, c=col: self.on_hover(e, r-1, c))
button.bind("<Leave>", lambda e, r=row, c=col: self.on_leave(e, r-1, c))
self.buttons[row-1][col] = button
def restart_game(self):
# Cancel all scheduled tasks
for task_id in self.scheduled_tasks:
self.master.after_cancel(task_id)
self.scheduled_tasks.clear() # Clear the list of task IDs
# Clear the current game state
for row in self.buttons:
for button in row:
button.destroy()
self.buttons.clear()
self.mines.clear()
self.revealed.clear()
self.flags.clear()
self.temp_blanks.clear()
self.first_click = True
self.game_active = False
# Start a new game with the same settings
self.start_game(GRID_WIDTH, GRID_HEIGHT, MINES_COUNT)
def on_hover(self, event, row, col):
# if the cell is revealed, flagged, or neither
if (row, col) not in self.revealed:
if (row, col) in self.flags:
# slightly lighter gray for flagged cells on hover
self.buttons[row][col].config(bg="#7e7e7e") # TODO: move to top constants
else:
# slightly lighter color for unclicked cells on hover
self.buttons[row][col].config(bg="#e89b53")
def on_leave(self, event, row, col):
# Reset the color based on whether the cell is flagged or not
if (row, col) not in self.revealed:
if (row, col) in self.flags:
# Reset to the original flag color
self.buttons[row][col].config(bg="#666666") # TODO: move to top constants
else:
# Reset to the original unclicked color
self.buttons[row][col].config(bg=UNCLICKED_COLOR)
def update_time_elapsed(self):
if self.game_active: # only update if the game is active
elapsed_time = int(time.time() - self.start_time)
self.time_elapsed_label.config(text=f"Time: {elapsed_time}s")
self.master.after(100, self.update_time_elapsed)
# ---- DRAWING
def place_flag(self, row, col, event=None):
if self.first_click or (row, col) in self.revealed: # so we can't flag on the first click or unrevealed cells
return
button = self.buttons[row][col]
if (row, col) in self.flags:
self.flags.remove((row, col))
button.delete("flag")
button.config(bg=UNCLICKED_COLOR) # reset if flag removed
self.on_hover(None, row, col)
else:
self.flags.add((row, col))
button.config(bg="#666666") # TODO: move to top constants
self.draw_flag(button)
# manually trigger hor the flag cell
# before, it was staying the main flag gray instead of becoming lighter
self.on_hover(None, row, col)
self.update_flag_counter()
self.update_adjacent_cells_status(row, col)
def update_flag_counter(self, flags=None):
# allows for manually setting flags to x when the user reveals all cells
if flags is None:
flags = len(self.flags)
self.flag_counter_label.config(text=f"Flagged: {flags}/{MINES_COUNT}")
def draw_flag(self, button):
flag_color = BG_COLOR
total_flag_width = CELL_SIZE / 3 # total width of the flag (line + square + rectangle)
flag_height = CELL_SIZE / 3
line_thickness = flag_height / 5
square_side = flag_height / 2
rectangle_height = square_side
rectangle_length = square_side * 0.8
rectangle_y_offset = square_side * 0.3
# positions for centering
flag_x_start = (CELL_SIZE - total_flag_width) / 2
line_y_start = (CELL_SIZE - flag_height) / 2
square_x_start = flag_x_start + line_thickness
rectangle_x_start = square_x_start + square_side
rectangle_y_start = line_y_start + rectangle_y_offset
# flagpole
button.create_rectangle(flag_x_start, line_y_start, flag_x_start + line_thickness, line_y_start + flag_height, tags="flag", fill=flag_color, outline=flag_color)
# right square
button.create_rectangle(square_x_start, line_y_start, square_x_start + square_side, line_y_start + square_side, tags="flag", fill=flag_color, outline=flag_color)
# right downwards rectangle
button.create_rectangle(rectangle_x_start, rectangle_y_start, rectangle_x_start + rectangle_length, rectangle_y_start + rectangle_height, tags="flag", fill=flag_color, outline=flag_color)
def cell_click(self, row, col, event):
if self.first_click:
self.first_click = False
self.start_time = time.time()
self.game_active = True # game starts
self.update_time_elapsed() # start updating time elapsed
self.place_mines(row, col)
self.reveal_cell(row, col)
elif (row, col) not in self.flags and (row, col) not in self.revealed:
if (row, col) in self.mines:
self.game_over(False)
else:
self.reveal_cell(row, col)
if self.check_win():
self.game_over(True)
elif (row, col) in self.revealed:
self.chord_or_show_temp_blanks(row, col)
def chord_or_show_temp_blanks(self, row, col):
num = self.adjacent_mines(row, col)
flags_around = sum((r, c) in self.flags for r in range(max(0, row-1), min(GRID_HEIGHT, row+2)) for c in range(max(0, col-1), min(GRID_WIDTH, col+2)))
if num == flags_around:
for r in range(max(0, row-1), min(GRID_HEIGHT, row+2)):
for c in range(max(0, col-1), min(GRID_WIDTH, col+2)):
if (r, c) not in self.flags:
if (r, c) in self.mines:
self.game_over(False)
return
self.reveal_cell(r, c)
# check for a win after chording
if self.check_win():
self.game_over(True)
elif flags_around > num: # red highlight if more flags are placed around it than the number indicates
self.buttons[row][col].config(bg=IMPOSSIBLE_COLOR)
else:
self.show_temporary_blanks(row, col)
def show_temporary_blanks(self, row, col):
for r in range(max(0, row-1), min(GRID_HEIGHT, row+2)):
for c in range(max(0, col-1), min(GRID_WIDTH, col+2)):
if (r, c) not in self.revealed and (r, c) not in self.flags:
self.buttons[r][c].config(bg=TEMP_BLANK_COLOR)
self.temp_blanks.add((r, c))
def hide_temporary_blanks(self, row, col, event):
for r, c in self.temp_blanks:
self.buttons[r][c].config(bg=UNCLICKED_COLOR)
self.temp_blanks.clear()
def update_adjacent_cells_status(self, row, col):
for r in range(max(0, row-1), min(GRID_HEIGHT, row+2)):
for c in range(max(0, col-1), min(GRID_WIDTH, col+2)):
if (r, c) in self.revealed:
num = self.adjacent_mines(r, c)
flags_around = sum((rr, cc) in self.flags for rr in range(max(0, r-1), min(GRID_HEIGHT, r+2)) for cc in range(max(0, c-1), min(GRID_WIDTH, c+2)))
if flags_around > num:
self.buttons[r][c].config(bg=IMPOSSIBLE_COLOR)
else:
self.buttons[r][c].config(bg=CLICKED_COLOR) # change to normal if it's logical now
def place_mines(self, start_row, start_col):
safe_zone = {(start_row + i, start_col + j) for i in range(-1, 2) for j in range(-1, 2)}
while len(self.mines) < MINES_COUNT:
r, c = random.randint(0, GRID_HEIGHT - 1), random.randint(0, GRID_WIDTH - 1)
if (r, c) not in safe_zone and (r, c) not in self.mines:
self.mines.add((r, c))
def fade_out_cell(self, button, steps, final_color, callback=None):
current_color = button.cget('bg')
r1, g1, b1 = self.master.winfo_rgb(current_color)
r2, g2, b2 = self.master.winfo_rgb(final_color)
delta_r = (r2 - r1) / steps
delta_g = (g2 - g1) / steps
delta_b = (b2 - b1) / steps
def fade(step=0):
nonlocal r1, g1, b1
if step < steps:
r1, g1, b1 = r1 + delta_r, g1 + delta_g, b1 + delta_b
next_color = f'#{int(r1/256):02x}{int(g1/256):02x}{int(b1/256):02x}' # what
button.config(bg=next_color)
self.master.after(25, lambda: fade(step+1)) # schedule next step
else:
if callback:
callback()
fade() # start animation
def interpolate_color(self, start_color, end_color, factor):
"""Interpolates between two colors with a given factor (0 to 1)."""
start_r, start_g, start_b = self.master.winfo_rgb(start_color)
end_r, end_g, end_b = self.master.winfo_rgb(end_color)
r = int(start_r + (end_r - start_r) * factor)
g = int(start_g + (end_g - start_g) * factor)
b = int(start_b + (end_b - start_b) * factor)
return f'#{r>>8:02x}{g>>8:02x}{b>>8:02x}' # what v2
def reveal_cell(self, row, col):
queue = [(row, col)]
while queue:
current_row, current_col = queue.pop(0)
if (current_row, current_col) in self.revealed or (current_row, current_col) in self.flags:
continue
self.revealed.add((current_row, current_col))
button = self.buttons[current_row][current_col]
steps = 10
for i in range(steps + 1):
factor = i / steps
color = self.interpolate_color(UNCLICKED_COLOR, CLICKED_COLOR, factor)
task_id = self.master.after(int(i * 50), lambda b=button, c=color: b.config(bg=c))
self.scheduled_tasks.append(task_id) # Store the task ID
mines_count = self.adjacent_mines(current_row, current_col)
if mines_count == 0:
for r in range(max(0, current_row-1), min(GRID_HEIGHT, current_row+2)):
for c in range(max(0, current_col-1), min(GRID_WIDTH, current_col+2)):
if (r, c) not in self.revealed and (r, c) not in self.flags:
queue.append((r, c))
else:
self.master.after(steps * 10, lambda b=button, mc=mines_count: b.create_text(CELL_SIZE//2, CELL_SIZE//2, text=str(mc), fill=NUMBER_COLORS, font=("Arial", int(CELL_SIZE/2.7), "bold")))
def adjacent_mines(self, row, col):
return sum((r, c) in self.mines for r in range(max(0, row-1), min(GRID_HEIGHT, row+2)) for c in range(max(0, col-1), min(GRID_WIDTH, col+2)))
def check_win(self):
# if all non-mine cells revealed
if len(self.revealed) == GRID_WIDTH * GRID_HEIGHT - MINES_COUNT:
return True
return False
def game_over(self, win):
self.game_active = False # stop time updates
# disable all buttons to prevent further interaction
for row in range(GRID_HEIGHT):
for col in range(GRID_WIDTH):
self.buttons[row][col].unbind("<Button-1>")
self.buttons[row][col].unbind("<Button-3>")
self.buttons[row][col].unbind("<Enter>")
self.buttons[row][col].unbind("<Leave>")
if not win:
# if game is lost, we reveal all the mines and the whole board
# base minesweeper may not show the whole board (i think) but
# it makes it more fun being able to see the whole thing. no harm.
for r, c in self.mines:
if (r, c) not in self.flags: # if mine was flagged
button = self.buttons[r][c]
button.config(bg=UNCLICKED_COLOR)
self.draw_mine(button) # if it wasn't flagged, draw a mine in the place
# if it was flagged, it's already indicated as such, so we don't change it
for row in range(GRID_HEIGHT):
for col in range(GRID_WIDTH):
if (row, col) not in self.mines:
self.reveal_cell(row, col) # show numbers on non-mine cells
else:
# if we won without flags, flag all unflagged mines
for r, c in self.mines:
if (r, c) not in self.flags:
button = self.buttons[r][c]
button.config(bg="#666666") # TODO: move to top constants
self.draw_flag(button)
self.update_flag_counter(MINES_COUNT) # update flag counter to x/x
if win:
end_time = time.time()
time_taken = end_time - self.start_time
self.store_win_record(time_taken)
# see, i want to move away from messagebox but i'm not sure what a better
# way to give info to the user is
# maybe i could put something in the top titlebar instead
# then make it return to the menu on confirmation so we have a loop
# for now this works
messagebox.showinfo("Minesweeper", "Congratulations! You've won!")
else:
messagebox.showinfo("Minesweeper", "Game Over! You hit a mine.")
self.master.destroy()
def store_win_record(self, time_taken):
# the only real reason i'm using struct here is because this project was
# primarily made for some friends. and i know for a fact they'd end up
# trying to change their own records smh
mode = f"{GRID_WIDTH}x{GRID_HEIGHT} - {MINES_COUNT} Mines"
mode_encoded = mode.encode('utf-8') # encode the mode string as bytes
record = struct.pack('I', len(mode_encoded)) + mode_encoded + struct.pack('f', time_taken)
# 'I' is for unsigned int (length of the mode string), 'f' is for float (time_taken)
with open("minesweeper.wins", "ab") as file:
file.write(record)
def draw_mine(self, button):
outer_circle_radius = CELL_SIZE * 0.2 # main mine body
inner_circle_radius = CELL_SIZE * 0.07 # the inner circle of the same color as the cell
leg_size = CELL_SIZE * 0.1 # mines legs
# Draw outer circle
button.create_oval(
CELL_SIZE/2 - outer_circle_radius, CELL_SIZE/2 - outer_circle_radius,
CELL_SIZE/2 + outer_circle_radius, CELL_SIZE/2 + outer_circle_radius,
fill=BG_COLOR, outline=BG_COLOR
)
# Draw inner circle
button.create_oval(
CELL_SIZE/2 - inner_circle_radius, CELL_SIZE/2 - inner_circle_radius,
CELL_SIZE/2 + inner_circle_radius, CELL_SIZE/2 + inner_circle_radius,
fill=UNCLICKED_COLOR, outline=UNCLICKED_COLOR
)
# calculate and draw legs at every 45 degrees around the outer circle
for angle in range(0, 360, 45):
radian = angle * (3.141592653589793 / 180) # angle -> radians
# getting the center position for each leg
x_center = CELL_SIZE/2 + (outer_circle_radius + leg_size/2) * math.cos(radian)
y_center = CELL_SIZE/2 + (outer_circle_radius + leg_size/2) * math.sin(radian)
# getting the top-left corner based on the center position
x = x_center - leg_size/2
y = y_center - leg_size/2
button.create_rectangle(
x, y, x + leg_size, y + leg_size,
fill=BG_COLOR, outline=BG_COLOR
)
def main():
root = tk.Tk()
root.title("Minesweeper")
root.resizable(False, False) # non-resizable because no point having it resizable (that i see)
# update so we can get size
root.update_idletasks()
window_width = root.winfo_width()
window_height = root.winfo_height()
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
center_x = int((screen_width - window_width) / 2)
center_y = int((screen_height - window_height) / 2)
root.geometry(f'+{center_x}+{center_y}')
game = Minesweeper(root)
root.mainloop()
if __name__ == "__main__":
main()