minesweeper.py

Created by kbenson8400

Created on January 01, 2026

29.4 KB

Classic minesweeper game! Use arrow keys to move, press “OK” to navigate menu, press “toolbox/paste” to uncover a tile, press “backspace/clear” to flag a tile.


from kandinsky import *
from ion import *
from random import *
from time import *
from math import *

# =====================
# CONFIG
# =====================

SCREEN_WIDTH, SCREEN_HEIGHT = 320, 222

HUD_HEIGHT = 22
GRID_WIDTH, GRID_HEIGHT = 16, 10
TILE_SIZE = 20

MINE_AMOUNT = 25

best_score = -1

# =====================
# INPUT
# =====================

class TapInputKey:
    key_code: int
    is_down: bool

    def __init__(self, key_code):
        self.key_code = key_code
        self.is_down = False
    
    def is_triggered(self) -> bool:
        if keydown(self.key_code):
            if not self.is_down:
                self.is_down = True
                return True
        else:
            self.is_down = False
        
        return False

class RepeatingInputKey:
    key_code: int
    initial_delay: float
    repeat_delay: float

    is_down: bool
    next_time: float

    def __init__(self, key_code, initial_delay, repeat_delay):
        self.key_code = key_code
        self.initial_delay = initial_delay
        self.repeat_delay = repeat_delay
        
        self.is_down = False
        self.next_time: float = 0.0

    def is_triggered(self) -> bool:
        now = monotonic()

        is_triggered = False

        if keydown(self.key_code):
            if not self.is_down:
                # Instant trigger on tap
                is_triggered = True

                self.is_down = True
                self.next_time = now + self.initial_delay
            
            elif now > self.next_time:
                # Repeated trigger
                is_triggered = True

                self.next_time = now + self.repeat_delay

        else:
            self.is_down = False
        
        return is_triggered

class DPadSelector:
    def __init__(self, max_x, max_y):
        self.UP_KEY = RepeatingInputKey(KEY_UP, 0.25, 0.05)
        self.DOWN_KEY = RepeatingInputKey(KEY_DOWN, 0.25, 0.05)
        self.LEFT_KEY = RepeatingInputKey(KEY_LEFT, 0.25, 0.05)
        self.RIGHT_KEY = RepeatingInputKey(KEY_RIGHT, 0.25, 0.05)

        self.x = 0
        self.y = 0
        self.max_x = max_x
        self.max_y = max_y
    
    def update(self):
        dx, dy = 0, 0

        if self.UP_KEY.is_triggered(): dy -= 1
        if self.DOWN_KEY.is_triggered(): dy += 1
        if self.LEFT_KEY.is_triggered(): dx -= 1
        if self.RIGHT_KEY.is_triggered(): dx += 1

        self.x = Util.clamp(self.x + dx, 0, self.max_x)
        self.y = Util.clamp(self.y + dy, 0, self.max_y)

        return (self.x, self.y)

class MinesweeperInputs:
    UNCOVER_KEY = TapInputKey(KEY_TOOLBOX)
    FLAG_KEY = TapInputKey(KEY_BACKSPACE)
    OK_KEY = TapInputKey(KEY_OK)

# =====================
# UTIL
# =====================

class Util:
    @staticmethod
    def clamp(value: int, minv: int, maxv: int) -> int:
        return max(minv, min(value, maxv))

# =====================
# GAME LOGIC
# =====================

class Tile:
    def __init__(self):
        self.is_mined = False
        self.neighboring_mine_count = 0

        self.is_uncovered = False
        self.is_flagged = False
        # Start with true to draw once at beginning
        self.needs_redraw = True

class GameState:
    PLAYING = 0
    WON = 1
    LOST = 2

