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 TileGrid
s 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 TileGrid
s 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.