From 58e89f5b6eec897a5bdb36f48c088e7f1fd7b1c7 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Wed, 16 Apr 2025 00:03:32 -0700 Subject: [PATCH 1/5] Add minesweeper code --- .../bitmaps/game_sprites.bmp | Bin 0 -> 4340 bytes .../bitmaps/mouse_cursor.bmp | Bin 0 -> 128 bytes Metro/Metro_RP2350_Minesweeper/code.py | 305 ++++++++++++++++++ Metro/Metro_RP2350_Minesweeper/eventbutton.py | 40 +++ Metro/Metro_RP2350_Minesweeper/gamelogic.py | 263 +++++++++++++++ Metro/Metro_RP2350_Minesweeper/menu.py | 152 +++++++++ 6 files changed, 760 insertions(+) create mode 100755 Metro/Metro_RP2350_Minesweeper/bitmaps/game_sprites.bmp create mode 100755 Metro/Metro_RP2350_Minesweeper/bitmaps/mouse_cursor.bmp create mode 100755 Metro/Metro_RP2350_Minesweeper/code.py create mode 100755 Metro/Metro_RP2350_Minesweeper/eventbutton.py create mode 100755 Metro/Metro_RP2350_Minesweeper/gamelogic.py create mode 100755 Metro/Metro_RP2350_Minesweeper/menu.py diff --git a/Metro/Metro_RP2350_Minesweeper/bitmaps/game_sprites.bmp b/Metro/Metro_RP2350_Minesweeper/bitmaps/game_sprites.bmp new file mode 100755 index 0000000000000000000000000000000000000000..57f286254845f20a8c662687229c8c518cb2d252 GIT binary patch literal 4340 zcmeHJJC55h5M?4D3g9LP;38eBMF2s_9a6c;ZQQx>L39$N9XOA*J0PKYxEpCciL#!*j;-70k$*yhXE{w?Erkc-O}Z9L5MTrpDgdbtE68_w}gG)ET#6Z@yfrZt8l6uY8?En zr_py!!w@Y8EQ8-LwpRYMn@QNmR!Xtr-1jwJ^D6=JF_Vaw`Kx$Y?*_!tVlviz#I1{2 zqVBe_4a`kLf9z1K5yY)LtS26?6&;vJ0`U#z8}z1ogkN+d6u%PCIaqv4M+{s1%EJ2P zNol&ruMW@yHmpCy`I!lSS)=rxdK}cin&2BX-W;3IG$!GEqvv zzNsnfW#jduyY`ZA@iz_fK@M{G&?jwT!RD}i|b!E1#1`?GrgSN!H628<%t(cJC7K5epO(GcEv%3r^~_Wfgiq*IMt zg;>7=VBX+&3;2t6#xIqH$1>stuH%K?17`cc_@gv3s@yi=!W;2chEcfBtNi}!7ae#?e?CY^48AW}gyZVjCn@K%+wI)`CB$*JcL|Ky z-^cR4P)D(N5ArjC*Z5zg5BmMS*uT!-b;tn5vS;}M!oEfu=D5`FdUQd0@N-L3lgJ?8;9@~e)_>a zpTA>H`3B~Ew5$Hx{nJL#>lPGT`j6_Z3-o|q2QPQ$cObn3HK4@PwlEXO*stF+`B(R! z?YFl8`RVXEMm=nRH6iNvDMm}0G|GHumAu6 literal 0 HcmV?d00001 diff --git a/Metro/Metro_RP2350_Minesweeper/bitmaps/mouse_cursor.bmp b/Metro/Metro_RP2350_Minesweeper/bitmaps/mouse_cursor.bmp new file mode 100755 index 0000000000000000000000000000000000000000..f941ad29b61157d4cdaee944c1342b47940bb15a GIT binary patch literal 128 zcmXAgF%E!02n6x8uru5X02?cxU~#>F^5^FemJGWj%XmL24eWv)4p?Mki#%}N#Y#?Q e&YYxFGZF8JHMIcebnW4)9gNIv)uJ7IVdw|;R10MQ literal 0 HcmV?d00001 diff --git a/Metro/Metro_RP2350_Minesweeper/code.py b/Metro/Metro_RP2350_Minesweeper/code.py new file mode 100755 index 000000000..f12024287 --- /dev/null +++ b/Metro/Metro_RP2350_Minesweeper/code.py @@ -0,0 +1,305 @@ +# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams for Adafruit Industries +# SPDX-License-Identifier: MIT +""" +An implementation of minesweeper. The logic game where the player +correctly identifies the locations of mines on a grid by clicking on squares +and revealing the number of mines in adjacent squares. + +The player can also flag squares they suspect contain mines. The game ends when +the player successfully reveals all squares without mines or clicks on a mine. +""" +import array +import time +from displayio import Group, OnDiskBitmap, TileGrid, Bitmap, Palette +from adafruit_display_text.bitmap_label import Label +from adafruit_display_text.text_box import TextBox +from eventbutton import EventButton +import supervisor +import terminalio +import usb.core +from gamelogic import GameLogic, BLANK, INFO_BAR_HEIGHT, DIFFICULTIES +from menu import Menu, SubMenu + +# pylint: disable=ungrouped-imports +if hasattr(supervisor.runtime, "display") and supervisor.runtime.display is not None: + # use the built-in HSTX display for Metro RP2350 + display = supervisor.runtime.display +else: + # pylint: disable=ungrouped-imports + from displayio import release_displays + import picodvi + import board + import framebufferio + + # initialize display + release_displays() + + fb = picodvi.Framebuffer( + 320, + 240, + clk_dp=board.CKP, + clk_dn=board.CKN, + red_dp=board.D0P, + red_dn=board.D0N, + green_dp=board.D1P, + green_dn=board.D1N, + blue_dp=board.D2P, + blue_dn=board.D2N, + color_depth=16, + ) + display = framebufferio.FramebufferDisplay(fb) + +game_logic = GameLogic(display) + +# Load the spritesheet +sprite_sheet = OnDiskBitmap("/bitmaps/game_sprites.bmp") + +# Main group will hold all the visual layers +main_group = Group() +display.root_group = main_group + +# Add Background to the Main Group +background = Bitmap(display.width, display.height, 1) +bg_color = Palette(1) +bg_color[0] = 0xaaaaaa +main_group.append(TileGrid( + background, + pixel_shader=bg_color +)) + +# Add Game group, which holds the game board, to the main group +game_group = Group() +main_group.append(game_group) + +# Add a group for the UI Elements +ui_group = Group() +main_group.append(ui_group) + +# Create the mouse graphics and add to the main group +mouse_bmp = OnDiskBitmap("/bitmaps/mouse_cursor.bmp") +mouse_bmp.pixel_shader.make_transparent(0) +mouse_tg = TileGrid(mouse_bmp, pixel_shader=mouse_bmp.pixel_shader) +mouse_tg.x = display.width // 2 +mouse_tg.y = display.height // 2 +main_group.append(mouse_tg) + +MENU_ITEM_HEIGHT = INFO_BAR_HEIGHT + +def create_game_board(): + # Remove the old game board + if len(game_group) > 0: + game_group.pop() + + x = display.width // 2 - (game_logic.grid_width * 16) // 2 + y = ((display.height - INFO_BAR_HEIGHT) // 2 - + (game_logic.grid_height * 16) // 2 + INFO_BAR_HEIGHT) + + # Create a new game board + game_board = TileGrid( + sprite_sheet, + pixel_shader=sprite_sheet.pixel_shader, + width=game_logic.grid_width, + height=game_logic.grid_height, + tile_height=16, + tile_width=16, + x=x, + y=y, + default_tile=BLANK, + ) + + game_group.append(game_board) + return game_board + +def update_ui(): + # Update the UI elements with the current game state + mines_left_label.text = f"Mines: {game_logic.mines_left}" + elapsed_time_label.text = f"Time: {game_logic.elapsed_time}" + +# variable for the mouse USB device instance +mouse = None + +# wait a second for USB devices to be ready +time.sleep(1) + +# scan for connected USB devices +for device in usb.core.find(find_all=True): + # print information about the found devices + print(f"{device.idVendor:04x}:{device.idProduct:04x}") + print(device.manufacturer, device.product) + print(device.serial_number) + + # assume this device is the mouse + mouse = device + + # detach from kernel driver if active + if mouse.is_kernel_driver_active(0): + mouse.detach_kernel_driver(0) + + # set the mouse configuration so it can be used + mouse.set_configuration() + +buf = array.array("b", [0] * 4) +waiting_for_release = False +left_button = right_button = False +mouse_coords = (0, 0) + +# Create the UI Elements (Ideally fit into 320x16 area) +# Label for the Mines Left (Left of Center) +mines_left_label = Label( + terminalio.FONT, + color=0x000000, + x=5, + y=0, +) +mines_left_label.anchor_point = (0, 0) +mines_left_label.anchored_position = (5, 2) +ui_group.append(mines_left_label) +# Label for the Elapsed Time (Right of Center) +elapsed_time_label = Label( + terminalio.FONT, + color=0x000000, + x=display.width - 50, + y=0, +) +elapsed_time_label.anchor_point = (1, 0) +elapsed_time_label.anchored_position = (display.width - 5, 2) +ui_group.append(elapsed_time_label) + +# Menu button to change difficulty +difficulty_menu = SubMenu( + "Difficulty", + 70, + 80, + display.width // 2 - 70, + 0 +) + +reset_menu = SubMenu( + "Reset", + 50, + 40, + display.width // 2 + 15, + 0 +) + +message_dialog = Group() +message_dialog.hidden = True + +def reset(): + # Reset the game logic + game_logic.reset() + + # Create a new game board and assign it into the game logic + game_logic.game_board = create_game_board() + + message_dialog.hidden = True + +def set_difficulty(diff): + game_logic.difficulty = diff + reset() + +def hide_group(group): + print("Hiding") + group.hidden = True + +for i, difficulty in enumerate(DIFFICULTIES): + # Create a button for each difficulty + difficulty_menu.add_item((set_difficulty, i), difficulty['label']) + +reset_menu.add_item(reset, "OK") + +menu = Menu() +menu.append(difficulty_menu) +menu.append(reset_menu) +ui_group.append(menu) + +reset() + +message_label = TextBox( + terminalio.FONT, + text="", + color=0x333333, + background_color=0xEEEEEE, + width=display.width // 4, + height=50, + align=TextBox.ALIGN_CENTER, + padding_top=5, +) +message_label.anchor_point = (0, 0) +message_label.anchored_position = ( + display.width // 2 - message_label.width // 2, + display.height // 2 - message_label.height // 2, +) +message_dialog.append(message_label) +message_button = EventButton( + (hide_group, message_dialog), + label="OK", + width=40, + height=16, + x=display.width // 2 - 20, + y=display.height // 2 - message_label.height // 2 + 20, + style=EventButton.RECT, +) +message_dialog.append(message_button) +ui_group.append(message_dialog) + +# Popup message for game over/win + +menus = (reset_menu, difficulty_menu) + +# main loop +while True: + update_ui() + # attempt mouse read + try: + # try to read data from the mouse, small timeout so the code will move on + # quickly if there is no data + data_len = mouse.read(0x81, buf, timeout=10) + left_button = buf[0] & 0x01 + right_button = buf[0] & 0x02 + + # if there was data, then update the mouse cursor on the display + # using min and max to keep it within the bounds of the display + mouse_tg.x = max(0, min(display.width - 1, mouse_tg.x + buf[1] // 2)) + mouse_tg.y = max(0, min(display.height - 1, mouse_tg.y + buf[2] // 2)) + mouse_coords = (mouse_tg.x, mouse_tg.y) + + if waiting_for_release and not left_button and not right_button: + # If both buttons are released, we can process the next click + waiting_for_release = False + + # timeout error is raised if no data was read within the allotted timeout + except usb.core.USBTimeoutError: + # no problem, just go on + pass + except AttributeError as exc: + raise RuntimeError("Mouse not found") from exc + if not message_dialog.hidden: + if message_button.handle_mouse(mouse_coords, left_button, waiting_for_release): + waiting_for_release = True + continue + + if menu.handle_mouse(mouse_coords, left_button, waiting_for_release): + waiting_for_release = True + else: + # process gameboard click if no menu + ms_board = game_logic.game_board + if (ms_board.x <= mouse_tg.x <= ms_board.x + game_logic.grid_width * 16 and + ms_board.y <= mouse_tg.y <= ms_board.y + game_logic.grid_height * 16 and + not waiting_for_release): + coords = ((mouse_tg.x - ms_board.x) // 16, (mouse_tg.y - ms_board.y) // 16) + if right_button: + game_logic.square_flagged(coords) + elif left_button: + if not game_logic.square_clicked(coords): + message_label.text = "Game Over" + message_dialog.hidden = False + if left_button or right_button: + waiting_for_release = True + status = game_logic.check_for_win() + if status: + message_label.text = "You win!" + message_dialog.hidden = False + # Display message + if status is None: + continue diff --git a/Metro/Metro_RP2350_Minesweeper/eventbutton.py b/Metro/Metro_RP2350_Minesweeper/eventbutton.py new file mode 100755 index 000000000..c50eea710 --- /dev/null +++ b/Metro/Metro_RP2350_Minesweeper/eventbutton.py @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams for Adafruit Industries +# SPDX-License-Identifier: MIT + +from adafruit_button import Button + +class EventButton(Button): + """A button that can be used to trigger a callback when clicked. + + :param callback: The callback function to call when the button is clicked. + A tuple can be passed with an argument that will be passed to the + callback function. The first element of the tuple should be the + callback function, and the remaining elements will be passed as + arguments to the callback function. + """ + def __init__(self, callback, *args, **kwargs): + super().__init__(*args, **kwargs) + self.args = [] + if isinstance(callback, tuple): + self.callback = callback[0] + self.args = callback[1:] + else: + self.callback = callback + + def click(self): + """Call the function when the button is pressed.""" + self.callback(*self.args) + + def handle_mouse(self, point, clicked, waiting_for_release): + if waiting_for_release: + return False + + # Handle mouse events for the button + if self.contains(point): + super().selected = True + if clicked: + self.click() + return True + else: + super().selected = False + return False diff --git a/Metro/Metro_RP2350_Minesweeper/gamelogic.py b/Metro/Metro_RP2350_Minesweeper/gamelogic.py new file mode 100755 index 000000000..61a90b494 --- /dev/null +++ b/Metro/Metro_RP2350_Minesweeper/gamelogic.py @@ -0,0 +1,263 @@ +# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams for Adafruit Industries +# SPDX-License-Identifier: MIT + +import random +from microcontroller import nvm +from adafruit_ticks import ticks_ms +from displayio import TileGrid + +# Mine Densities are about the same as the original +DIFFICULTIES = ( + { + 'label': "Beginner", + 'grid_size': (8,8), + 'mines': 10, + }, + { + 'label': "Intermediate", + 'grid_size': (14, 14), + 'mines': 30, + }, + { + 'label': "Expert", + 'grid_size': (20, 14), + 'mines': 58, + }, +) + +INFO_BAR_HEIGHT = 16 + +OPEN = 0 +OPEN1 = 1 +OPEN2 = 2 +OPEN3 = 3 +OPEN4 = 4 +OPEN5 = 5 +OPEN6 = 6 +OPEN7 = 7 +OPEN8 = 8 + +BLANK = 9 +FLAG = 10 +MINE_CLICKED = 11 +MINE_FLAGGED_WRONG = 12 +MINE = 13 +MINE_QUESTION = 14 +MINE_QUESTION_OPEN = 15 + +STATUS_NEWGAME = 0 +STATUS_PLAYING = 1 +STATUS_WON = 2 +STATUS_LOST = 3 + +class GameLogic: + def __init__(self, display): + self._board_data = bytearray() + self.game_board = None + self._difficulty = nvm[0] + if self._difficulty not in DIFFICULTIES: + self._difficulty = 0 + self._display = display + self._start_time = None + self._end_time = None + self._mine_count = 0 + self._status = STATUS_NEWGAME + self.reset() + + def reset(self): + if (self.grid_width * 16 > self._display.width or + self.grid_height * 16 > self._display.height - INFO_BAR_HEIGHT): + raise ValueError("Grid size exceeds display size") + self._board_data = bytearray(self.grid_width * self.grid_height) + self._mine_count = DIFFICULTIES[self._difficulty]['mines'] + self._status = STATUS_NEWGAME + self._start_time = None + self._end_time = None + + def _seed_mines(self, coords): + for _ in range(DIFFICULTIES[self._difficulty]['mines']): + while True: + mine_x = random.randint(0, self.grid_width - 1) + mine_y = random.randint(0, self.grid_height - 1) + if self._get_data(mine_x, mine_y) == 0 and (mine_x, mine_y) != coords: + self._set_data(mine_x, mine_y, MINE) + break + self._compute_counts() + + def _set_data(self, x, y, value): + self._board_data[y * self.grid_width + x] = value + + def _get_data(self, x, y): + return self._board_data[y * self.grid_width + x] + + def _set_board(self, x, y, value): + if not isinstance(self.game_board, TileGrid): + raise ValueError("Game board not initialized") + self.game_board[x, y] = value # pylint: disable=unsupported-assignment-operation + + def _get_board(self, x, y): + if not isinstance(self.game_board, TileGrid): + raise ValueError("Game board not initialized") + return self.game_board[x, y] # pylint: disable=unsubscriptable-object + + def _compute_counts(self): + """For each mine, increment the count in each non-mine square around it""" + for y in range(self.grid_height): + for x in range(self.grid_width): + if self._get_data(x, y) != MINE: + continue # keep looking for mines + for dx in (-1, 0, 1): + if x + dx < 0 or x + dx >= self.grid_width: + continue # off screen + for dy in (-1, 0, 1): + if y + dy < 0 or y + dy >= self.grid_height: + continue # off screen + grid_value = self._get_data(x + dx, y + dy) + if grid_value == MINE: + continue # don't process mines + self._set_data(x + dx, y + dy, grid_value + 1) + + def _flag_count(self): + flags = 0 + for x in range(self.grid_width): + for y in range(self.grid_height): + if self._get_board(x, y) == FLAG: + flags += 1 + return flags + + def expand_uncovered(self, start_x, start_y): + # pylint: disable=too-many-nested-blocks + number_uncovered = 1 + stack = [(start_x, start_y)] + while len(stack) > 0: + x, y = stack.pop() + if self._get_board(x, y) == BLANK: + under_the_tile = self._get_data(x, y) + if under_the_tile <= OPEN8: + self._set_board(x, y, under_the_tile) + number_uncovered += 1 + if under_the_tile == OPEN: + for dx in (-1, 0, 1): + if x + dx < 0 or x + dx >= self.grid_width: + continue # off screen + for dy in (-1, 0, 1): + if y + dy < 0 or y + dy >= self.grid_height: + continue # off screen + if dx == 0 and dy == 0: + continue # don't process where the mine + stack.append((x + dx, y + dy)) + return number_uncovered + + def square_flagged(self, coords): + if self._status in (STATUS_WON, STATUS_LOST): + return False + + x, y = coords + TOGGLE_STATES = (BLANK, FLAG, MINE_QUESTION) + for state in TOGGLE_STATES: + if self._get_board(x, y) == state: + self._set_board(x, y, + TOGGLE_STATES[(TOGGLE_STATES.index(state) + 1) % len(TOGGLE_STATES)]) + break + return True + + def square_clicked(self, coords): + x, y = coords + + if self._status in (STATUS_WON, STATUS_LOST): + return False + + # First click is never a mine, so start the game + if self._status == STATUS_NEWGAME: + self._seed_mines(coords) + self._status = STATUS_PLAYING + if self._start_time is None: + self._start_time = ticks_ms() + + if self._get_board(x, y) != FLAG: + under_the_tile = self._get_data(x, y) + if under_the_tile == MINE: + self._set_data(x, y, MINE_CLICKED) + self._set_board(x, y, MINE_CLICKED) + self._status = STATUS_LOST + self.reveal_board() + if self._end_time is None: + self._end_time = ticks_ms() + return False #lost + elif OPEN1 <= under_the_tile <= OPEN8: + self._set_board(x, y, under_the_tile) + elif under_the_tile == OPEN: + self._set_board(x, y, BLANK) + self.expand_uncovered(x, y) + else: + raise ValueError(f'Unexpected value {under_the_tile} on board') + return True + + def reveal_board(self): + for x in range(self.grid_width): + for y in range(self.grid_height): + if self._get_board(x, y) == FLAG and self._get_data(x, y) != MINE: + self._set_board(x, y, MINE_FLAGGED_WRONG) + else: + self._set_board(x, y, self._get_data(x, y)) + + def check_for_win(self): + """Check for a complete, winning game. That's one with all squares uncovered + and all bombs correctly flagged, with no non-bomb squares flaged. + """ + if self._status in (STATUS_WON, STATUS_LOST): + return None + + # first make sure everything has been explored and decided + for x in range(self.grid_width): + for y in range(self.grid_height): + if self._get_board(x, y) == BLANK or self._get_board(x, y) == MINE_QUESTION: + return None # still ignored or question squares + # then check for mistagged bombs + for x in range(self.grid_width): + for y in range(self.grid_height): + if self._get_board(x, y) == FLAG and self._get_data(x, y) != MINE: + return False # misflagged bombs, not done + self._status = STATUS_WON + if self._end_time is None: + self._end_time = ticks_ms() + return True # nothing unexplored, and no misflagged bombs + + @property + def grid_width(self): + return DIFFICULTIES[self._difficulty]['grid_size'][0] + + @property + def grid_height(self): + return DIFFICULTIES[self._difficulty]['grid_size'][1] + + @property + def status(self): + return self._status + + @property + def elapsed_time(self): + """Elapsed time in seconds since the game started with a maximum of 999 seconds""" + if self._start_time is None: + return 0 + if self._end_time is None: + print(ticks_ms() / 1000, self._start_time / 1000) + return min(999, (ticks_ms() - self._start_time) // 1000) + return min(999, (self._end_time - self._start_time) // 1000) + + @property + def mines_left(self): + # This number can be negative + return self._mine_count - self._flag_count() + + @property + def difficulty(self): + return self._difficulty + + @difficulty.setter + def difficulty(self, value): + if not 0 <= value < len(DIFFICULTIES): + raise ValueError("Invalid difficulty option") + self._difficulty = value + nvm[0] = value + self.reset() diff --git a/Metro/Metro_RP2350_Minesweeper/menu.py b/Metro/Metro_RP2350_Minesweeper/menu.py new file mode 100755 index 000000000..16160889b --- /dev/null +++ b/Metro/Metro_RP2350_Minesweeper/menu.py @@ -0,0 +1,152 @@ +# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams for Adafruit Industries +# SPDX-License-Identifier: MIT + +from displayio import Group +from adafruit_display_shapes.rect import Rect +from eventbutton import EventButton + +MENU_ITEM_HEIGHT = 16 + +class Menu(Group): + def handle_mouse(self, point, clicked, waiting_for_release): + if waiting_for_release: + return False + # Check if the point is in the menu items group + handled_submenu = None + for submenu in self: + if isinstance(submenu, SubMenu): + if submenu.handle_mouse(point, clicked): + handled_submenu = submenu + if clicked: + # Hide any visible menus + for submenu in self: + if isinstance(submenu, SubMenu) and submenu != handled_submenu: + submenu.hide() + return handled_submenu is not None + +class SubMenu(Group): + def __init__(self, label, button_width, menu_width, x, y): + super().__init__() + self._label = label + self._button_width = button_width + self._menu_width = menu_width + self._menu_items_group = None + self._xpos = x + self._ypos = y + self._menu_items = [] + self._root_button = None + + def add_item(self, function, label): + self._menu_items.append( + { + "function": function, + "label": label, + } + ) + self._render() + + def _create_button(self, callback, label, width, x, y=0, border=True): + if border: + outline_color = 0x000000 + selected_outline = 0x333333 + else: + outline_color = 0xEEEEEE + selected_outline = 0xBBBBBB + + button = EventButton( + callback, + x=x, + y=y, + width=width, + height=MENU_ITEM_HEIGHT, + label=label, + style=EventButton.RECT, + fill_color=0xEEEEEE, + outline_color=outline_color, + label_color=0x333333, + selected_fill=0xBBBBBB, + selected_label=0x333333, + selected_outline=selected_outline, + ) + return button + + def _toggle_submenu(self): + self._menu_items_group.hidden = not self._menu_items_group.hidden + + def _render(self): + # Redraw the menu + # Remove all existing elements contained inside of this class + while len(self) > 0: + self.pop() + + # create a new root button + self._root_button = self._create_button( + self._toggle_submenu, + self._label, + self._button_width, + self._xpos, + self._ypos, + border=True, + ) + self.append(self._root_button) + + # Create the menu items group + self._menu_items_group = Group() + self._menu_items_group.hidden = True + self.append(self._menu_items_group) + + # Add the background rectangle to the menu items group + self._menu_items_group.append( + Rect(self._xpos, self._ypos + self._root_button.height - 1, self._menu_width, + len(self._menu_items) * MENU_ITEM_HEIGHT + 2, + fill=0xEEEEEE, + outline=0x333333 + ) + ) + + # Add the menu items to the menu items group + for index, item in enumerate(self._menu_items): + button = self._create_button( + item["function"], + item["label"], + self._menu_width - 2, + self._xpos + 1, + self._ypos + index * MENU_ITEM_HEIGHT + self._root_button.height, + border=False, + ) + self._menu_items_group.append(button) + + def hide(self): + self._menu_items_group.hidden = True + + def handle_mouse(self, point, clicked): + # Check if the point is in the root button + if self._menu_items_group.hidden: + if self._root_button.contains(point): + self._root_button.selected = True + if clicked: + self._root_button.click() + return True + else: + self._root_button.selected = False + else: + # Check if the point is in the menu items group + for button in self._menu_items_group: + if isinstance(button, EventButton): + if button.contains(point): + button.selected = True + if clicked: + button.click() + self._menu_items_group.hidden = True + return True + else: + button.selected = False + return False + + @property + def visible(self): + return not self._menu_items_group.hidden + + @property + def items_group(self): + return self._menu_items_group From e94ba7e31fb9a632681ccc42ca1269a6f42421da Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Wed, 16 Apr 2025 00:09:44 -0700 Subject: [PATCH 2/5] Fix bugs introduced by pylint --- Metro/Metro_RP2350_Minesweeper/eventbutton.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Metro/Metro_RP2350_Minesweeper/eventbutton.py b/Metro/Metro_RP2350_Minesweeper/eventbutton.py index c50eea710..0aa910e40 100755 --- a/Metro/Metro_RP2350_Minesweeper/eventbutton.py +++ b/Metro/Metro_RP2350_Minesweeper/eventbutton.py @@ -26,15 +26,16 @@ def click(self): self.callback(*self.args) def handle_mouse(self, point, clicked, waiting_for_release): + # pylint: disable=attribute-defined-outside-init if waiting_for_release: return False # Handle mouse events for the button if self.contains(point): - super().selected = True + self.selected = True if clicked: self.click() return True else: - super().selected = False + self.selected = False return False From 0cb30f69964c704b40347b3b7b847f5d3aced8b0 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Wed, 16 Apr 2025 00:13:05 -0700 Subject: [PATCH 3/5] Fix pylint issues --- Metro/Metro_RP2350_Minesweeper/code.py | 5 ++--- Metro/Metro_RP2350_Minesweeper/menu.py | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Metro/Metro_RP2350_Minesweeper/code.py b/Metro/Metro_RP2350_Minesweeper/code.py index f12024287..21bbe3f2a 100755 --- a/Metro/Metro_RP2350_Minesweeper/code.py +++ b/Metro/Metro_RP2350_Minesweeper/code.py @@ -17,8 +17,8 @@ import supervisor import terminalio import usb.core -from gamelogic import GameLogic, BLANK, INFO_BAR_HEIGHT, DIFFICULTIES -from menu import Menu, SubMenu +from .gamelogic import GameLogic, BLANK, INFO_BAR_HEIGHT, DIFFICULTIES +from .menu import Menu, SubMenu # pylint: disable=ungrouped-imports if hasattr(supervisor.runtime, "display") and supervisor.runtime.display is not None: @@ -199,7 +199,6 @@ def set_difficulty(diff): reset() def hide_group(group): - print("Hiding") group.hidden = True for i, difficulty in enumerate(DIFFICULTIES): diff --git a/Metro/Metro_RP2350_Minesweeper/menu.py b/Metro/Metro_RP2350_Minesweeper/menu.py index 16160889b..4b397de72 100755 --- a/Metro/Metro_RP2350_Minesweeper/menu.py +++ b/Metro/Metro_RP2350_Minesweeper/menu.py @@ -45,7 +45,8 @@ def add_item(self, function, label): ) self._render() - def _create_button(self, callback, label, width, x, y=0, border=True): + @staticmethod + def _create_button(callback, label, width, x, y=0, border=True): if border: outline_color = 0x000000 selected_outline = 0x333333 From 5cc6a257d361500cfa470b9ca02e08937a9483be Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Wed, 16 Apr 2025 00:14:40 -0700 Subject: [PATCH 4/5] Remove relative imports --- Metro/Metro_RP2350_Minesweeper/code.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Metro/Metro_RP2350_Minesweeper/code.py b/Metro/Metro_RP2350_Minesweeper/code.py index 21bbe3f2a..d4d887643 100755 --- a/Metro/Metro_RP2350_Minesweeper/code.py +++ b/Metro/Metro_RP2350_Minesweeper/code.py @@ -17,8 +17,8 @@ import supervisor import terminalio import usb.core -from .gamelogic import GameLogic, BLANK, INFO_BAR_HEIGHT, DIFFICULTIES -from .menu import Menu, SubMenu +from gamelogic import GameLogic, BLANK, INFO_BAR_HEIGHT, DIFFICULTIES +from menu import Menu, SubMenu # pylint: disable=ungrouped-imports if hasattr(supervisor.runtime, "display") and supervisor.runtime.display is not None: From 00d5e27de7a7816ab9a65630099c21e3bf2d0f21 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Wed, 16 Apr 2025 00:23:47 -0700 Subject: [PATCH 5/5] Fix pylint false positive --- Metro/Metro_RP2350_Minesweeper/code.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Metro/Metro_RP2350_Minesweeper/code.py b/Metro/Metro_RP2350_Minesweeper/code.py index d4d887643..3f1a19f36 100755 --- a/Metro/Metro_RP2350_Minesweeper/code.py +++ b/Metro/Metro_RP2350_Minesweeper/code.py @@ -49,7 +49,7 @@ ) display = framebufferio.FramebufferDisplay(fb) -game_logic = GameLogic(display) +game_logic = GameLogic(display) # pylint: disable=no-value-for-parameter # Load the spritesheet sprite_sheet = OnDiskBitmap("/bitmaps/game_sprites.bmp")