|
| 1 | +# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams for Adafruit Industries |
| 2 | +# SPDX-License-Identifier: MIT |
| 3 | +""" |
| 4 | +An implementation of minesweeper. The logic game where the player |
| 5 | +correctly identifies the locations of mines on a grid by clicking on squares |
| 6 | +and revealing the number of mines in adjacent squares. |
| 7 | +
|
| 8 | +The player can also flag squares they suspect contain mines. The game ends when |
| 9 | +the player successfully reveals all squares without mines or clicks on a mine. |
| 10 | +""" |
| 11 | +import array |
| 12 | +import time |
| 13 | +from displayio import Group, OnDiskBitmap, TileGrid, Bitmap, Palette |
| 14 | +from adafruit_display_text.bitmap_label import Label |
| 15 | +from adafruit_display_text.text_box import TextBox |
| 16 | +from eventbutton import EventButton |
| 17 | +import supervisor |
| 18 | +import terminalio |
| 19 | +import usb.core |
| 20 | +from gamelogic import GameLogic, BLANK, INFO_BAR_HEIGHT, DIFFICULTIES |
| 21 | +from menu import Menu, SubMenu |
| 22 | + |
| 23 | +# pylint: disable=ungrouped-imports |
| 24 | +if hasattr(supervisor.runtime, "display") and supervisor.runtime.display is not None: |
| 25 | + # use the built-in HSTX display for Metro RP2350 |
| 26 | + display = supervisor.runtime.display |
| 27 | +else: |
| 28 | + # pylint: disable=ungrouped-imports |
| 29 | + from displayio import release_displays |
| 30 | + import picodvi |
| 31 | + import board |
| 32 | + import framebufferio |
| 33 | + |
| 34 | + # initialize display |
| 35 | + release_displays() |
| 36 | + |
| 37 | + fb = picodvi.Framebuffer( |
| 38 | + 320, |
| 39 | + 240, |
| 40 | + clk_dp=board.CKP, |
| 41 | + clk_dn=board.CKN, |
| 42 | + red_dp=board.D0P, |
| 43 | + red_dn=board.D0N, |
| 44 | + green_dp=board.D1P, |
| 45 | + green_dn=board.D1N, |
| 46 | + blue_dp=board.D2P, |
| 47 | + blue_dn=board.D2N, |
| 48 | + color_depth=16, |
| 49 | + ) |
| 50 | + display = framebufferio.FramebufferDisplay(fb) |
| 51 | + |
| 52 | +game_logic = GameLogic(display) # pylint: disable=no-value-for-parameter |
| 53 | + |
| 54 | +# Load the spritesheet |
| 55 | +sprite_sheet = OnDiskBitmap("/bitmaps/game_sprites.bmp") |
| 56 | + |
| 57 | +# Main group will hold all the visual layers |
| 58 | +main_group = Group() |
| 59 | +display.root_group = main_group |
| 60 | + |
| 61 | +# Add Background to the Main Group |
| 62 | +background = Bitmap(display.width, display.height, 1) |
| 63 | +bg_color = Palette(1) |
| 64 | +bg_color[0] = 0xaaaaaa |
| 65 | +main_group.append(TileGrid( |
| 66 | + background, |
| 67 | + pixel_shader=bg_color |
| 68 | +)) |
| 69 | + |
| 70 | +# Add Game group, which holds the game board, to the main group |
| 71 | +game_group = Group() |
| 72 | +main_group.append(game_group) |
| 73 | + |
| 74 | +# Add a group for the UI Elements |
| 75 | +ui_group = Group() |
| 76 | +main_group.append(ui_group) |
| 77 | + |
| 78 | +# Create the mouse graphics and add to the main group |
| 79 | +mouse_bmp = OnDiskBitmap("/bitmaps/mouse_cursor.bmp") |
| 80 | +mouse_bmp.pixel_shader.make_transparent(0) |
| 81 | +mouse_tg = TileGrid(mouse_bmp, pixel_shader=mouse_bmp.pixel_shader) |
| 82 | +mouse_tg.x = display.width // 2 |
| 83 | +mouse_tg.y = display.height // 2 |
| 84 | +main_group.append(mouse_tg) |
| 85 | + |
| 86 | +MENU_ITEM_HEIGHT = INFO_BAR_HEIGHT |
| 87 | + |
| 88 | +def create_game_board(): |
| 89 | + # Remove the old game board |
| 90 | + if len(game_group) > 0: |
| 91 | + game_group.pop() |
| 92 | + |
| 93 | + x = display.width // 2 - (game_logic.grid_width * 16) // 2 |
| 94 | + y = ((display.height - INFO_BAR_HEIGHT) // 2 - |
| 95 | + (game_logic.grid_height * 16) // 2 + INFO_BAR_HEIGHT) |
| 96 | + |
| 97 | + # Create a new game board |
| 98 | + game_board = TileGrid( |
| 99 | + sprite_sheet, |
| 100 | + pixel_shader=sprite_sheet.pixel_shader, |
| 101 | + width=game_logic.grid_width, |
| 102 | + height=game_logic.grid_height, |
| 103 | + tile_height=16, |
| 104 | + tile_width=16, |
| 105 | + x=x, |
| 106 | + y=y, |
| 107 | + default_tile=BLANK, |
| 108 | + ) |
| 109 | + |
| 110 | + game_group.append(game_board) |
| 111 | + return game_board |
| 112 | + |
| 113 | +def update_ui(): |
| 114 | + # Update the UI elements with the current game state |
| 115 | + mines_left_label.text = f"Mines: {game_logic.mines_left}" |
| 116 | + elapsed_time_label.text = f"Time: {game_logic.elapsed_time}" |
| 117 | + |
| 118 | +# variable for the mouse USB device instance |
| 119 | +mouse = None |
| 120 | + |
| 121 | +# wait a second for USB devices to be ready |
| 122 | +time.sleep(1) |
| 123 | + |
| 124 | +# scan for connected USB devices |
| 125 | +for device in usb.core.find(find_all=True): |
| 126 | + # print information about the found devices |
| 127 | + print(f"{device.idVendor:04x}:{device.idProduct:04x}") |
| 128 | + print(device.manufacturer, device.product) |
| 129 | + print(device.serial_number) |
| 130 | + |
| 131 | + # assume this device is the mouse |
| 132 | + mouse = device |
| 133 | + |
| 134 | + # detach from kernel driver if active |
| 135 | + if mouse.is_kernel_driver_active(0): |
| 136 | + mouse.detach_kernel_driver(0) |
| 137 | + |
| 138 | + # set the mouse configuration so it can be used |
| 139 | + mouse.set_configuration() |
| 140 | + |
| 141 | +buf = array.array("b", [0] * 4) |
| 142 | +waiting_for_release = False |
| 143 | +left_button = right_button = False |
| 144 | +mouse_coords = (0, 0) |
| 145 | + |
| 146 | +# Create the UI Elements (Ideally fit into 320x16 area) |
| 147 | +# Label for the Mines Left (Left of Center) |
| 148 | +mines_left_label = Label( |
| 149 | + terminalio.FONT, |
| 150 | + color=0x000000, |
| 151 | + x=5, |
| 152 | + y=0, |
| 153 | +) |
| 154 | +mines_left_label.anchor_point = (0, 0) |
| 155 | +mines_left_label.anchored_position = (5, 2) |
| 156 | +ui_group.append(mines_left_label) |
| 157 | +# Label for the Elapsed Time (Right of Center) |
| 158 | +elapsed_time_label = Label( |
| 159 | + terminalio.FONT, |
| 160 | + color=0x000000, |
| 161 | + x=display.width - 50, |
| 162 | + y=0, |
| 163 | +) |
| 164 | +elapsed_time_label.anchor_point = (1, 0) |
| 165 | +elapsed_time_label.anchored_position = (display.width - 5, 2) |
| 166 | +ui_group.append(elapsed_time_label) |
| 167 | + |
| 168 | +# Menu button to change difficulty |
| 169 | +difficulty_menu = SubMenu( |
| 170 | + "Difficulty", |
| 171 | + 70, |
| 172 | + 80, |
| 173 | + display.width // 2 - 70, |
| 174 | + 0 |
| 175 | +) |
| 176 | + |
| 177 | +reset_menu = SubMenu( |
| 178 | + "Reset", |
| 179 | + 50, |
| 180 | + 40, |
| 181 | + display.width // 2 + 15, |
| 182 | + 0 |
| 183 | +) |
| 184 | + |
| 185 | +message_dialog = Group() |
| 186 | +message_dialog.hidden = True |
| 187 | + |
| 188 | +def reset(): |
| 189 | + # Reset the game logic |
| 190 | + game_logic.reset() |
| 191 | + |
| 192 | + # Create a new game board and assign it into the game logic |
| 193 | + game_logic.game_board = create_game_board() |
| 194 | + |
| 195 | + message_dialog.hidden = True |
| 196 | + |
| 197 | +def set_difficulty(diff): |
| 198 | + game_logic.difficulty = diff |
| 199 | + reset() |
| 200 | + |
| 201 | +def hide_group(group): |
| 202 | + group.hidden = True |
| 203 | + |
| 204 | +for i, difficulty in enumerate(DIFFICULTIES): |
| 205 | + # Create a button for each difficulty |
| 206 | + difficulty_menu.add_item((set_difficulty, i), difficulty['label']) |
| 207 | + |
| 208 | +reset_menu.add_item(reset, "OK") |
| 209 | + |
| 210 | +menu = Menu() |
| 211 | +menu.append(difficulty_menu) |
| 212 | +menu.append(reset_menu) |
| 213 | +ui_group.append(menu) |
| 214 | + |
| 215 | +reset() |
| 216 | + |
| 217 | +message_label = TextBox( |
| 218 | + terminalio.FONT, |
| 219 | + text="", |
| 220 | + color=0x333333, |
| 221 | + background_color=0xEEEEEE, |
| 222 | + width=display.width // 4, |
| 223 | + height=50, |
| 224 | + align=TextBox.ALIGN_CENTER, |
| 225 | + padding_top=5, |
| 226 | +) |
| 227 | +message_label.anchor_point = (0, 0) |
| 228 | +message_label.anchored_position = ( |
| 229 | + display.width // 2 - message_label.width // 2, |
| 230 | + display.height // 2 - message_label.height // 2, |
| 231 | +) |
| 232 | +message_dialog.append(message_label) |
| 233 | +message_button = EventButton( |
| 234 | + (hide_group, message_dialog), |
| 235 | + label="OK", |
| 236 | + width=40, |
| 237 | + height=16, |
| 238 | + x=display.width // 2 - 20, |
| 239 | + y=display.height // 2 - message_label.height // 2 + 20, |
| 240 | + style=EventButton.RECT, |
| 241 | +) |
| 242 | +message_dialog.append(message_button) |
| 243 | +ui_group.append(message_dialog) |
| 244 | + |
| 245 | +# Popup message for game over/win |
| 246 | + |
| 247 | +menus = (reset_menu, difficulty_menu) |
| 248 | + |
| 249 | +# main loop |
| 250 | +while True: |
| 251 | + update_ui() |
| 252 | + # attempt mouse read |
| 253 | + try: |
| 254 | + # try to read data from the mouse, small timeout so the code will move on |
| 255 | + # quickly if there is no data |
| 256 | + data_len = mouse.read(0x81, buf, timeout=10) |
| 257 | + left_button = buf[0] & 0x01 |
| 258 | + right_button = buf[0] & 0x02 |
| 259 | + |
| 260 | + # if there was data, then update the mouse cursor on the display |
| 261 | + # using min and max to keep it within the bounds of the display |
| 262 | + mouse_tg.x = max(0, min(display.width - 1, mouse_tg.x + buf[1] // 2)) |
| 263 | + mouse_tg.y = max(0, min(display.height - 1, mouse_tg.y + buf[2] // 2)) |
| 264 | + mouse_coords = (mouse_tg.x, mouse_tg.y) |
| 265 | + |
| 266 | + if waiting_for_release and not left_button and not right_button: |
| 267 | + # If both buttons are released, we can process the next click |
| 268 | + waiting_for_release = False |
| 269 | + |
| 270 | + # timeout error is raised if no data was read within the allotted timeout |
| 271 | + except usb.core.USBTimeoutError: |
| 272 | + # no problem, just go on |
| 273 | + pass |
| 274 | + except AttributeError as exc: |
| 275 | + raise RuntimeError("Mouse not found") from exc |
| 276 | + if not message_dialog.hidden: |
| 277 | + if message_button.handle_mouse(mouse_coords, left_button, waiting_for_release): |
| 278 | + waiting_for_release = True |
| 279 | + continue |
| 280 | + |
| 281 | + if menu.handle_mouse(mouse_coords, left_button, waiting_for_release): |
| 282 | + waiting_for_release = True |
| 283 | + else: |
| 284 | + # process gameboard click if no menu |
| 285 | + ms_board = game_logic.game_board |
| 286 | + if (ms_board.x <= mouse_tg.x <= ms_board.x + game_logic.grid_width * 16 and |
| 287 | + ms_board.y <= mouse_tg.y <= ms_board.y + game_logic.grid_height * 16 and |
| 288 | + not waiting_for_release): |
| 289 | + coords = ((mouse_tg.x - ms_board.x) // 16, (mouse_tg.y - ms_board.y) // 16) |
| 290 | + if right_button: |
| 291 | + game_logic.square_flagged(coords) |
| 292 | + elif left_button: |
| 293 | + if not game_logic.square_clicked(coords): |
| 294 | + message_label.text = "Game Over" |
| 295 | + message_dialog.hidden = False |
| 296 | + if left_button or right_button: |
| 297 | + waiting_for_release = True |
| 298 | + status = game_logic.check_for_win() |
| 299 | + if status: |
| 300 | + message_label.text = "You win!" |
| 301 | + message_dialog.hidden = False |
| 302 | + # Display message |
| 303 | + if status is None: |
| 304 | + continue |
0 commit comments