Skip to content

Commit a87f6ab

Browse files
authored
Merge pull request #3045 from makermelissa/main
Add 3-in-a-row tile matching game
2 parents b94d8a9 + 08dc836 commit a87f6ab

File tree

6 files changed

+801
-0
lines changed

6 files changed

+801
-0
lines changed
Binary file not shown.
Binary file not shown.
Binary file not shown.
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
"""
4+
An implementation of a match3 jewel swap game. The idea is to move one character at a time
5+
to line up at least 3 characters.
6+
"""
7+
from displayio import Group, OnDiskBitmap, TileGrid, Bitmap, Palette
8+
from adafruit_display_text.bitmap_label import Label
9+
from adafruit_display_text.text_box import TextBox
10+
from eventbutton import EventButton
11+
import supervisor
12+
import terminalio
13+
from adafruit_usb_host_mouse import find_and_init_boot_mouse
14+
from gamelogic import GameLogic, SELECTOR_SPRITE, EMPTY_SPRITE, GAMEBOARD_POSITION
15+
16+
GAMEBOARD_SIZE = (8, 7)
17+
HINT_TIMEOUT = 10 # seconds before hint is shown
18+
GAME_PIECES = 7 # Number of different game pieces (set between 3 and 8)
19+
20+
# pylint: disable=ungrouped-imports
21+
if hasattr(supervisor.runtime, "display") and supervisor.runtime.display is not None:
22+
# use the built-in HSTX display for Metro RP2350
23+
display = supervisor.runtime.display
24+
else:
25+
# pylint: disable=ungrouped-imports
26+
from displayio import release_displays
27+
import picodvi
28+
import board
29+
import framebufferio
30+
31+
# initialize display
32+
release_displays()
33+
34+
fb = picodvi.Framebuffer(
35+
320,
36+
240,
37+
clk_dp=board.CKP,
38+
clk_dn=board.CKN,
39+
red_dp=board.D0P,
40+
red_dn=board.D0N,
41+
green_dp=board.D1P,
42+
green_dn=board.D1N,
43+
blue_dp=board.D2P,
44+
blue_dn=board.D2N,
45+
color_depth=16,
46+
)
47+
display = framebufferio.FramebufferDisplay(fb)
48+
49+
def get_color_index(color, shader=None):
50+
for index, palette_color in enumerate(shader):
51+
if palette_color == color:
52+
return index
53+
return None
54+
55+
# Load the spritesheet
56+
sprite_sheet = OnDiskBitmap("/bitmaps/game_sprites.bmp")
57+
sprite_sheet.pixel_shader.make_transparent(
58+
get_color_index(0x00ff00, sprite_sheet.pixel_shader)
59+
)
60+
61+
# Main group will hold all the visual layers
62+
main_group = Group()
63+
display.root_group = main_group
64+
65+
# Add Background to the Main Group
66+
background = Bitmap(display.width, display.height, 1)
67+
bg_color = Palette(1)
68+
bg_color[0] = 0x333333
69+
main_group.append(TileGrid(
70+
background,
71+
pixel_shader=bg_color
72+
))
73+
74+
# Add Game grid, which holds the game board, to the main group
75+
game_grid = TileGrid(
76+
sprite_sheet,
77+
pixel_shader=sprite_sheet.pixel_shader,
78+
width=GAMEBOARD_SIZE[0],
79+
height=GAMEBOARD_SIZE[1],
80+
tile_width=32,
81+
tile_height=32,
82+
x=GAMEBOARD_POSITION[0],
83+
y=GAMEBOARD_POSITION[1],
84+
default_tile=EMPTY_SPRITE,
85+
)
86+
main_group.append(game_grid)
87+
88+
# Add a special selection groupd to highlight the selected piece and allow animation
89+
selected_piece_group = Group()
90+
selected_piece = TileGrid(
91+
sprite_sheet,
92+
pixel_shader=sprite_sheet.pixel_shader,
93+
width=1,
94+
height=1,
95+
tile_width=32,
96+
tile_height=32,
97+
x=0,
98+
y=0,
99+
default_tile=EMPTY_SPRITE,
100+
)
101+
selected_piece_group.append(selected_piece)
102+
selector = TileGrid(
103+
sprite_sheet,
104+
pixel_shader=sprite_sheet.pixel_shader,
105+
width=1,
106+
height=1,
107+
tile_width=32,
108+
tile_height=32,
109+
x=0,
110+
y=0,
111+
default_tile=SELECTOR_SPRITE,
112+
)
113+
selected_piece_group.append(selector)
114+
selected_piece_group.hidden = True
115+
main_group.append(selected_piece_group)
116+
117+
# Add a group for the swap piece to help with animation
118+
swap_piece = TileGrid(
119+
sprite_sheet,
120+
pixel_shader=sprite_sheet.pixel_shader,
121+
width=1,
122+
height=1,
123+
tile_width=32,
124+
tile_height=32,
125+
x=0,
126+
y=0,
127+
default_tile=EMPTY_SPRITE,
128+
)
129+
swap_piece.hidden = True
130+
main_group.append(swap_piece)
131+
132+
# Add foreground
133+
foreground_bmp = OnDiskBitmap("/bitmaps/foreground.bmp")
134+
foreground_bmp.pixel_shader.make_transparent(0)
135+
foreground_tg = TileGrid(foreground_bmp, pixel_shader=foreground_bmp.pixel_shader)
136+
foreground_tg.x = 0
137+
foreground_tg.y = 0
138+
main_group.append(foreground_tg)
139+
140+
# Add a group for the UI Elements
141+
ui_group = Group()
142+
main_group.append(ui_group)
143+
144+
# Create the game logic object
145+
# pylint: disable=no-value-for-parameter, too-many-function-args
146+
game_logic = GameLogic(
147+
display,
148+
game_grid,
149+
swap_piece,
150+
selected_piece_group,
151+
GAME_PIECES
152+
)
153+
154+
# Create the mouse graphics and add to the main group
155+
mouse = find_and_init_boot_mouse("/bitmaps/mouse_cursor.bmp")
156+
if mouse is None:
157+
raise RuntimeError("No mouse found connected to USB Host")
158+
main_group.append(mouse.tilegrid)
159+
160+
def update_ui():
161+
# Update the UI elements with the current game state
162+
score_label.text = f"Score:\n{game_logic.score}"
163+
164+
waiting_for_release = False
165+
game_over_shown = False
166+
167+
# Create the UI Elements
168+
# Label for the Score
169+
score_label = Label(
170+
terminalio.FONT,
171+
color=0xffff00,
172+
x=5,
173+
y=10,
174+
)
175+
ui_group.append(score_label)
176+
177+
message_dialog = Group()
178+
message_dialog.hidden = True
179+
180+
def reset():
181+
global game_over_shown # pylint: disable=global-statement
182+
# Reset the game logic
183+
game_logic.reset()
184+
message_dialog.hidden = True
185+
game_over_shown = False
186+
187+
def hide_group(group):
188+
group.hidden = True
189+
190+
reset()
191+
192+
reset_button = EventButton(
193+
reset,
194+
label="Reset",
195+
width=40,
196+
height=16,
197+
x=5,
198+
y=50,
199+
style=EventButton.RECT,
200+
)
201+
ui_group.append(reset_button)
202+
203+
message_label = TextBox(
204+
terminalio.FONT,
205+
text="",
206+
color=0x333333,
207+
background_color=0xEEEEEE,
208+
width=display.width // 3,
209+
height=90,
210+
align=TextBox.ALIGN_CENTER,
211+
padding_top=5,
212+
)
213+
message_label.anchor_point = (0, 0)
214+
message_label.anchored_position = (
215+
display.width // 2 - message_label.width // 2,
216+
display.height // 2 - message_label.height // 2,
217+
)
218+
message_dialog.append(message_label)
219+
message_button = EventButton(
220+
(hide_group, message_dialog),
221+
label="OK",
222+
width=40,
223+
height=16,
224+
x=display.width // 2 - 20,
225+
y=display.height // 2 - message_label.height // 2 + 60,
226+
style=EventButton.RECT,
227+
)
228+
message_dialog.append(message_button)
229+
ui_group.append(message_dialog)
230+
231+
# main loop
232+
while True:
233+
update_ui()
234+
# update mouse
235+
pressed_btns = mouse.update()
236+
237+
if waiting_for_release and not pressed_btns:
238+
# If both buttons are released, we can process the next click
239+
waiting_for_release = False
240+
241+
if not message_dialog.hidden:
242+
if message_button.handle_mouse((mouse.x, mouse.y),
243+
pressed_btns and "left" in pressed_btns,
244+
waiting_for_release):
245+
waiting_for_release = True
246+
continue
247+
248+
if reset_button.handle_mouse((mouse.x, mouse.y),
249+
pressed_btns and "left" in pressed_btns,
250+
waiting_for_release):
251+
waiting_for_release = True
252+
253+
# process gameboard click if no menu
254+
game_board = game_logic.game_board
255+
if (game_board.x <= mouse.x <= game_board.x + game_board.columns * 32 and
256+
game_board.y <= mouse.y <= game_board.y + game_board.rows * 32 and
257+
not waiting_for_release):
258+
piece_coords = ((mouse.x - game_board.x) // 32, (mouse.y - game_board.y) // 32)
259+
if pressed_btns and "left" in pressed_btns:
260+
game_logic.piece_clicked(piece_coords)
261+
waiting_for_release = True
262+
game_over = game_logic.check_for_game_over()
263+
if game_over and not game_over_shown:
264+
message_label.text = ("No more moves available. your final score is:\n"
265+
+ str(game_logic.score))
266+
message_dialog.hidden = False
267+
game_over_shown = True
268+
if game_logic.time_since_last_update > HINT_TIMEOUT:
269+
game_logic.show_hint()
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+
self.selected = False
19+
if isinstance(callback, tuple):
20+
self.callback = callback[0]
21+
self.args = callback[1:]
22+
else:
23+
self.callback = callback
24+
25+
def click(self):
26+
"""Call the function when the button is pressed."""
27+
self.callback(*self.args)
28+
29+
def handle_mouse(self, point, clicked, waiting_for_release):
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)