class MinesweeperBoard:
    width: int
    height: int
    tiles: list[list[Tile]]

    game_state: GameState
    mine_amount: int

    uncovered_tiles_amount: int
    flags_left: int

    is_first_click: bool

    def __init__(self, width, height, mine_amount):
        self.width = width
        self.height = height
        self.tiles = [[Tile() for _ in range(width)] for _ in range(height)]

        self.game_state = GameState.PLAYING
        self.mine_amount = mine_amount

        self.uncovered_tiles_amount = 0
        self.flags_left = mine_amount

        self.is_first_click = True
    
    # --- ACCESS TILES ---

    def is_within_bounds(self, x, y) -> bool:
        within_x = x >= 0 and x < self.width
        within_y = y >= 0 and y < self.height
        return within_x and within_y
   
    def get_tile(self, x, y) -> Tile:
        if self.is_within_bounds(x, y):
            return self.tiles[y][x]
        else:
            return None
        
    # --- GENERATING MINES ---

    def get_neighbors(self, x, y) -> list[tuple[int, int]]:
        adj_pos = [
            (x + 1, y), (x - 1, y), # Left / Right
            (x, y + 1), (x, y - 1), # Top / Bottom
            (x - 1, y + 1), (x - 1, y - 1), # Leftmost corners
            (x + 1, y + 1), (x + 1, y - 1) # Rightmost corners
        ]

        valid_adj_pos = []

        for pos in adj_pos:
            x, y = pos
            if self.is_within_bounds(x, y):
                valid_adj_pos.append(pos)

        return valid_adj_pos

    def generate_mines(self, first_click_x, first_click_y) -> None:
        counter: int = 0

        while counter < self.mine_amount:
            x = randint(0, self.width - 1)
            y = randint(0, self.height - 1)
            tile = self.get_tile(x, y)
           
            if tile.is_mined:
                continue
            
            # Don't place mines around clicked area
            if abs(first_click_x - x) <= 1 and abs(first_click_y - y) <= 1:
                continue

            tile.is_mined = True

            # Increment neighboring mine counter
            for nx, ny in self.get_neighbors(x, y):
                tile = self.get_tile(nx, ny)
                tile.neighboring_mine_count += 1

            counter += 1

    # --- PLAYER ACTIONS ---

    def uncover_tile(self, start_x, start_y):
        tile = self.get_tile(start_x, start_y)

        # Can't uncover
        if tile.is_uncovered or tile.is_flagged:
            return

        # Generate mines on first click
        if self.is_first_click:
            self.generate_mines(start_x, start_y)
            self.is_first_click = False

        queue = [(start_x, start_y)]
        visited = set()

        while queue:
            x, y = queue.pop()

            if (x, y) in visited:
                continue
            visited.add((x, y))

            tile = self.get_tile(x, y)

            if tile.is_uncovered or tile.is_flagged:
                continue

            tile.is_uncovered = True
            tile.needs_redraw = True
            self.uncovered_tiles_amount += 1

            # Lose if mine hit
            if tile.is_mined:
                self.game_state = GameState.LOST
                return

            # Expand only if empty
            if tile.neighboring_mine_count == 0:
                for nx, ny in self.get_neighbors(x, y):
                    queue.append((nx, ny))

        # Win check
        if self.is_game_won():
            self.game_state = GameState.WON

    def flag_tile(self, x, y) -> None:
        tile = self.get_tile(x, y)

        if tile.is_uncovered:
            return

        # Remove flag
        if tile.is_flagged:
            tile.is_flagged = False
            self.flags_left += 1
        
        # Place flag
        else:
            # Can't place more flags
            if self.flags_left == 0:
                return
            tile.is_flagged = True
            self.flags_left -= 1
        
        tile.needs_redraw = True
    
    def reset(self) -> None:
        self.tiles = [[Tile() for _ in range(self.width)] for _ in range(self.height)]
        self.game_state = GameState.PLAYING
        self.uncovered_tiles_amount = 0
        self.flags_left = self.mine_amount
        self.is_first_click = True
    
    def is_game_won(self) -> bool:
        tiles_amount: int = self.width * self.height
        return (tiles_amount - self.mine_amount == self.uncovered_tiles_amount)

# =====================
# RENDERING
# =====================

# --- Util ---

