Fix many bugs
This commit is contained in:
parent
08dc8367c5
commit
f476d0a6c6
2 changed files with 94 additions and 65 deletions
|
|
@ -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()
|
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue