In the Memory game, multiple players take turns playing the game. Each player will need to use the mouse to select cards to flip over and when their turn is over, the mouse is passed to the other player.
The code will need to keep track of which turn it is to assign points to the appropriate player when matching cards are found.
Before getting into all of the complexity that comes with the rest of the Memory game, a look at something simpler: an implementation of Tic-Tac-Toe. This Tic-Tac-Toe game will use the same core mechanism to keep track of player turns as the Memory game. As an added bonus, it also uses the same GridLayout technique to position its visual elements on the screen as the Memory game.
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
# SPDX-License-Identifier: MIT
"""
This example is made for a basic Microsoft optical mouse with
two buttons and a wheel that can be pressed.
It assumes there is a single mouse connected to USB Host,
and no other devices connected.
It illustrates multi-player turn based logic with a very
basic implementation of tic-tac-toe.
"""
import array
import random
from displayio import Group, OnDiskBitmap, TileGrid
from adafruit_display_text.bitmap_label import Label
from adafruit_displayio_layout.layouts.grid_layout import GridLayout
import supervisor
import terminalio
import usb.core
# 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:
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)
# group to hold visual elements
main_group = Group()
# make the group visible on the display
display.root_group = main_group
# load the mouse cursor bitmap
mouse_bmp = OnDiskBitmap("mouse_cursor.bmp")
# make the background pink pixels transparent
mouse_bmp.pixel_shader.make_transparent(0)
# create a TileGrid for the mouse, using its bitmap and pixel_shader
mouse_tg = TileGrid(mouse_bmp, pixel_shader=mouse_bmp.pixel_shader)
# move it to the center of the display
mouse_tg.x = display.width // 2
mouse_tg.y = display.height // 2
# text label to show the x, y coordinates on the screen
output_lbl = Label(terminalio.FONT, text="", color=0xFFFFFF, scale=1)
# move it to the right side of the screen
output_lbl.anchor_point = (0, 0)
output_lbl.anchored_position = (180, 40)
# add it to the main group
main_group.append(output_lbl)
# scan for connected USB device and loop over any found
for device in usb.core.find(find_all=True):
# print device info
print(f"{device.idVendor:04x}:{device.idProduct:04x}")
print(device.manufacturer, device.product)
print(device.serial_number)
# assume the device is the mouse
mouse = device
# detach the kernel driver if needed
if mouse.is_kernel_driver_active(0):
mouse.detach_kernel_driver(0)
# set configuration on the mouse so we can use it
mouse.set_configuration()
# buffer to hold mouse data
# Boot mice have 4 byte reports
buf = array.array("b", [0] * 4)
# set up a 3x3 grid for the tic-tac-toe board
board_grid = GridLayout(x=40, y=40, width=128, height=128, grid_size=(3, 3))
# load the tic-tac-toe spritesheet
tictactoe_spritesheet = OnDiskBitmap("tictactoe_spritesheet.bmp")
# X is index 1 in the spritesheet, O is index 2 in the spritesheet
player_icon_indexes = [1, 2]
# current player variable.
# When this equlas 0 its X's turn,
# when it equals 1 it is O's turn.
current_player_index = random.randint(0, 1) # randomize the initial player
# loop over rows
for y in range(3):
# loop over columns
for x in range(3):
# create a TileGrid for this cell
new_tg = TileGrid(
bitmap=tictactoe_spritesheet,
default_tile=0,
tile_height=32,
tile_width=32,
height=1,
width=1,
pixel_shader=tictactoe_spritesheet.pixel_shader,
)
# add the new TileGrid to the board grid at the current position
board_grid.add_content(new_tg, grid_position=(x, y), cell_size=(1, 1))
# add the board grid to the main group
main_group.append(board_grid)
# add the mouse tile grid to the main group
main_group.append(mouse_tg)
def check_for_winner():
"""
check if a player has won
:return: the player icon index of the winning player,
None if no winner and game continues, -1 if game ended in a tie.
"""
found_empty = False
# check rows
for row_idx in range(3):
# if the 3 cells in this row match
if (
board_grid[0 + (row_idx * 3)][0] != 0
and board_grid[0 + (row_idx * 3)][0]
== board_grid[1 + (row_idx * 3)][0]
== board_grid[2 + (row_idx * 3)][0]
):
return board_grid[0 + (row_idx * 3)][0]
# if any of the cells in this row are empty
if 0 in (
board_grid[0 + (row_idx * 3)][0],
board_grid[1 + (row_idx * 3)][0],
board_grid[2 + (row_idx * 3)][0],
):
found_empty = True
# check columns
for col_idx in range(3):
# if the 3 cells in this column match
if (
board_grid[0 + col_idx][0] != 0
and board_grid[0 + col_idx][0]
== board_grid[3 + col_idx][0]
== board_grid[6 + col_idx][0]
):
return board_grid[0 + col_idx][0]
# if any of the cells in this column are empty
if 0 in (
board_grid[0 + col_idx][0],
board_grid[3 + col_idx][0],
board_grid[6 + col_idx][0],
):
found_empty = True
# check diagonals
if (
board_grid[0][0] != 0
and board_grid[0][0] == board_grid[4][0] == board_grid[8][0]
):
return board_grid[0][0]
if (
board_grid[2][0] != 0
and board_grid[2][0] == board_grid[4][0] == board_grid[6][0]
):
return board_grid[2][0]
if found_empty:
# return None if there is no winner and the game continues
return None
else:
# return -1 if it's a tie game with no winner
return -1
# main loop
while True:
try:
# attempt to read data from the mouse
# 10ms timeout, so we don't block long if there
# is no data
count = mouse.read(0x81, buf, timeout=10)
except usb.core.USBTimeoutError:
# skip the rest of the loop if there is no data
continue
# update the mouse tilegrid x and y coordinates
# based on the delta values read from the mouse
mouse_tg.x = max(0, min(display.width - 1, mouse_tg.x + buf[1]))
mouse_tg.y = max(0, min(display.height - 1, mouse_tg.y + buf[2]))
# if left button clicked
if buf[0] & (1 << 0) != 0:
# get the mouse pointer coordinates accounting for the offset of
# the board grid location
coords = (mouse_tg.x - board_grid.x, mouse_tg.y - board_grid.y, 0)
# loop over all cells in the board
for cell_tg in board_grid:
# if the current cell is blank, and contains the clicked coordinates
if cell_tg[0] == 0 and cell_tg.contains(coords):
# set the current cell tile index to the current player's icon
cell_tg[0] = player_icon_indexes[current_player_index]
# change to the next player
current_player_index = (current_player_index + 1) % 2
# print out which player's turn it is
print(f"It is now {'X' if current_player_index == 0 else 'O'}'s turn")
# check for a winner
result = check_for_winner()
# if Xs or Os have won
if result == 1:
output_lbl.text = "X is the winner!"
elif result == 2:
output_lbl.text = "O is the winner!"
# if it was a tie game
elif result == -1:
output_lbl.text = "Tie game, no winner."
The code contains comments describing the purpose each section. A overview of the code functionality can be found below. Reading both will give you a good understanding of core principals that will get used by the Memory game.
The code makes use of things detailed on the Mouse Input guide page, see that page for more information about how the mouse data is read and the cursor drawn and moved around the display.
Visual Elements Setup
In addition to the mouse cursor, this code also creates board_grid, a GridLayout object, which will hold the cells that make up the Tic-Tac-Toe board. The GridLayout takes a height and width in pixels and a grid_size when it is initialized and will automatically distribute the content items added into the cells of the grid using the values specified to determine the appropriate placement.
The tictactoe_spritesheet.bmp is loaded into an OnDiskBitmap.
A set of nested for loops with y and x counter variables are used to create a TileGrid for each cell on the board and add it to the board_grid. Each TileGrid is set to default_tile=0 which is the index of the blank cell within the spritesheet.
Player Turn Setup & Logic
An integer variable, current_player_index, is created to store a number which will keep track of which player's turn it is. Tic-Tac-Toe and Memory are both 2 player games, but the same approach can be used for games with more than two players as well.
For Memory and Tic-Tac-Toe, the current_player_index will always be either 0 or 1. When the time comes to change turns, the code will swap from the current value to the other.
Initially the value of this variable is set to a random number using random.randint(0, 1).
Now that we have an integer variable that holds whose turn it is, any other player specific data or variables can be stored inside of a list and use the indexes of the list to match up with the current_player_index values. In the Tic-Tac-Toe code, it creates the list player_icon_indexes to hold the indexes within the spritesheet of the X and O tiles respectively. current_player_index of 0 matches up to the X sprite tile, and current_player_index of 1 matches up to the O sprite tile.
Whenever the current player clicks a tile to play their turn, the code sets the tile sprite by looking up the appropriate sprite index with player_icon_indexes[current_player_index]
Other lists can be created and used similarly to store and retrieve information specific to each player. The Memory game will make use of a few lists like this.
check_winner() Function
This function will check all of the horizontal, vertical and diagonal lines within the game board to find if any player has 3 in a row and thus has won. If a player does have 3 in a row then check_winner() will return their current_player_index value, i.e. if X wins then 0 will be returned, and if O wins then 1 will be returned. If there is no winner and there are still empty cells then None is returned. If there is no winner and there are no empty cells remaining then -1 is returned to indicate it is a tie game.
Main Loop
The first part of the main loop reads data from the mouse and moves the mouse_tg just as was shown in the mouse demo on the previous page.
if buf[0] & (1 << 0) != 0 is used to check if the left mouse button has been pressed. Nothing else happens until that button does get pressed.
When the left button is pressed, the code puts the current mouse_tg coordinates into a the tuple coords then it loops over all of the cell TileGrids that are in the board_grid and checks if the mouse coordinates are contained within the bounding box of each by calling cell_tg.contains(coords). If the click was not inside of any of the cell TileGrids then nothing else happens.
If the click was inside of a cell, TileGrid the code ensures that the cell is currently empty by checking the sprite index. If it is empty then the current player's sprite is put into the cell TileGrid. Afterward the turn is changed by updating current_player_index.
Next check_for_winner() is called, if there is a winner the output_lbl is updated to say which player won. If it's a tie game, the output_lbl is updated to reflect that. If there is no winner and the game is continuing, the program goes to the next iteration of the main loop.
Page last edited April 03, 2025
Text editor powered by tinymce.