class SpriteLibrary:
    CLOCK_SPRITE = (20, 20, bytes([
        0b00000000, 0b00000000, 0b00000000,
        0b00000000, 0b00000000, 0b00000000,
        0b00000000, 0b11110000, 0b00000000,
        0b00000011, 0b11111100, 0b00000000,
        0b00000111, 0b00001110, 0b00000000,
        0b00001100, 0b00000011, 0b00000000,
        0b00011000, 0b01100001, 0b10000000,
        0b00011000, 0b01100001, 0b10000000,
        0b00110000, 0b01100000, 0b11000000,
        0b00110000, 0b01111110, 0b11000000,
        0b00110000, 0b01111110, 0b11000000,
        0b00110000, 0b00000000, 0b11000000,
        0b00011000, 0b00000001, 0b10000000,
        0b00011000, 0b00000001, 0b10000000,
        0b00001100, 0b00000011, 0b00000000,
        0b00000111, 0b00001110, 0b00000000,
        0b00000011, 0b11111100, 0b00000000,
        0b00111111, 0b11111111, 0b11000000,
        0b00111111, 0b11111111, 0b11000000,
        0b00000000, 0b00000000, 0b00000000
    ]))

    FLAG_SPRITE = (20, 20, bytes([
        0b00000000, 0b00000000, 0b00000000,
        0b00000000, 0b00000000, 0b00000000,
        0b00001100, 0b00000000, 0b00000000,
        0b00001111, 0b10000000, 0b00000000,
        0b00001111, 0b11111000, 0b00000000,
        0b00001111, 0b11111111, 0b00000000,
        0b00001111, 0b11111111, 0b11000000,
        0b00001111, 0b11111111, 0b11000000,
        0b00001111, 0b11111111, 0b00000000,
        0b00001111, 0b11110000, 0b00000000,
        0b00001111, 0b00000000, 0b00000000,
        0b00001100, 0b00000000, 0b00000000,
        0b00001100, 0b00000000, 0b00000000,
        0b00001100, 0b00000000, 0b00000000,
        0b00001100, 0b00000000, 0b00000000,
        0b00001100, 0b00000000, 0b00000000,
        0b00011110, 0b00000000, 0b00000000,
        0b00011110, 0b00000000, 0b00000000,
        0b00000000, 0b00000000, 0b00000000,
        0b00000000, 0b00000000, 0b00000000
    ]))

    MINE_SPRITE = (20, 20, bytes([
        0b00000000, 0b00000000, 0b00000000,
        0b00000000, 0b00000000, 0b00000000,
        0b00000000, 0b11110000, 0b00000000,
        0b00000011, 0b11111100, 0b00000000,
        0b00000111, 0b11111110, 0b00000000,
        0b00001111, 0b11111111, 0b00000000,
        0b00011111, 0b11111111, 0b10000000,
        0b00011111, 0b11111111, 0b10000000,
        0b00111111, 0b11111111, 0b11000000,
        0b00111111, 0b11111111, 0b11000000,
        0b00111111, 0b11111111, 0b11000000,
        0b00111111, 0b11111111, 0b11000000,
        0b00011111, 0b11111111, 0b10000000,
        0b00011111, 0b11111111, 0b10000000,
        0b00001111, 0b11111111, 0b00000000,
        0b00000111, 0b11111110, 0b00000000,
        0b00000011, 0b11111100, 0b00000000,
        0b00000000, 0b11110000, 0b00000000,
        0b00000000, 0b00000000, 0b00000000,
        0b00000000, 0b00000000, 0b00000000
    ]))

    NUMBER_SPRITES = [
    (6, 9, bytes([
        0b01111000,
        0b11001100,
        0b11001100,
        0b11001100,
        0b11001100,
        0b11001100,
        0b11001100,
        0b11001100,
        0b01111000
    ])),
    (6, 9, bytes([
        0b11110000,
        0b00110000,
        0b00110000,
        0b00110000,
        0b00110000,
        0b00110000,
        0b00110000,
        0b00110000,
        0b11111100
    ])),
    (6, 9, bytes([
        0b01111000,
        0b11001100,
        0b00001100,
        0b00001100,
        0b00011000,
        0b00110000,
        0b01100000,
        0b11000000,
        0b11111100
    ])),
    (6, 9, bytes([
        0b01111000,
        0b11001100,
        0b00001100,
        0b00001100,
        0b00111000,
        0b00001100,
        0b00001100,
        0b11001100,
        0b01111000
    ])),
    (6, 9, bytes([
        0b00111100,
        0b00101100,
        0b01101100,
        0b01001100,
        0b11001100,
        0b11111100,
        0b00001100,
        0b00001100,
        0b00001100
    ])),
    (6, 9, bytes([
        0b11111100,
        0b11001100,
        0b11000000,
        0b11000000,
        0b11111000,
        0b00001100,
        0b00001100,
        0b11001100,
        0b01111000
    ])),
    (6, 9, bytes([
        0b01111000,
        0b11001100,
        0b11000000,
        0b11000000,
        0b11111000,
        0b11001100,
        0b11001100,
        0b11001100,
        0b01111000
    ])),
    (6, 9, bytes([
        0b11111100,
        0b11001100,
        0b00001100,
        0b00011000,
        0b00011000,
        0b00110000,
        0b00110000,
        0b01100000,
        0b01100000
    ])),
    (6, 9, bytes([
        0b01111000,
        0b11001100,
        0b11001100,
        0b11001100,
        0b01111000,
        0b11001100,
        0b11001100,
        0b11001100,
        0b01111000
    ])),
    (6, 9, bytes([
        0b01111000,
        0b11001100,
        0b11001100,
        0b11001100,
        0b01111100,
        0b00001100,
        0b00001100,
        0b11001100,
        0b01111000
    ]))]

    MINESWEEPER_SPRITE = (51, 7, bytes([
        0b10001010, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
        0b11011000, 0b11100011, 0b00011010, 0b00100110, 0b00110011, 0b10001100, 0b10100000,
        0b10101010, 0b10010100, 0b10100010, 0b00101001, 0b01001010, 0b01010010, 0b11000000,
        0b10001010, 0b10010111, 0b10010010, 0b10101111, 0b01111010, 0b01011110, 0b10000000,
        0b10001010, 0b10010100, 0b00001010, 0b10101000, 0b01000011, 0b10010000, 0b10000000,
        0b10001010, 0b10010011, 0b00110001, 0b01000110, 0b00110010, 0b00001100, 0b10000000,
        0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000010, 0b00000000, 0b00000000
    ]))

    PLAY_SPRITE = (16, 7, bytes([
        0b11100100, 0b00000000,
        0b10010100, 0b11001001,
        0b10010100, 0b00101001,
        0b11100100, 0b11101001,
        0b10000101, 0b00100111,
        0b10000100, 0b11100001,
        0b00000000, 0b00000110
    ]))

    RESET_SCORE_SPRITE = (48, 7, bytes([
        0b11100000, 0b00000000, 0b00010000, 0b00000000, 0b00000000, 0b00000000,
        0b10010011, 0b00011001, 0b10011000, 0b00011001, 0b11001100, 0b10100110,
        0b10010100, 0b10100010, 0b01010000, 0b00100010, 0b00010010, 0b11001001,
        0b11100111, 0b10010011, 0b11010000, 0b00010010, 0b00010010, 0b10001111,
        0b10010100, 0b00001010, 0b00010000, 0b00001010, 0b00010010, 0b10001000,
        0b10010011, 0b00110001, 0b10001000, 0b00110001, 0b11001100, 0b10000110,
        0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000
    ]))

    QUIT_SPRITE = (15, 7, bytes([
        0b01110000, 0b00010100,
        0b10001010, 0b01000110,
        0b10001010, 0b01010100,
        0b10001010, 0b01010100,
        0b10001010, 0b01010100,
        0b01110001, 0b11010010,
        0b00011000, 0b00000000
    ]))

    ARROW_SPRITE = (7, 7, bytes([
        0b01000000,
        0b01100000,
        0b01110000,
        0b01111000,
        0b01110000,
        0b01100000,
        0b01000000
    ]))

    COLORS = {
        # --- GAME ---
        
        # Tile backgrounds
        "covered_1" : (255, 157, 62), # Darker brown
        "covered_2" : (245, 181, 84), # Lighter brown
        "uncovered_1" : (44, 201, 78), # Darker green
        "uncovered_2" : (141, 227, 93), # Lighter green
        "uncovered_border" : (250, 103, 53), # Maroon?
        
        # Tile numbers
        "num_1" : (76, 137, 238), # Light blue
        "num_2" : (11, 157, 95), # Green
        "num_3" : (232, 35, 55), # Red
        "num_4_and_up" : (105, 29, 140), # Purple

        # Other tile sprites
        "flag" : (232, 35, 55), # Red
        "mine" : (232, 35, 55), # Red
        "selection_border" : (255, 245, 122), # Bright yellow creme

        # --- HUD --- 
        "hud_bg" : (35, 124, 128), # Blue
        "hud_flag" : (232, 35, 55), # Red
        "hud_clock" : (105, 29, 140), # Purple
        "hud_numbers" : (4, 5, 25), # Black

        # --- MENU ---
        "menu_bg" : (245, 181, 84), # Orange
        "menu_text" : (75, 8, 67), # Purple
        "menu_numbers" : (75, 8, 67), # Purple
        "menu_arrow" : (75, 8, 67) # Purple
    }

    @staticmethod
    def draw_sprite(x, y, sprite, color, scale=1):
        width, height, data = sprite
        bytes_per_row = (width + 7) // 8

        for row in range(height):
            row_offset = row * bytes_per_row
            col = 0

            while col < width:
                byte = data[row_offset + (col >> 3)]
                mask = 0x80 >> (col & 7)

                if not (byte & mask):
                    col += 1
                    continue

                run_start = col
                while col < width:
                    byte = data[row_offset + (col >> 3)]
                    if not (byte & (0x80 >> (col & 7))):
                        break
                    col += 1

                run_len = col - run_start

                fill_rect(
                    x + run_start * scale,
                    y + row * scale,
                    run_len * scale,
                    scale,
                    color
                )
    
    @staticmethod
    def erase_sprite(x, y, sprite, bg_color, scale=1):
        width, height, _ = sprite
        fill_rect(x, y, width * scale, height * scale, bg_color)

    @staticmethod
    def draw_digit(x, y, digit, color, scale=1):
        SpriteLibrary.draw_sprite(
            x, y, 
            SpriteLibrary.NUMBER_SPRITES[digit],
            color, scale
        )
    
    @staticmethod
    def erase_digit(x, y, bg_color, scale=1):
        SpriteLibrary.erase_sprite(
            x, y, 
            SpriteLibrary.NUMBER_SPRITES[0],
            bg_color, scale
        )

class NumberDisplayer:
    digits: list[int]

    def __init__(self, x, y, color, bg_color, scale, spacing, max_digits):
        self.x = x
        self.y = y
        self.color = color
        self.bg_color = bg_color
        self.scale = scale
        self.spacing = spacing
        self.max_digits = max_digits
        self.digits =[-1 for _ in range(max_digits)]
    
    def update(self, num):
        if num < 0:
            num = 0

        num_str = str(num)

        while len(num_str) < self.max_digits:
            num_str = "0" + num_str

        for i in range(self.max_digits):
            pos_x = self.x + i * self.spacing
            pos_y = self.y
            new_digit = int(num_str[i])
            if self.digits[i] != new_digit:
                SpriteLibrary.erase_digit(pos_x, pos_y, self.bg_color, self.scale)
                SpriteLibrary.draw_digit(pos_x, pos_y, new_digit, self.color, self.scale)
                self.digits[i] = new_digit

# --- Game + Menu --- 

class MinesweeperDisplay:
    BORDER_WEIGHT: int = 2

    def __init__(self, offset_x, offset_y, tile_size):
        self.offset_x = offset_x
        self.offset_y = offset_y
        self.tile_size = tile_size
    
    # --- HELPERS ---

    def get_bg_color(self, board: MinesweeperBoard, x, y):
        tile = board.get_tile(x, y)
        toggle: bool = (x + y) % 2 == 0

        if tile.is_uncovered:
            if toggle:
                return SpriteLibrary.COLORS["uncovered_1"]
            else:
                return SpriteLibrary.COLORS["uncovered_2"]
        else:
            if toggle:
                return SpriteLibrary.COLORS["covered_1"]
            else:
                return SpriteLibrary.COLORS["covered_2"]

    def get_num_color(self, num):
        if num == 1:
            return SpriteLibrary.COLORS["num_1"]
        elif num == 2:
            return SpriteLibrary.COLORS["num_2"]
        elif num == 3:
            return SpriteLibrary.COLORS["num_3"]
        else:
            return SpriteLibrary.COLORS["num_4_and_up"]

    def get_tile_borders(self, board: MinesweeperBoard, x, y):
        borders = [False, False, False, False]

        up_tile = board.get_tile(x, y - 1)
        down_tile = board.get_tile(x, y + 1)
        left_tile = board.get_tile(x - 1, y)
        right_tile = board.get_tile(x + 1, y)

        if up_tile and not up_tile.is_uncovered:
            borders[0] = True
        if down_tile and not down_tile.is_uncovered:
            borders[1] = True
        if left_tile and not left_tile.is_uncovered:
            borders[2] = True
        if right_tile and not right_tile.is_uncovered:
            borders[3] = True
        
        return borders

    # --- DRAWING TILES ---

    def draw_dirty_tiles(self, board: MinesweeperBoard):
        for y in range(board.height):
            for x in range(board.width):
                tile = board.get_tile(x, y)

                if tile.needs_redraw:
                    self.draw_tile(board, x, y)
                    tile.needs_redraw = False

                    # Redraw neighbors for border updates
                    for nx, ny in board.get_neighbors(x, y):
                        self.draw_tile(board, nx, ny)

    def draw_tile(self, board: MinesweeperBoard, x, y):
        tile = board.get_tile(x, y)
        screen_x = self.offset_x + x * self.tile_size
        screen_y = self.offset_y + y * self.tile_size

        # Draw background
        bg_color = self.get_bg_color(board, x, y)

        fill_rect(
            screen_x, screen_y,
            self.tile_size, self.tile_size,
            bg_color
        )

        # Draw borders
        if tile.is_uncovered:
            borders = self.get_tile_borders(board, x, y)
            border_color = SpriteLibrary.COLORS["uncovered_border"]
            w = self.BORDER_WEIGHT

            if borders[0]:
                fill_rect(screen_x, screen_y, self.tile_size, w, border_color) # Top
            if borders[1]:
                fill_rect(screen_x, screen_y + self.tile_size - w, self.tile_size, w, border_color) # Bottom
            if borders[2]:
                fill_rect(screen_x, screen_y, w, self.tile_size, border_color) # Left
            if borders[3]:
                fill_rect(screen_x + self.tile_size - w, screen_y, w, self.tile_size, border_color) # Right

        # Draw number
        if tile.is_uncovered and tile.neighboring_mine_count > 0 and not tile.is_mined:
            num = tile.neighboring_mine_count
            num_color = self.get_num_color(num)
            num_sprite = SpriteLibrary.NUMBER_SPRITES[num]
            SpriteLibrary.draw_sprite(screen_x + 7, screen_y + 6, num_sprite, num_color, 1)

        # Draw flag
        if tile.is_flagged:
            SpriteLibrary.draw_sprite(screen_x, screen_y, SpriteLibrary.FLAG_SPRITE, SpriteLibrary.COLORS["flag"], 1)
        
        # Draw mine
        if tile.is_uncovered and tile.is_mined:
            SpriteLibrary.draw_sprite(screen_x, screen_y, SpriteLibrary.MINE_SPRITE, SpriteLibrary.COLORS["mine"], 1)

    def draw_selection_border(self, x, y):
        t = self.tile_size
        w = self.BORDER_WEIGHT
        color = SpriteLibrary.COLORS["selection_border"]

        # Convert to screen coords
        x = self.offset_x + x * t
        y = self.offset_y + y * t

        fill_rect(x, y, t, w, color) # Top
        fill_rect(x, y + t - w, t, w, color) # Bottom
        fill_rect(x, y, w, t, color) # Left
        fill_rect(x + t - w, y, w, t, color) # Right

