The code for the tile-matching game is divided into 3 files: The main code (which glues everything together), the game logic, and a custom UI element that was reused from the Minesweeper on Metro RP2350 guide. In many games, I try and separate the game logic from the user interface, which makes porting to other platforms much easier.
Main Code
The main code.py file handles setting everything up, responding to the user inputs, and updating the User Interface. Everything within the UI is handled by CircuitPython's displayio. There are some custom controls such as dialogs made using the TextBox component of the adafruit_display_text library as well as an event button, which will be looked at in more detail below. This file is fairly self explanatory with comments throughout the file.
The setup mainly consists of setting up the game board, setting up the dialogs, and setting up the score label and reset button. It also creates some tilegrids that are used to animate the piece swaps.
# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams for Adafruit Industries # SPDX-License-Identifier: MIT """ 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 from eventbutton import EventButton import supervisor import terminalio from adafruit_usb_host_mouse import find_and_init_boot_mouse from gamelogic import GameLogic, SELECTOR_SPRITE, EMPTY_SPRITE, GAMEBOARD_POSITION GAMEBOARD_SIZE = (8, 7) HINT_TIMEOUT = 10 # seconds before hint is shown GAME_PIECES = 7 # Number of different game pieces (set between 3 and 8) # pylint: disable=ungrouped-imports if hasattr(supervisor.runtime, "display") and supervisor.runtime.display is not None: # use the built-in HSTX display for Metro RP2350 display = supervisor.runtime.display else: # pylint: disable=ungrouped-imports from displayio import release_displays import picodvi import board import framebufferio # initialize display release_displays() fb = picodvi.Framebuffer( 320, 240, clk_dp=board.CKP, clk_dn=board.CKN, red_dp=board.D0P, red_dn=board.D0N, green_dp=board.D1P, green_dn=board.D1N, blue_dp=board.D2P, blue_dn=board.D2N, color_depth=16, ) display = framebufferio.FramebufferDisplay(fb) def get_color_index(color, shader=None): for index, palette_color in enumerate(shader): if palette_color == color: return index return None # Load the spritesheet sprite_sheet = OnDiskBitmap("/bitmaps/game_sprites.bmp") sprite_sheet.pixel_shader.make_transparent( get_color_index(0x00ff00, sprite_sheet.pixel_shader) ) # Main group will hold all the visual layers main_group = Group() display.root_group = main_group # Add Background to the Main Group background = Bitmap(display.width, display.height, 1) bg_color = Palette(1) bg_color[0] = 0x333333 main_group.append(TileGrid( background, pixel_shader=bg_color )) # Add Game grid, which holds the game board, to the main group game_grid = TileGrid( sprite_sheet, pixel_shader=sprite_sheet.pixel_shader, width=GAMEBOARD_SIZE[0], height=GAMEBOARD_SIZE[1], tile_width=32, tile_height=32, x=GAMEBOARD_POSITION[0], y=GAMEBOARD_POSITION[1], default_tile=EMPTY_SPRITE, ) main_group.append(game_grid) # Add a special selection groupd to highlight the selected piece and allow animation selected_piece_group = Group() selected_piece = TileGrid( sprite_sheet, pixel_shader=sprite_sheet.pixel_shader, width=1, height=1, tile_width=32, tile_height=32, x=0, y=0, default_tile=EMPTY_SPRITE, ) selected_piece_group.append(selected_piece) selector = TileGrid( sprite_sheet, pixel_shader=sprite_sheet.pixel_shader, width=1, height=1, tile_width=32, tile_height=32, x=0, y=0, default_tile=SELECTOR_SPRITE, ) selected_piece_group.append(selector) selected_piece_group.hidden = True main_group.append(selected_piece_group) # Add a group for the swap piece to help with animation swap_piece = TileGrid( sprite_sheet, pixel_shader=sprite_sheet.pixel_shader, width=1, height=1, tile_width=32, tile_height=32, x=0, y=0, default_tile=EMPTY_SPRITE, ) swap_piece.hidden = True main_group.append(swap_piece) # Add foreground foreground_bmp = OnDiskBitmap("/bitmaps/foreground.bmp") foreground_bmp.pixel_shader.make_transparent(0) foreground_tg = TileGrid(foreground_bmp, pixel_shader=foreground_bmp.pixel_shader) foreground_tg.x = 0 foreground_tg.y = 0 main_group.append(foreground_tg) # Add a group for the UI Elements 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, HINT_TIMEOUT ) def update_ui(): # Update the UI elements with the current game state score_label.text = f"Score:\n{game_logic.score}" waiting_for_release = False game_over_shown = False # Create the UI Elements # Label for the Score score_label = Label( terminalio.FONT, color=0xffff00, x=5, y=10, ) ui_group.append(score_label) message_dialog = Group() message_dialog.hidden = True def reset(): global game_over_shown # pylint: disable=global-statement # Reset the game logic game_logic.reset() message_dialog.hidden = True game_over_shown = False def hide_group(group): group.hidden = True reset() reset_button = EventButton( reset, label="Reset", width=40, height=16, x=5, y=50, style=EventButton.RECT, ) ui_group.append(reset_button) message_label = TextBox( terminalio.FONT, text="", color=0x333333, background_color=0xEEEEEE, width=display.width // 3, height=90, align=TextBox.ALIGN_CENTER, padding_top=5, ) message_label.anchor_point = (0, 0) message_label.anchored_position = ( display.width // 2 - message_label.width // 2, display.height // 2 - message_label.height // 2, ) message_dialog.append(message_label) message_button = EventButton( (hide_group, message_dialog), label="OK", width=40, height=16, x=display.width // 2 - 20, y=display.height // 2 - message_label.height // 2 + 60, style=EventButton.RECT, ) message_dialog.append(message_button) ui_group.append(message_dialog) # main loop while True: update_ui() # update mouse game_logic.update_mouse() if not message_dialog.hidden: 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), 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_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
Game Logic
The game logic handles the Tile-matching game logic. I came up with the majority of the code and used Claude to come up with the logic to check for remaining moves, which I then used to check if the game was over as well as showing hints.
The file includes a couple of different classes. That is the GameBoard
class, which helps keep track of the state of various elements, and the GameLogic
class, which applies game logic based on the current conditions.
This file also includes a few settings, but they really shouldn't be altered unless you have a good reason. The GAMEBOARD_POSITION
is meant to represent the upper left-hand corner of where the game board starts. The SPRITE
variables are for the sprite indices and the DEBOUNCE_TIME
is the amount of delay in seconds so that the mouse doesn't accidentally double click.
GAMEBOARD_POSITION = (55, 8) SELECTOR_SPRITE = 9 EMPTY_SPRITE = 10 DEBOUNCE_TIME = 0.1 # seconds for debouncing mouse clicks
One of the more interesting functions in the Game Logic is apply_gravity
. This scans through the board column by column and if there is an empty tile, the piece above it is moved down. If the top row is empty, a new piece is generated. It keeps doing this until nothing changes.
The update
function is where the score is calculated. It checks for any matches, removes the tiles, and updates the score. The score is calculated based on the number of pieces in a match, the number of simultaneous matches, and the length of the chain of moves done.
The code to check if there are any move moves is interesting as well. The functions check_match_after_move
, check_horizontal_match
, check_vertical_match
, and find_all_possible_matches
work together to find these matches. This is done by scanning each piece and then attempting to make a move using a virtual board. This virtual board is simply a copy of the board as an array made through the Game Board's game_grid_copy
function. By doing all of this using simple structures, the algorithm is fairly quick, but is only performed during an update and saved into an available moves list to avoid lag.
The other interesting functionality is the code to show a hint. It takes one of the moves from the available moves list and performs a swap animation. Animating the tile swaps will be covered in more detail in the Animations section of this guide.
Most of the mouse handling code was also placed into the game logic because while the pieces are being shifted around, the mouse needs to continue to be updated or the buffer gets full and it stops responding.
# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams for Adafruit Industries # SPDX-License-Identifier: MIT import random import time from adafruit_ticks import ticks_ms GAMEBOARD_POSITION = (55, 8) SELECTOR_SPRITE = 9 EMPTY_SPRITE = 10 DEBOUNCE_TIME = 0.2 # seconds for debouncing mouse clicks class GameBoard: "Contains the game board" def __init__(self, game_grid, swap_piece, selected_piece_group): self.x = GAMEBOARD_POSITION[0] self.y = GAMEBOARD_POSITION[1] self._game_grid = game_grid self._selected_coords = None self._selected_piece = selected_piece_group[0] self._selector = selected_piece_group[1] self._swap_piece = swap_piece self.selected_piece_group = selected_piece_group def add_game_piece(self, column, row, piece_type): if 0 <= column < self.columns and 0 <= row < self.rows: if self._game_grid[(column, row)] != EMPTY_SPRITE: raise ValueError("Position already occupied") self._game_grid[(column, row)] = piece_type else: raise IndexError("Position out of bounds") def remove_game_piece(self, column, row): if 0 <= column < self.columns and 0 <= row < self.rows: self._game_grid[(column, row)] = EMPTY_SPRITE else: raise IndexError("Position out of bounds") def reset(self): for column in range(self.columns): 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: if 0 <= new_x < self.columns and 0 <= new_y < self.rows: if self._game_grid[(new_x, new_y)] == EMPTY_SPRITE: self._game_grid[(new_x, new_y)] = self._game_grid[(old_x, old_y)] self._game_grid[(old_x, old_y)] = EMPTY_SPRITE else: raise ValueError("New position already occupied") else: raise IndexError("New position out of bounds") else: raise IndexError("Old position out of bounds") @property def columns(self): return self._game_grid.width @property def rows(self): return self._game_grid.height @property def selected_piece(self): if self._selected_coords is not None and self._selected_piece[0] != EMPTY_SPRITE: return self._selected_piece[0] return None @property def swap_piece(self): return self._swap_piece def set_swap_piece(self, column, row): # Set the swap piece to the piece at the specified coordinates piece = self.get_piece(column, row) if self._swap_piece[0] is None and self._swap_piece[0] == EMPTY_SPRITE: raise ValueError("Can't swap an empty piece") if self._swap_piece.hidden: self._swap_piece[0] = piece self._swap_piece.x = column * 32 + self.x self._swap_piece.y = row * 32 + self.y self._swap_piece.hidden = False self._game_grid[(column, row)] = EMPTY_SPRITE else: self._game_grid[(column, row)] = self._swap_piece[0] self._swap_piece[0] = EMPTY_SPRITE self._swap_piece.hidden = True @property def selected_coords(self): if self._selected_coords is not None: return self._selected_coords return None @property def selector_hidden(self): return self._selector.hidden @selector_hidden.setter def selector_hidden(self, value): # Set the visibility of the selector self._selector.hidden = value def set_selected_coords(self, column, row): # Set the selected coordinates to the specified column and row if 0 <= column < self.columns and 0 <= row < self.rows: self._selected_coords = (column, row) self.selected_piece_group.x = column * 32 + self.x self.selected_piece_group.y = row * 32 + self.y else: raise IndexError("Selected coordinates out of bounds") def select_piece(self, column, row, show_selector=True): # Take care of selecting a piece piece = self.get_piece(column, row) if self.selected_piece is None and piece == EMPTY_SPRITE: # If no piece is selected and the clicked piece is empty, do nothing return if (self.selected_piece is not None and (self._selected_coords[0] != column or self._selected_coords[1] != row)): # If a piece is already selected and the coordinates don't match, do nothing return if self.selected_piece is None: # No piece selected, so select the specified piece self._selected_piece[0] = self.get_piece(column, row) self._selected_coords = (column, row) self.selected_piece_group.x = column * 32 + self.x self.selected_piece_group.y = row * 32 + self.y self.selected_piece_group.hidden = False self.selector_hidden = not show_selector self._game_grid[(column, row)] = EMPTY_SPRITE else: self._game_grid[(column, row)] = self._selected_piece[0] self._selected_piece[0] = EMPTY_SPRITE self.selected_piece_group.hidden = True self._selected_coords = None def get_piece(self, column, row): if 0 <= column < self.columns and 0 <= row < self.rows: return self._game_grid[(column, row)] return None @property def game_grid_copy(self): # Return a copy of the game grid as a 2D list return [[self._game_grid[(x, y)] for x in range(self.columns)] for y in range(self.rows)] class GameLogic: "Contains the Logic to examine the game board and determine if a move is valid." 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 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 if ticks_ms() <= self._last_click_time + (DEBOUNCE_TIME * 1000): print("Debouncing click, too soon after last click.") return self._last_click_time = ticks_ms() # Update last click time column, row = coords self._last_update_time = ticks_ms() # Check if the clicked piece is valid if not 0 <= column < self.game_board.columns or not 0 <= row < self.game_board.rows: print(f"Clicked coordinates ({column}, {row}) are out of bounds.") return # If clicked piece is empty and no piece is selected, do nothing if (self.game_board.get_piece(column, row) == EMPTY_SPRITE and self.game_board.selected_piece is None): print(f"No piece at ({column}, {row}) and no piece selected.") return if self.game_board.selected_piece is None: # If no piece is selected, select the piece at the clicked coordinates self.game_board.select_piece(column, row) return if (self.game_board.selected_coords is not None and (self.game_board.selected_coords[0] == column and self.game_board.selected_coords[1] == row)): # If the clicked piece is already selected, deselect it self.game_board.select_piece(column, row) return # If piece is selected and the new coordinates are 1 position # away horizontally or vertically, swap the pieces if self.game_board.selected_coords is not None: previous_x, previous_y = self.game_board.selected_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) def show_hint(self): """ Show a hint by selecting a random available move and swapping the pieces back and forth. """ if self._available_moves: move = random.choice(self._available_moves) 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.game_board.select_piece(from_coords[0], from_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): """ 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_board(): self.game_board.select_piece(column, row, show_selector=False) self._animate_swap(old_coords[0], old_coords[1]) 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 if selected_coords is None: print("No piece selected to swap.") return # Set the swap piece value to the column, row value self.game_board.set_swap_piece(column, row) self.game_board.selector_hidden = True # Calculate the steps for animation to move the pieces in the correct direction selected_piece_steps = ( (self.game_board.swap_piece.x - self.game_board.selected_piece_group.x) // 32, (self.game_board.swap_piece.y - self.game_board.selected_piece_group.y) // 32 ) swap_piece_steps = ( (self.game_board.selected_piece_group.x - self.game_board.swap_piece.x) // 32, (self.game_board.selected_piece_group.y - self.game_board.swap_piece.y) // 32 ) # Move the tilegrids in small steps to create an animation effect for _ in range(32): # Move the selected piece tilegrid to the swap piece position self.game_board.selected_piece_group.x += selected_piece_steps[0] self.game_board.selected_piece_group.y += selected_piece_steps[1] # Move the swap piece tilegrid to the selected piece position self.game_board.swap_piece.x += swap_piece_steps[0] self.game_board.swap_piece.y += swap_piece_steps[1] time.sleep(0.002) # Set the existing selected piece coords to the swap piece value self.game_board.set_swap_piece(selected_coords[0], selected_coords[1]) # Update the selected piece coordinates to the new column, row self.game_board.set_selected_coords(column, row) # Deselect the selected piece (which sets the value) self.game_board.select_piece(column, row) 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): piece = self.game_board.get_piece(x, y) if piece != EMPTY_SPRITE: # Check if the piece can fall for new_y in range(y + 1, self.game_board.rows): if self.game_board.get_piece(x, new_y) == EMPTY_SPRITE: # Move the piece down self.game_board.move_game_piece(x, y, x, new_y) moved = True break # If the piece was in the top slot before falling, add a new piece if y == 0 and self.game_board.get_piece(x, 0) == EMPTY_SPRITE: self.game_board.add_game_piece(x, 0, random.randint(0, self._game_pieces)) moved = True if not moved: break 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): for y in range(self.game_board.rows): piece = self.game_board.get_piece(x, y) if piece != EMPTY_SPRITE: # Check horizontal matches horizontal_match = [(x, y)] for dx in range(1, 3): if (x + dx < self.game_board.columns and self.game_board.get_piece(x + dx, y) == piece): horizontal_match.append((x + dx, y)) else: break if len(horizontal_match) >= 3: matches.append(horizontal_match) # Check vertical matches vertical_match = [(x, y)] for dy in range(1, 3): if (y + dy < self.game_board.rows and self.game_board.get_piece(x, y + dy) == piece): vertical_match.append((x, y + dy)) else: break if len(vertical_match) >= 3: matches.append(vertical_match) return matches def _update_board(self): """ Update the game logic, check for matches, and apply gravity. """ matches_found = False multiplier = 1 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() matches_found = True matches = self._check_for_matches() multiplier += 1 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_board() print("Reset completed") 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 else: # vertical new_row, new_column = row + direction, column # Check if move is within bounds if (new_row < 0 or new_row >= self.game_board.rows or new_column < 0 or new_column >= self.game_board.columns): return False, False # Create a copy of the grid with the moved piece new_grid = self.game_board.game_grid_copy piece = new_grid[row][column] 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) # Check for vertical matches at the new position 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) 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): """Check for horizontal 3-in-a-row matches centered around or including the given position.""" matches = [] columns = len(grid[0]) # Check all possible 3-piece horizontal combinations that include this position for start_column in range(max(0, column - 2), min(columns - 2, column + 1)): if (start_column + 2 < columns and grid[row][start_column] == piece and grid[row][start_column + 1] == piece and grid[row][start_column + 2] == piece): matches.append([(row, start_column), (row, start_column + 1), (row, start_column + 2)]) return matches @staticmethod 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) # Check all possible 3-piece vertical combinations that include this position for start_row in range(max(0, row - 2), min(rows - 2, row + 1)): if (start_row + 2 < rows and grid[start_row][column] == piece and grid[start_row + 1][column] == piece and grid[start_row + 2][column] == piece): matches.append([(start_row, column), (start_row + 1, column), (start_row + 2, column)]) return matches def check_for_game_over(self): """ Check if there are no available moves left on the game board. """ if not self._available_moves: return True return False 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. """ possible_moves = [] 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') 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') 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') 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') if can_move and creates_match: possible_moves.append({ 'from': (column, row), 'to': (column, row - 1), }) # Remove duplicates because from and to can be reversed unique_moves = set() for move in possible_moves: from_coords = tuple(move['from']) to_coords = tuple(move['to']) if from_coords > to_coords: unique_moves.add((to_coords, from_coords)) else: unique_moves.add((from_coords, to_coords)) possible_moves = [{'from': move[0], 'to': move[1]} for move in unique_moves] return possible_moves @property def score(self): return self._score @property def time_since_last_update(self): return (ticks_ms() - self._last_update_time) / 1000.0
Event Button
This button was reused from the Minesweeper guide. The event button builds on the standard button available in the adafruit_button library. It adds the option to specify a callback function when the button is clicked as well as some mouse handling code so that a click is only registered if another element wasn't already selected and the click is within the boundaries of the button. This way if another UI element is selected and the mouse is dragged onto the button, it is handled properly.
# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams for Adafruit Industries # SPDX-License-Identifier: MIT from adafruit_button import Button class EventButton(Button): """A button that can be used to trigger a callback when clicked. :param callback: The callback function to call when the button is clicked. A tuple can be passed with an argument that will be passed to the callback function. The first element of the tuple should be the callback function, and the remaining elements will be passed as arguments to the callback function. """ def __init__(self, callback, *args, **kwargs): super().__init__(*args, **kwargs) self.args = [] self.selected = False if isinstance(callback, tuple): self.callback = callback[0] self.args = callback[1:] else: self.callback = callback def click(self): """Call the function when the button is pressed.""" self.callback(*self.args) def handle_mouse(self, point, clicked, waiting_for_release): if waiting_for_release: return False # Handle mouse events for the button if self.contains(point): self.selected = True if clicked: self.click() return True else: self.selected = False return False
Page last edited June 11, 2025
Text editor powered by tinymce.