Skip to content

Commit b415616

Browse files
authored
Merge pull request #3022 from makermelissa/main
Adding Minesweeper code for new guide
2 parents eca663c + 00d5e27 commit b415616

File tree

6 files changed

+761
-0
lines changed

6 files changed

+761
-0
lines changed
Binary file not shown.
Binary file not shown.
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
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
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
4+
from adafruit_button import Button
5+
6+
class EventButton(Button):
7+
"""A button that can be used to trigger a callback when clicked.
8+
9+
:param callback: The callback function to call when the button is clicked.
10+
A tuple can be passed with an argument that will be passed to the
11+
callback function. The first element of the tuple should be the
12+
callback function, and the remaining elements will be passed as
13+
arguments to the callback function.
14+
"""
15+
def __init__(self, callback, *args, **kwargs):
16+
super().__init__(*args, **kwargs)
17+
self.args = []
18+
if isinstance(callback, tuple):
19+
self.callback = callback[0]
20+
self.args = callback[1:]
21+
else:
22+
self.callback = callback
23+
24+
def click(self):
25+
"""Call the function when the button is pressed."""
26+
self.callback(*self.args)
27+
28+
def handle_mouse(self, point, clicked, waiting_for_release):
29+
# pylint: disable=attribute-defined-outside-init
30+
if waiting_for_release:
31+
return False
32+
33+
# Handle mouse events for the button
34+
if self.contains(point):
35+
self.selected = True
36+
if clicked:
37+
self.click()
38+
return True
39+
else:
40+
self.selected = False
41+
return False

0 commit comments

Comments
 (0)