class Hud:
    WIDTH = SCREEN_WIDTH
    HEIGHT = HUD_HEIGHT

    FLAG_SPRITE_POS = (107, 2)
    FLAG_NUM_POS = (130, 8)

    CLOCK_SPRITE_POS = (168, 1)
    CLOCK_NUM_POS = (193, 8)

    NUM_SPACING = 9

    # Draw icons and bg once
    def reset(self):
        fill_rect(
             0, 0, 
            self.WIDTH, self.HEIGHT,
            SpriteLibrary.COLORS["hud_bg"]
        )

        fx, fy = self.FLAG_SPRITE_POS
        SpriteLibrary.draw_sprite(
            fx, fy, SpriteLibrary.FLAG_SPRITE, SpriteLibrary.COLORS["hud_flag"]
        )

        cx, cy = self.CLOCK_SPRITE_POS
        SpriteLibrary.draw_sprite(
            cx, cy, SpriteLibrary.CLOCK_SPRITE, SpriteLibrary.COLORS["hud_clock"]
        )

        fx, fy = self.FLAG_NUM_POS
        self.flags_left_displayer = NumberDisplayer(
            fx, fy, 
            SpriteLibrary.COLORS["hud_numbers"], SpriteLibrary.COLORS["hud_bg"],
            1, self.NUM_SPACING, 2
        )

        cx, cy = self.CLOCK_NUM_POS
        self.time_taken_displayer = NumberDisplayer(
            cx, cy,
            SpriteLibrary.COLORS["hud_numbers"], SpriteLibrary.COLORS["hud_bg"],
            1, self.NUM_SPACING, 3
        )
        
        self.update_flags_left(0)
        self.update_time_taken(0)

    def update_flags_left(self, flags_left: int):
        self.flags_left_displayer.update(flags_left)

    def update_time_taken(self, time_taken: int):
        self.time_taken_displayer.update(time_taken)

