Skip to content

Fix many bugs #3052

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 25 additions & 30 deletions Metro/Metro_RP2350_Matching_Game/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
An implementation of a match3 jewel swap game. The idea is to move one character at a time
to line up at least 3 characters.
"""
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
Expand Down Expand Up @@ -141,22 +142,25 @@ def get_color_index(color, shader=None):
ui_group = Group()
main_group.append(ui_group)

# Create the mouse graphics and add to the main group
time.sleep(1) # Allow time for USB host to initialize
mouse = find_and_init_boot_mouse("/bitmaps/mouse_cursor.bmp")
if mouse is None:
raise RuntimeError("No mouse found connected to USB Host")
main_group.append(mouse.tilegrid)

# Create the game logic object
# pylint: disable=no-value-for-parameter, too-many-function-args
game_logic = GameLogic(
display,
mouse,
game_grid,
swap_piece,
selected_piece_group,
GAME_PIECES
GAME_PIECES,
HINT_TIMEOUT
)

# Create the mouse graphics and add to the main group
mouse = find_and_init_boot_mouse("/bitmaps/mouse_cursor.bmp")
if mouse is None:
raise RuntimeError("No mouse found connected to USB Host")
main_group.append(mouse.tilegrid)

def update_ui():
# Update the UI elements with the current game state
score_label.text = f"Score:\n{game_logic.score}"
Expand Down Expand Up @@ -232,38 +236,29 @@ def hide_group(group):
while True:
update_ui()
# update mouse
pressed_btns = mouse.update()

if waiting_for_release and not pressed_btns:
# If both buttons are released, we can process the next click
waiting_for_release = False
game_logic.update_mouse()

if not message_dialog.hidden:
if message_button.handle_mouse((mouse.x, mouse.y),
pressed_btns and "left" in pressed_btns,
waiting_for_release):
waiting_for_release = True
if message_button.handle_mouse(
(mouse.x, mouse.y),
game_logic.pressed_btns and "left" in game_logic.pressed_btns,
waiting_for_release
):
game_logic.waiting_for_release = True
continue

if reset_button.handle_mouse((mouse.x, mouse.y),
pressed_btns and "left" in pressed_btns,
waiting_for_release):
waiting_for_release = True
if reset_button.handle_mouse(
(mouse.x, mouse.y),
game_logic.pressed_btns is not None and "left" in game_logic.pressed_btns,
game_logic.waiting_for_release
):
game_logic.waiting_for_release = True

# process gameboard click if no menu
game_board = game_logic.game_board
if (game_board.x <= mouse.x <= game_board.x + game_board.columns * 32 and
game_board.y <= mouse.y <= game_board.y + game_board.rows * 32 and
not waiting_for_release):
piece_coords = ((mouse.x - game_board.x) // 32, (mouse.y - game_board.y) // 32)
if pressed_btns and "left" in pressed_btns:
game_logic.piece_clicked(piece_coords)
waiting_for_release = True
game_logic.update()
game_over = game_logic.check_for_game_over()
if game_over and not game_over_shown:
message_label.text = ("No more moves available. your final score is:\n"
+ str(game_logic.score))
message_dialog.hidden = False
game_over_shown = True
if game_logic.time_since_last_update > HINT_TIMEOUT:
game_logic.show_hint()
98 changes: 66 additions & 32 deletions Metro/Metro_RP2350_Matching_Game/gamelogic.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

SELECTOR_SPRITE = 9
EMPTY_SPRITE = 10
DEBOUNCE_TIME = 0.1 # seconds for debouncing mouse clicks
DEBOUNCE_TIME = 0.2 # seconds for debouncing mouse clicks

class GameBoard:
"Contains the game board"
Expand Down Expand Up @@ -42,6 +42,10 @@ def reset(self):
for row in range(self.rows):
if self._game_grid[(column, row)] != EMPTY_SPRITE:
self.remove_game_piece(column, row)
# Hide the animation TileGrids
self._selector.hidden = True
self._swap_piece.hidden = True
self.selected_piece_group.hidden = True

def move_game_piece(self, old_x, old_y, new_x, new_y):
if 0 <= old_x < self.columns and 0 <= old_y < self.rows:
Expand Down Expand Up @@ -153,18 +157,41 @@ def game_grid_copy(self):

class GameLogic:
"Contains the Logic to examine the game board and determine if a move is valid."
def __init__(self, display, game_grid, swap_piece, selected_piece_group, game_pieces):
def __init__(self, display, mouse, game_grid, swap_piece,
selected_piece_group, game_pieces, hint_timeout):
self._display = display
self._mouse = mouse
self.game_board = GameBoard(game_grid, swap_piece, selected_piece_group)
self._score = 0
self._available_moves = []
if not 3 <= game_pieces <= 8:
raise ValueError("game_pieces must be between 3 and 8")
self._game_pieces = game_pieces # Number of different game pieces
self._hint_timeout = hint_timeout
self._last_update_time = ticks_ms() # For hint timing
self._last_click_time = ticks_ms() # For debouncing mouse clicks
self.pressed_btns = None
self.waiting_for_release = False

def piece_clicked(self, coords):
def update_mouse(self):
self.pressed_btns = self._mouse.update()
if self.waiting_for_release and not self.pressed_btns:
# If both buttons are released, we can process the next click
self.waiting_for_release = False

def update(self):
gb = self.game_board
if (gb.x <= self._mouse.x <= gb.x + gb.columns * 32 and
gb.y <= self._mouse.y <= gb.y + gb.rows * 32 and
not self.waiting_for_release):
piece_coords = ((self._mouse.x - gb.x) // 32, (self._mouse.y - gb.y) // 32)
if self.pressed_btns and "left" in self.pressed_btns:
self._piece_clicked(piece_coords)
self.waiting_for_release = True
if self.time_since_last_update > self._hint_timeout:
self.show_hint()

def _piece_clicked(self, coords):
""" Handle a piece click event. """
if ticks_ms() <= self._last_click_time:
self._last_click_time -= 2**29 # ticks_ms() wraps around after 2**29 ms
Expand Down Expand Up @@ -206,7 +233,7 @@ def piece_clicked(self, coords):
if ((abs(previous_x - column) == 1 and previous_y == row) or
(previous_x == column and abs(previous_y - row) == 1)):
# Swap the pieces
self.swap_selected_piece(column, row)
self._swap_selected_piece(column, row)

def show_hint(self):
""" Show a hint by selecting a random available
Expand All @@ -216,21 +243,21 @@ def show_hint(self):
from_coords = move['from']
to_coords = move['to']
self.game_board.select_piece(from_coords[0], from_coords[1])
self.animate_swap(to_coords[0], to_coords[1])
self._animate_swap(to_coords[0], to_coords[1])
self.game_board.select_piece(from_coords[0], from_coords[1])
self.animate_swap(to_coords[0], to_coords[1])
self._animate_swap(to_coords[0], to_coords[1])
self._last_update_time = ticks_ms() # Reset hint timer

