# Intro # Having spent quite a bit of time learning Python, I thought it was time to work on something. However, an Google search for "python projects 2022" came up with projects I had already completed. Looking through my projects, however, I came across this. ([link](https://codereview.stackexchange.com/q/277696/241327)). I decided to test my abilities and rewrote the code. Please review, thanks! ## What I am looking for ## * Any best practices or performance flaws in my code. * Proper separation of code into functions. * **Proper usage of type hinting in my functions.** * Code formating, spacing, and logical breaks in my code. * **A better emoji for my mine: currently using 💣 (maybe switch to images?)** ## The code ## ``` import random import tkinter as tk import tkinter.messagebox as msgbox from typing import List, Literal, Tuple # TODO # improve astethics and change colors # add colors of numbers class Main: def __init__(self): self.status_bar_on = True self.game_started = False self.gameover = False self.cols = 30 self.rows = 16 self.flags = 80 self.original_flags = self.flags self.time = 0 self.widget_board = [[0 for _ in range(self.cols)] for _ in range(self.rows)] self.setupTkinter() self.generateBoard() def run(self) -> Literal[None]: '''Run the GUI application: call tk.Tk.mainloop.''' self.window.after(200, self.updateTimer) self.window.mainloop() def gameOver(self, won: bool, bad: bool = False) -> Literal[None]: ''' GUI game over function. Called by Main.checkWon. Shows a popup message and also displays all mines: self.showBombs. ''' self.showBombs() if bad: msgbox.showinfo(title = 'Minesweeper', message = f'You lost!') self.gameover = True return if won: msgbox.showinfo(title = 'Minesweeper', message = f'You won in {self.timer} seconds!') self.gameover = True else: self.window.after(100, self.gameOver(won, bad = True)) def checkWon(self) -> Literal[None]: ''' Checks the board for a wins. Uses the following formula: (amount of opened cells) + (mines) == (total tiles) to check. Calls Main.gameOver when a win is detected. ''' opened_cells = 0 for widget in self.board_frame.winfo_children(): if widget.cget('text').isnumeric(): opened_cells += 1 if (self.rows * self.cols) == opened_cells + self.original_flags: self.gameOver(won = True) def showBombs(self) -> Literal[None]: ''' Iterates through the game frame's widgets: (winfo_children) and finds widgets that correspond to mines. When one is found, if it was correctly marked with a flag, it is marked with a green background. Otherwise, with a red background. ''' for row_index, row in enumerate(self.game_board): for item_index, item in enumerate(row): if item == '\N{BOMB}': widget = self.widget_board[row_index][item_index] if widget.cget('text') == ' ': widget.config(text = '\N{BOMB}', bg = 'red') elif widget.cget('text') == '\u2691': widget.config(bg = 'dark green') for widget in self.board_frame.winfo_children(): if widget.cget('text') == '\u2691': if widget.cget('bg') != 'dark green': widget.config(bg = 'red') def setupTkinter(self) -> Literal[None]: ''' An encapsulating function with Tkinter setup and layouting. Generates the main window along with the status widgets, and frames. ''' self.window = tk.Tk() self.window.geometry('1125x590') self.window.title('Minesweeper') self.window.config(bg = 'blue') self.status_bar = tk.Frame(self.window, height = 50) self.status_bar.config(bg = 'blue') self.board_frame = tk.Frame(self.window) self.board_frame.config(bg = 'black') self.time_variable = tk.StringVar(self.status_bar, value = '⌛ 0') self.flags_variable = tk.StringVar(self.status_bar, value = f'🚩 {self.flags}') self.time_label = tk.Label(self.status_bar, textvariable = self.time_variable, font = ('Courier', 30), bg = 'blue') self.flags_label = tk.Label(self.status_bar, textvariable = self.flags_variable, font = ('Courier', 30), bg = 'blue') self.generateBoardWidgets(self.board_frame) if self.status_bar_on: self.status_bar.pack(fill = tk.BOTH, expand = tk.NO, pady = 5) self.board_frame.pack(fill = tk.BOTH, expand = tk.YES) self.time_label.pack(side = tk.LEFT, padx = 5, expand = tk.YES) self.flags_label.pack(side = tk.RIGHT, padx = 5, expand = tk.YES) def generateBoardWidgets(self, board_frame: tk.Frame) -> Literal[None]: ''' Generates the tk.Label widgets in the board frame (board_frame). Iterates through required rows and columns to generate tk.Label widgets. Also binds the three buttons to the respective functions. ''' for row in range(self.rows): for col in range(self.cols): widget = tk.Label( board_frame, text = ' ', fg = 'white', width = 1, font = ('Courier', 25), bg = 'gray') widget.bind('<Button-1>', self.handleLeftClick) widget.bind('<Button-2>', self.handleRightClick) widget.bind('<Button-3>', self.handleMiddleClick) widget.grid(row = row, column = col, padx = 2, pady = 2, sticky = tk.NSEW) tk.Grid.rowconfigure(board_frame, row, weight=1) tk.Grid.columnconfigure(board_frame, col, weight=1) self.widget_board[row][col] = widget def floodfill(self, location, first = False) -> Literal[None]: ''' Flood fills the tiles where an 0 is clicked on. This opens all connected groups of zero, and a border around this. Uses the neighbors from Main.getNeighbors and calls openNeighbors to actually open the tiles. ''' if first: self.stack = [] neighbors = self.getNeighbors(*location, [self.rows, self.cols]) self.openNeighbors(*location) for neighbor_loc in neighbors: if neighbor_loc in self.stack: continue item = self.game_board[neighbor_loc[0]][neighbor_loc[1]] if item == 0: self.stack.append(neighbor_loc) self.floodfill(neighbor_loc) def openNeighbors(self, y, x) -> Literal[None]: ''' Opens the neighbors of an tile. Used by the middle click function (Main.handleMiddleClick) and the floodfill (Main.floodfill) ''' neighbors = self.getNeighbors(y, x, [self.rows, self.cols]) for neighbor_location in neighbors: item = self.game_board[neighbor_location[0]][neighbor_location[1]] widget = self.widget_board[neighbor_location[0]][neighbor_location[1]] if item == '\N{BOMB}' and widget.cget('text') != '\u2691': self.gameOver(won = False) if widget.cget('text') != ' ': continue widget.config(text = str(item)) if item == 0: self.floodfill(neighbor_location, first = True) def handleLeftClick(self, event = None) -> Literal[None]: ''' Tkinter event handling for left mouse button. Will regenerate boards on the first move if the first move is on a mine. Also floodfills the first move. ''' if self.gameover or event.widget.cget('text') != ' ': return widget = event.widget row, col = widget.grid_info()['row'], widget.grid_info()['column'] game_board_item = self.game_board[row][col] if not self.game_started: while game_board_item != 0: self.generateBoard() game_board_item = self.game_board[row][col] self.floodfill([row, col], first = True) widget.config(text = str(game_board_item)) if type(game_board_item) != int: self.gameOver(won = False) self.game_started = True self.checkWon() def handleMiddleClick(self, event = None) -> Literal[None]: ''' Tkinter event handling for middle click (mouse). Calls Main.openNeighbors. ''' if self.gameover: return widget = event.widget row, col = widget.grid_info()['row'], widget.grid_info()['column'] self.openNeighbors(row, col) def handleRightClick(self, event = None) -> Literal[None]: ''' Tkinter event handling for right mouse button. Toggles an flag on the tile if it is not opened. ''' if self.gameover or event.widget.cget('text') not in (' ', '\u2691') or self.flags <= 0: return widget = event.widget if event.widget.cget('text') == '\u2691': widget.config(text = ' ') self.flags += 1 else: widget.config(text = '\u2691') self.flags -= 1 self.flags_variable.set(f'🚩 {self.flags}') def updateTimer(self) -> Literal[None]: ''' An function that calls itself to update an timer. Stops once the game is over, and starts on the first click. ''' if self.gameover: return if self.game_started: self.time += 1 self.time_variable.set(f'⌛ {self.time}') self.window.after(1000, self.updateTimer) def generateBoard(self) -> Literal[None]: ''' Generates the Minesweeper game board. Places mines, and updates the numbers of neighbors by +1. Can be called multiple times to regenerate boards. ''' self.game_board = [[0 for _ in range(self.cols)] for _ in range(self.rows)] all_locations = [] for x in range(16): for y in range(30): all_locations.append((x, y)) sample = random.sample(all_locations, self.flags) for location in sample: self.setBomb(*location) def setBomb(self, y, x) -> Literal[None]: ''' Sets a spot on the game board to a bomb and increments the neighboring values. ''' self.game_board[y][x] = '\N{BOMB}' neighbors = self.getNeighbors(y, x, [self.rows, self.cols]) for neighbor_location in neighbors: item = self.game_board[neighbor_location[0]][neighbor_location[1]] if type(item) == int: self.game_board[neighbor_location[0]][neighbor_location[1]] += 1 def getNeighbors(self, y, x, board_shape) -> List[Tuple[int, int]]: ''' Returns a list of the indexes of neighboring tiles. (diagonals included) ''' neighbors = list() neighbors.append((y, x - 1)) neighbors.append((y, x + 1)) neighbors.append((y - 1, x)) neighbors.append((y - 1, x - 1)) neighbors.append((y - 1, x + 1)) neighbors.append((y + 1, x)) neighbors.append((y + 1, x + 1)) neighbors.append((y + 1, x - 1)) neighbors = [i for i in neighbors if i[0] in range(board_shape[0]) and i[1] in range(board_shape[1])] return neighbors if __name__ == '__main__': app = Main() app.run()