class MenuDisplay:
    TITLE_POS = (35, 17)

    BUTTON_1_POS = (134, 76)
    BUTTON_2_POS = (90, 111)
    BUTTON_3_POS = (137, 145)

    ARROW_1_POS = (55, 78)
    ARROW_2_POS = (55, 113)
    ARROW_3_POS = (55, 148)

    BEST_SCORE_POS = (136, 192)
    NUM_SPACING = 18

    def reset(self):
        fill_rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, SpriteLibrary.COLORS["menu_bg"])

        tx, ty = self.TITLE_POS
        SpriteLibrary.draw_sprite(tx, ty, SpriteLibrary.MINESWEEPER_SPRITE, SpriteLibrary.COLORS["menu_text"], 5)
        b1x, b1y = self.BUTTON_1_POS
        SpriteLibrary.draw_sprite(b1x, b1y, SpriteLibrary.PLAY_SPRITE, SpriteLibrary.COLORS["menu_text"], 3)
        b2x, b2y = self.BUTTON_2_POS
        SpriteLibrary.draw_sprite(b2x, b2y, SpriteLibrary.RESET_SCORE_SPRITE, SpriteLibrary.COLORS["menu_text"], 3)
        b3x, b3y = self.BUTTON_3_POS
        SpriteLibrary.draw_sprite(b3x, b3y, SpriteLibrary.QUIT_SPRITE, SpriteLibrary.COLORS["menu_text"], 3)

        self.selector_pos = -1
        self.update_selector_pos(0)

        bsx, bsy = self.BEST_SCORE_POS
        self.best_score_drawer = NumberDisplayer(
            bsx, bsy,
            SpriteLibrary.COLORS["menu_numbers"], SpriteLibrary.COLORS["menu_bg"],
            3, self.NUM_SPACING, 3
        )

    def update_selector_pos(self, pos: int):
        if self.selector_pos != pos:
            x, y = 0, 0
            
            # Erase old
            if self.selector_pos == 0:
                x, y = self.ARROW_1_POS
            elif self.selector_pos == 1:
                x, y = self.ARROW_2_POS
            elif self.selector_pos == 2:
                x, y = self.ARROW_3_POS
            
            fill_rect(x, y, 17, 17, SpriteLibrary.COLORS["menu_bg"])

            # Draw new
            self.selector_pos = pos
            if pos == 0:
                x, y = self.ARROW_1_POS
            elif pos == 1:
                x, y = self.ARROW_2_POS
            elif pos == 2:
                x, y = self.ARROW_3_POS
            
            SpriteLibrary.draw_sprite(x, y, SpriteLibrary.ARROW_SPRITE, SpriteLibrary.COLORS["menu_text"], 2)

    def update_best_score(self, best_score: int):
        self.best_score_drawer.update(best_score)