def swap_selected_piece(self, column, row):
def _swap_selected_piece(self, column, row):
""" Swap the selected piece with the piece at the specified column and row.
If the swap is not valid, revert to the previous selection. """
old_coords = self.game_board.selected_coords
self.animate_swap(column, row)
if not self.update():
self._animate_swap(column, row)
if not self._update_board():
self.game_board.select_piece(column, row, show_selector=False)
self.animate_swap(old_coords[0], old_coords[1])
self._animate_swap(old_coords[0], old_coords[1])

def animate_swap(self, column, row):
def _animate_swap(self, column, row):
""" Copy the pieces to separate tilegrids, animate the swap, and update the game board. """
if 0 <= column < self.game_board.columns and 0 <= row < self.game_board.rows:
selected_coords = self.game_board.selected_coords
Expand Down Expand Up @@ -271,11 +298,12 @@ def animate_swap(self, column, row):
# Deselect the selected piece (which sets the value)
self.game_board.select_piece(column, row)

def apply_gravity(self):
def _apply_gravity(self):
""" Go through each column from the bottom up and move pieces down
continue until there are no more pieces to move """
# pylint:disable=too-many-nested-blocks
while True:
self.pressed_btns = self._mouse.update()
moved = False
for x in range(self.game_board.columns):
for y in range(self.game_board.rows - 1, -1, -1):
Expand All @@ -295,7 +323,7 @@ def apply_gravity(self):
if not moved:
break

def check_for_matches(self):
def _check_for_matches(self):
""" Scan the game board for matches of 3 or more in a row or column """
matches = []
for x in range(self.game_board.columns):
Expand Down Expand Up @@ -325,35 +353,37 @@ def check_for_matches(self):
matches.append(vertical_match)
return matches

