Fix many bugs

This commit is contained in:
Melissa LeBlanc-Williams 2025-06-04 14:34:54 -07:00
parent 08dc8367c5
commit f476d0a6c6
2 changed files with 94 additions and 65 deletions

View file

@ -4,6 +4,7 @@
An implementation of a match3 jewel swap game. The idea is to move one character at a time 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. to line up at least 3 characters.
""" """
import time
from displayio import Group, OnDiskBitmap, TileGrid, Bitmap, Palette from displayio import Group, OnDiskBitmap, TileGrid, Bitmap, Palette
from adafruit_display_text.bitmap_label import Label from adafruit_display_text.bitmap_label import Label
from adafruit_display_text.text_box import TextBox from adafruit_display_text.text_box import TextBox
@ -141,22 +142,25 @@ main_group.append(foreground_tg)
ui_group = Group() ui_group = Group()
main_group.append(ui_group) main_group.append(ui_group)
# Create the game logic object
# pylint: disable=no-value-for-parameter, too-many-function-args
game_logic = GameLogic(
display,
game_grid,
swap_piece,
selected_piece_group,
GAME_PIECES
)
# Create the mouse graphics and add to the main 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") mouse = find_and_init_boot_mouse("/bitmaps/mouse_cursor.bmp")
if mouse is None: if mouse is None:
raise RuntimeError("No mouse found connected to USB Host") raise RuntimeError("No mouse found connected to USB Host")
main_group.append(mouse.tilegrid) 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,
HINT_TIMEOUT
)
def update_ui(): def update_ui():
# Update the UI elements with the current game state # Update the UI elements with the current game state
score_label.text = f"Score:\n{game_logic.score}" score_label.text = f"Score:\n{game_logic.score}"
@ -232,38 +236,29 @@ ui_group.append(message_dialog)
while True: while True:
update_ui() update_ui()
# update mouse # update mouse
pressed_btns = mouse.update() game_logic.update_mouse()
if waiting_for_release and not pressed_btns:
# If both buttons are released, we can process the next click
waiting_for_release = False
if not message_dialog.hidden: if not message_dialog.hidden:
if message_button.handle_mouse((mouse.x, mouse.y), if message_button.handle_mouse(
pressed_btns and "left" in pressed_btns, (mouse.x, mouse.y),
waiting_for_release): game_logic.pressed_btns and "left" in game_logic.pressed_btns,
waiting_for_release = True waiting_for_release
):
game_logic.waiting_for_release = True
continue continue
if reset_button.handle_mouse((mouse.x, mouse.y), if reset_button.handle_mouse(
pressed_btns and "left" in pressed_btns, (mouse.x, mouse.y),
waiting_for_release): game_logic.pressed_btns is not None and "left" in game_logic.pressed_btns,
waiting_for_release = True game_logic.waiting_for_release
):
game_logic.waiting_for_release = True
# process gameboard click if no menu # process gameboard click if no menu
game_board = game_logic.game_board game_logic.update()
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_over = game_logic.check_for_game_over() game_over = game_logic.check_for_game_over()
if game_over and not game_over_shown: if game_over and not game_over_shown:
message_label.text = ("No more moves available. your final score is:\n" message_label.text = ("No more moves available. your final score is:\n"
+ str(game_logic.score)) + str(game_logic.score))
message_dialog.hidden = False message_dialog.hidden = False
game_over_shown = True game_over_shown = True
if game_logic.time_since_last_update > HINT_TIMEOUT:
game_logic.show_hint()

View file

