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.