def update(self):
def _update_board(self):
""" Update the game logic, check for matches, and apply gravity. """
matches_found = False
multiplier = 1
matches = self.check_for_matches()
matches = self._check_for_matches()
while matches:
if matches:
for match in matches:
for x, y in match:
self.game_board.remove_game_piece(x, y)
self._score += 10 * multiplier * len(matches) * (len(match) - 2)
time.sleep(0.5) # Pause to show the match removal
self.apply_gravity()
self._apply_gravity()
matches_found = True
matches = self.check_for_matches()
matches = self._check_for_matches()
multiplier += 1
self._available_moves = self.find_all_possible_matches()
self._available_moves = self._find_all_possible_matches()
print(f"{len(self._available_moves)} available moves found.")
return matches_found

def reset(self):
""" Reset the game board and score. """
print("Reset started")
self.game_board.reset()
self._score = 0
self._last_update_time = ticks_ms()
self.apply_gravity()
self.update()
self._apply_gravity()
self._update_board()
print("Reset completed")

def check_match_after_move(self, row, column, direction, move_type='horizontal'):
def _check_match_after_move(self, row, column, direction, move_type='horizontal'):
""" Move the piece in a copy of the board to see if it creates a match."""
if move_type == 'horizontal':
new_row, new_column = row, column + direction
Expand All @@ -371,23 +401,23 @@ def check_match_after_move(self, row, column, direction, move_type='horizontal')
new_grid[row][column], new_grid[new_row][new_column] = new_grid[new_row][new_column], piece

# Check for horizontal matches at the new position
horizontal_match = self.check_horizontal_match(new_grid, new_row, new_column, piece)
horizontal_match = self._check_horizontal_match(new_grid, new_row, new_column, piece)

# Check for vertical matches at the new position
vertical_match = self.check_vertical_match(new_grid, new_row, new_column, piece)
vertical_match = self._check_vertical_match(new_grid, new_row, new_column, piece)

# Also check the original position for matches after the swap
original_piece = new_grid[row][column]
horizontal_match_orig = self.check_horizontal_match(new_grid, row, column, original_piece)
vertical_match_orig = self.check_vertical_match(new_grid, row, column, original_piece)
horizontal_match_orig = self._check_horizontal_match(new_grid, row, column, original_piece)
vertical_match_orig = self._check_vertical_match(new_grid, row, column, original_piece)

all_matches = (horizontal_match + vertical_match +
horizontal_match_orig + vertical_match_orig)

return True, len(all_matches) > 0

@staticmethod
def check_horizontal_match(grid, row, column, piece):
def _check_horizontal_match(grid, row, column, piece):
"""Check for horizontal 3-in-a-row matches centered
around or including the given position."""
matches = []
Expand All @@ -406,7 +436,7 @@ def check_horizontal_match(grid, row, column, piece):
return matches

@staticmethod
def check_vertical_match(grid, row, column, piece):
def _check_vertical_match(grid, row, column, piece):
"""Check for vertical 3-in-a-row matches centered around or including the given position."""
matches = []
rows = len(grid)
Expand All @@ -429,7 +459,7 @@ def check_for_game_over(self):
return True
return False

def find_all_possible_matches(self):
def _find_all_possible_matches(self):
"""
Scan the entire game board to find all possible moves that would create a 3-in-a-row match.
"""
Expand All @@ -438,31 +468,35 @@ def find_all_possible_matches(self):
for row in range(self.game_board.rows):
for column in range(self.game_board.columns):
# Check move right
can_move, creates_match = self.check_match_after_move(row, column, 1, 'horizontal')
can_move, creates_match = self._check_match_after_move(
row, column, 1, 'horizontal')
if can_move and creates_match:
possible_moves.append({
'from': (column, row),
'to': (column + 1, row),
})

# Check move left
can_move, creates_match = self.check_match_after_move(row, column, -1, 'horizontal')
can_move, creates_match = self._check_match_after_move(
row, column, -1, 'horizontal')
if can_move and creates_match:
possible_moves.append({
'from': (column, row),
'to': (column - 1, row),
})

# Check move down
can_move, creates_match = self.check_match_after_move(row, column, 1, 'vertical')
can_move, creates_match = self._check_match_after_move(
row, column, 1, 'vertical')
if can_move and creates_match:
possible_moves.append({
'from': (column, row),
'to': (column, row + 1),
})

# Check move up
can_move, creates_match = self.check_match_after_move(row, column, -1, 'vertical')
can_move, creates_match = self._check_match_after_move(
row, column, -1, 'vertical')
if can_move and creates_match:
possible_moves.append({
'from': (column, row),
Expand Down