# =====================
# PROGRAM FLOW
# =====================

class ProgramState:
    GAME = 0
    MENU = 1
    QUIT = 2

class MinesweeperManager:
    selector = DPadSelector(GRID_WIDTH -   1, GRID_HEIGHT - 1)

    board = MinesweeperBoard(GRID_WIDTH, GRID_HEIGHT, MINE_AMOUNT)
    display = MinesweeperDisplay(0, HUD_HEIGHT, TILE_SIZE)

    hud = Hud()

    start_time: float
    time_taken: int

    def reset(self):
        self.start_time = monotonic()
        self.time_taken = 0

        self.board.reset()
        self.display.draw_dirty_tiles(self.board)

        self.selector.x = 0
        self.selector.y = 0

        self.hud.reset()

    def update(self) -> ProgramState:
        now = monotonic()
        self.time_taken = int(now - self.start_time)

        # INPUT
        prev_x, prev_y = self.selector.x, self.selector.y
        self.selector.update()
        x, y = self.selector.x, self.selector.y

        # ACTIONS
        if MinesweeperInputs.FLAG_KEY.is_triggered():
            self.board.flag_tile(x, y)
        
        if MinesweeperInputs.UNCOVER_KEY.is_triggered():
            self.board.uncover_tile(x, y)

        # RENDER
        if x != prev_x or y != prev_y:
            # Erase prev selection border
            self.board.get_tile(prev_x, prev_y).needs_redraw = True
        
        self.display.draw_dirty_tiles(self.board)
        self.display.draw_selection_border(x, y)
        
        # UPDATE HUD
        self.hud.update_flags_left(self.board.flags_left)
        self.hud.update_time_taken(self.time_taken)
        
        # CHECK GAME STATE
        if self.board.game_state == GameState.WON:
            self.win()
            return ProgramState.MENU
        
        elif self.board.game_state == GameState.LOST:
            self.lose()
            return ProgramState.MENU
        
        return ProgramState.GAME
    
    def win(self):
        global best_score
        current_score = self.time_taken

        if best_score == -1 or current_score < best_score:
            best_score = current_score

        sleep(1.0)
    
    def lose(self):
        sleep(1.0)

