Skip to content

Commit 58e89f5

Browse files
committed
Add minesweeper code
1 parent de887d1 commit 58e89f5

File tree

6 files changed

+760
-0
lines changed

6 files changed

+760
-0
lines changed
4.24 KB
Binary file not shown.
128 Bytes
Binary file not shown.
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
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)
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+
print("Hiding")
203+
group.hidden = True
204+
205+
for i, difficulty in enumerate(DIFFICULTIES):
206+
# Create a button for each difficulty
207+
difficulty_menu.add_item((set_difficulty, i), difficulty['label'])
208+
209+
reset_menu.add_item(reset, "OK")
210+
211+
menu = Menu()
212+
menu.append(difficulty_menu)
213+
menu.append(reset_menu)
214+
ui_group.append(menu)
215+
216+
reset()
217+
218+
message_label = TextBox(
219+
terminalio.FONT,
220+
text="",
221+
color=0x333333,
222+
background_color=0xEEEEEE,
223+
width=display.width // 4,
224+
height=50,
225+
align=TextBox.ALIGN_CENTER,
226+
padding_top=5,
227+
)
228+
message_label.anchor_point = (0, 0)
229+
message_label.anchored_position = (
230+
display.width // 2 - message_label.width // 2,
231+
display.height // 2 - message_label.height // 2,
232+
)
233+
message_dialog.append(message_label)
234+
message_button = EventButton(
235+
(hide_group, message_dialog),
236+
label="OK",
237+
width=40,
238+
height=16,
239+
x=display.width // 2 - 20,
240+
y=display.height // 2 - message_label.height // 2 + 20,
241+
style=EventButton.RECT,
242+
)
243+
message_dialog.append(message_button)
244+
ui_group.append(message_dialog)
245+
246+
# Popup message for game over/win
247+
248+
menus = (reset_menu, difficulty_menu)
249+
250+
# main loop
251+
while True:
252+
update_ui()
253+
# attempt mouse read
254+
try:
255+
# try to read data from the mouse, small timeout so the code will move on
256+
# quickly if there is no data
257+
data_len = mouse.read(0x81, buf, timeout=10)
258+
left_button = buf[0] & 0x01
259+
right_button = buf[0] & 0x02
260+
261+
# if there was data, then update the mouse cursor on the display
262+
# using min and max to keep it within the bounds of the display
263+
mouse_tg.x = max(0, min(display.width - 1, mouse_tg.x + buf[1] // 2))
264+
mouse_tg.y = max(0, min(display.height - 1, mouse_tg.y + buf[2] // 2))
265+
mouse_coords = (mouse_tg.x, mouse_tg.y)
266+
267+
if waiting_for_release and not left_button and not right_button:
268+
# If both buttons are released, we can process the next click
269+
waiting_for_release = False
270+
271+
# timeout error is raised if no data was read within the allotted timeout
272+
except usb.core.USBTimeoutError:
273+
# no problem, just go on
274+
pass
275+
except AttributeError as exc:
276+
raise RuntimeError("Mouse not found") from exc
277+
if not message_dialog.hidden:
278+
if message_button.handle_mouse(mouse_coords, left_button, waiting_for_release):
279+
waiting_for_release = True
280+
continue
281+
282+
if menu.handle_mouse(mouse_coords, left_button, waiting_for_release):
283+
waiting_for_release = True
284+
else:
285+
# process gameboard click if no menu
286+
ms_board = game_logic.game_board
287+
if (ms_board.x <= mouse_tg.x <= ms_board.x + game_logic.grid_width * 16 and
288+
ms_board.y <= mouse_tg.y <= ms_board.y + game_logic.grid_height * 16 and
289+
not waiting_for_release):
290+
coords = ((mouse_tg.x - ms_board.x) // 16, (mouse_tg.y - ms_board.y) // 16)
291+
if right_button:
292+
game_logic.square_flagged(coords)
293+
elif left_button:
294+
if not game_logic.square_clicked(coords):
295+
message_label.text = "Game Over"
296+
message_dialog.hidden = False
297+
if left_button or right_button:
298+
waiting_for_release = True
299+
status = game_logic.check_for_win()
300+
if status:
301+
message_label.text = "You win!"
302+
message_dialog.hidden = False
303+
# Display message
304+
if status is None:
305+
continue
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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+
if waiting_for_release:
30+
return False
31+
32+
# Handle mouse events for the button
33+
if self.contains(point):
34+
super().selected = True
35+
if clicked:
36+
self.click()
37+
return True
38+
else:
39+
super().selected = False
40+
return False

0 commit comments

Comments
 (0)