Skip to main content
added 80 characters in body
Source Link
cold10
  • 479
  • 3
  • 12
  • Code formating, spacing, and logical breaksAny best practices or performance flaws in my code.
  • Proper separation of code into functions.
  • Proper usage of type hinting in my functions.Proper usage of type hinting in my functions.
  • Any best practices or performance flawsCode formating, spacing, and logical breaks in my code.
  • A better emoji for my mine: currently using 💣 (maybe switch to images?)
  • Code formating, spacing, and logical breaks in my code.
  • Proper separation of code into functions.
  • Proper usage of type hinting in my functions.
  • Any best practices or performance flaws in my code.
  • 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?)
Source Link
cold10
  • 479
  • 3
  • 12

Tkinter Minesweeper Clone V2

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). I decided to test my abilities and rewrote the code.

Please review, thanks!

What I am looking for

  • Code formating, spacing, and logical breaks in my code.
  • Proper separation of code into functions.
  • Proper usage of type hinting in my functions.
  • Any best practices or performance flaws in my code.

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()