class MenuManager:
    selector = DPadSelector(0, 2) # 3 options
    menu_display = MenuDisplay()

    def reset(self):
        self.selector.y = 0
        self.menu_display.reset()

        if best_score != -1:
            self.menu_display.update_best_score(best_score)
    
    def update(self) -> ProgramState:
        self.selector.update()

        selector_pos = self.selector.y

        self.menu_display.update_selector_pos(selector_pos)

        if MinesweeperInputs.OK_KEY.is_triggered():
            if selector_pos == 0:
                # Start game
                return ProgramState.GAME

            elif self.selector.y == 1:
                # Reset best score
                global best_score
                best_score = -1
                self.menu_display.update_best_score(best_score)

            elif self.selector.y == 2:
                # Quit
                return ProgramState.QUIT
                
        return ProgramState.MENU

game = MinesweeperManager()

def enter_game():
    game.reset()
    while True:
        result = game.update()
        if result == ProgramState.MENU:
            enter_menu()

menu = MenuManager()

def enter_menu():
    menu.reset()
    while True:
        result = menu.update()
        if result == ProgramState.GAME:
            enter_game()
        elif result == ProgramState.QUIT:
            break

enter_menu()

During your visit to our site, NumWorks needs to install "cookies" or use other technologies to collect data about you in order to:

With the exception of Cookies essential to the operation of the site, NumWorks leaves you the choice: you can accept Cookies for audience measurement by clicking on the "Accept and continue" button, or refuse these Cookies by clicking on the "Continue without accepting" button or by continuing your browsing. You can update your choice at any time by clicking on the link "Manage my cookies" at the bottom of the page. For more information, please consult our cookies policy.