@ -9,7 +9,7 @@ GAMEBOARD_POSITION = (55, 8)
SELECTOR_SPRITE = 9 SELECTOR_SPRITE = 9
EMPTY_SPRITE = 10 EMPTY_SPRITE = 10
DEBOUNCE_TIME = 0.1 # seconds for debouncing mouse clicks DEBOUNCE_TIME = 0.2 # seconds for debouncing mouse clicks
class GameBoard: class GameBoard:
"Contains the game board" "Contains the game board"
@ -42,6 +42,10 @@ class GameBoard:
for row in range(self.rows): for row in range(self.rows):
if self._game_grid[(column, row)] != EMPTY_SPRITE: if self._game_grid[(column, row)] != EMPTY_SPRITE:
self.remove_game_piece(column, row) 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): 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: if 0 <= old_x < self.columns and 0 <= old_y < self.rows:
@ -153,18 +157,41 @@ class GameBoard:
class GameLogic: class GameLogic:
"Contains the Logic to examine the game board and determine if a move is valid." "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._display = display
self._mouse = mouse
self.game_board = GameBoard(game_grid, swap_piece, selected_piece_group) self.game_board = GameBoard(game_grid, swap_piece, selected_piece_group)
self._score = 0 self._score = 0
self._available_moves = [] self._available_moves = []
if not 3 <= game_pieces <= 8: if not 3 <= game_pieces <= 8:
raise ValueError("game_pieces must be between 3 and 8") raise ValueError("game_pieces must be between 3 and 8")
self._game_pieces = game_pieces # Number of different game pieces 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_update_time = ticks_ms() # For hint timing
self._last_click_time = ticks_ms() # For debouncing mouse clicks 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. """ """ Handle a piece click event. """
if ticks_ms() <= self._last_click_time: if ticks_ms() <= self._last_click_time:
self._last_click_time -= 2**29 # ticks_ms() wraps around after 2**29 ms self._last_click_time -= 2**29 # ticks_ms() wraps around after 2**29 ms
@ -206,7 +233,7 @@ class GameLogic:
if ((abs(previous_x - column) == 1 and previous_y == row) or if ((abs(previous_x - column) == 1 and previous_y == row) or
(previous_x == column and abs(previous_y - row) == 1)): (previous_x == column and abs(previous_y - row) == 1)):
# Swap the pieces # Swap the pieces
self.swap_selected_piece(column, row) self._swap_selected_piece(column, row)
def show_hint(self): def show_hint(self):
""" Show a hint by selecting a random available """ Show a hint by selecting a random available
@ -216,21 +243,21 @@ class GameLogic:
from_coords = move['from'] from_coords = move['from']
to_coords = move['to'] to_coords = move['to']
self.game_board.select_piece(from_coords[0], from_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.game_board.select_piece(from_coords[0], from_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 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. """ Swap the selected piece with the piece at the specified column and row.
If the swap is not valid, revert to the previous selection. """ If the swap is not valid, revert to the previous selection. """
old_coords = self.game_board.selected_coords old_coords = self.game_board.selected_coords
self.animate_swap(column, row) self._animate_swap(column, row)
if not self.update(): if not self._update_board():
self.game_board.select_piece(column, row, show_selector=False) 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. """ """ 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: if 0 <= column < self.game_board.columns and 0 <= row < self.game_board.rows:
selected_coords = self.game_board.selected_coords selected_coords = self.game_board.selected_coords
@ -271,11 +298,12 @@ class GameLogic:
# Deselect the selected piece (which sets the value) # Deselect the selected piece (which sets the value)
self.game_board.select_piece(column, row) 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 """ Go through each column from the bottom up and move pieces down
continue until there are no more pieces to move """ continue until there are no more pieces to move """
# pylint:disable=too-many-nested-blocks # pylint:disable=too-many-nested-blocks
while True: while True:
self.pressed_btns = self._mouse.update()
moved = False moved = False
for x in range(self.game_board.columns): for x in range(self.game_board.columns):
for y in range(self.game_board.rows - 1, -1, -1): for y in range(self.game_board.rows - 1, -1, -1):
@ -295,7 +323,7 @@ class GameLogic:
if not moved: if not moved:
break 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 """ """ Scan the game board for matches of 3 or more in a row or column """
matches = [] matches = []
for x in range(self.game_board.columns): for x in range(self.game_board.columns):
@ -325,11 +353,11 @@ class GameLogic:
matches.append(vertical_match) matches.append(vertical_match)
return matches return matches
def update(self): def _update_board(self):
""" Update the game logic, check for matches, and apply gravity. """ """ Update the game logic, check for matches, and apply gravity. """
matches_found = False matches_found = False
multiplier = 1 multiplier = 1
matches = self.check_for_matches() matches = self._check_for_matches()
while matches: while matches:
if matches: if matches:
for match in matches: for match in matches:
@ -337,23 +365,25 @@ class GameLogic:
self.game_board.remove_game_piece(x, y) self.game_board.remove_game_piece(x, y)
self._score += 10 * multiplier * len(matches) * (len(match) - 2) self._score += 10 * multiplier * len(matches) * (len(match) - 2)
time.sleep(0.5) # Pause to show the match removal time.sleep(0.5) # Pause to show the match removal
self.apply_gravity() self._apply_gravity()
matches_found = True matches_found = True
matches = self.check_for_matches() matches = self._check_for_matches()
multiplier += 1 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.") print(f"{len(self._available_moves)} available moves found.")
return matches_found return matches_found
def reset(self): def reset(self):
""" Reset the game board and score. """ """ Reset the game board and score. """
print("Reset started")
self.game_board.reset() self.game_board.reset()
self._score = 0 self._score = 0
self._last_update_time = ticks_ms() self._last_update_time = ticks_ms()
self.apply_gravity() self._apply_gravity()
self.update() 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.""" """ Move the piece in a copy of the board to see if it creates a match."""
if move_type == 'horizontal': if move_type == 'horizontal':
new_row, new_column = row, column + direction new_row, new_column = row, column + direction
@ -371,15 +401,15 @@ class GameLogic:
new_grid[row][column], new_grid[new_row][new_column] = new_grid[new_row][new_column], piece 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 # 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 # 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 # Also check the original position for matches after the swap
original_piece = new_grid[row][column] original_piece = new_grid[row][column]
horizontal_match_orig = self.check_horizontal_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) vertical_match_orig = self._check_vertical_match(new_grid, row, column, original_piece)
all_matches = (horizontal_match + vertical_match + all_matches = (horizontal_match + vertical_match +
horizontal_match_orig + vertical_match_orig) horizontal_match_orig + vertical_match_orig)
@ -387,7 +417,7 @@ class GameLogic:
return True, len(all_matches) > 0 return True, len(all_matches) > 0
@staticmethod @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 """Check for horizontal 3-in-a-row matches centered
around or including the given position.""" around or including the given position."""
matches = [] matches = []
@ -406,7 +436,7 @@ class GameLogic:
return matches return matches
@staticmethod @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.""" """Check for vertical 3-in-a-row matches centered around or including the given position."""
matches = [] matches = []
rows = len(grid) rows = len(grid)
@ -429,7 +459,7 @@ class GameLogic:
return True return True
return False 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. Scan the entire game board to find all possible moves that would create a 3-in-a-row match.
""" """
@ -438,7 +468,8 @@ class GameLogic:
for row in range(self.game_board.rows): for row in range(self.game_board.rows):
for column in range(self.game_board.columns): for column in range(self.game_board.columns):
# Check move right # 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: if can_move and creates_match:
possible_moves.append({ possible_moves.append({
'from': (column, row), 'from': (column, row),
@ -446,7 +477,8 @@ class GameLogic:
}) })
# Check move left # 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: if can_move and creates_match:
possible_moves.append({ possible_moves.append({
'from': (column, row), 'from': (column, row),
@ -454,7 +486,8 @@ class GameLogic:
}) })
# Check move down # 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: if can_move and creates_match:
possible_moves.append({ possible_moves.append({
'from': (column, row), 'from': (column, row),
@ -462,7 +495,8 @@ class GameLogic:
}) })
# Check move up # 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: if can_move and creates_match:
possible_moves.append({ possible_moves.append({
'from': (column, row), 'from': (column, row),