The Match3 game supports auto-save and resume if an SD Card is inserted into the Metro. Without an SD Card, the game will work as normal, but progress will be lost if the device loses power or resets.
In order to understand how the auto-save and resume mechanics work, first consider this simplified demo script.
Demo Code
The demo code is shown below followed by a screenshot and explanation. If you want to run it on your own device click the "Download Project Bundle" button, unzip the project files and copy them to the CIRCUITPY drive.
# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
# SPDX-License-Identifier: MIT
"""
This example demonstrates basic autosave and resume functionality. There are two buttons
that can be clicked to increment respective counters. The number of clicks is stored
in a game_state dictionary and saved to a data file on the SDCard. When the code first
launches it will read the data file and load the game_state from it.
"""
import array
from io import BytesIO
import os
import board
import busio
import digitalio
import displayio
import msgpack
import storage
import supervisor
import terminalio
import usb
import adafruit_sdcard
from adafruit_display_text.bitmap_label import Label
from adafruit_button import Button
# use the default built-in display
display = supervisor.runtime.display
# button configuration
BUTTON_WIDTH = 100
BUTTON_HEIGHT = 30
BUTTON_STYLE = Button.ROUNDRECT
# game state object will get loaded from SDCard
# or a new one initialized as a dictionary
game_state = None
save_to = None
# boolean variables for possible SDCard states
sd_pins_in_use = False
# The SD_CS pin is the chip select line.
SD_CS = board.SD_CS
# try to Connect to the sdcard card and mount the filesystem.
try:
# initialze CS pin
cs = digitalio.DigitalInOut(SD_CS)
except ValueError:
# likely the SDCard was auto-initialized by the core
sd_pins_in_use = True
try:
# if sd CS pin was not in use
if not sd_pins_in_use:
# try to initialize and mount the SDCard
sdcard = adafruit_sdcard.SDCard(
busio.SPI(board.SD_SCK, board.SD_MOSI, board.SD_MISO), cs
)
vfs = storage.VfsFat(sdcard)
storage.mount(vfs, "/sd")
# check for the autosave data file
if "autosave_demo.dat" in os.listdir("/sd/"):
# if the file is found read data from it into a BytesIO buffer
buffer = BytesIO()
with open("/sd/autosave_demo.dat", "rb") as f:
buffer.write(f.read())
buffer.seek(0)
# unpack the game_state object from the read data in the buffer
game_state = msgpack.unpack(buffer)
print(game_state)
# if placeholder.txt file does not exist
if "placeholder.txt" not in os.listdir("/sd/"):
# if we made it to here then /sd/ exists and has a card
# so use it for save data
save_to = "/sd/autosave_demo.dat"
except OSError as e:
# sdcard init or mounting failed
raise OSError(
"This demo requires an SDCard. Please power off the device " +
"insert an SDCard and then plug it back in."
) from e
# if no saved game_state was loaded
if game_state is None:
# create a new game state dictionary
game_state = {"pink_count": 0, "blue_count": 0}
# Make the display context
main_group = displayio.Group()
display.root_group = main_group
# make buttons
blue_button = Button(
x=30,
y=40,
width=BUTTON_WIDTH,
height=BUTTON_HEIGHT,
style=BUTTON_STYLE,
fill_color=0x0000FF,
outline_color=0xFFFFFF,
label="BLUE",
label_font=terminalio.FONT,
label_color=0xFFFFFF,
)
pink_button = Button(
x=30,
y=80,
width=BUTTON_WIDTH,
height=BUTTON_HEIGHT,
style=BUTTON_STYLE,
fill_color=0xFF00FF,
outline_color=0xFFFFFF,
label="PINK",
label_font=terminalio.FONT,
label_color=0x000000,
)
# add them to a list for easy iteration
all_buttons = [blue_button, pink_button]
# Add buttons to the display context
main_group.append(blue_button)
main_group.append(pink_button)
# make labels for each button
blue_lbl = Label(
terminalio.FONT, text=f"Blue: {game_state['blue_count']}", color=0x3F3FFF
)
blue_lbl.anchor_point = (0, 0)
blue_lbl.anchored_position = (4, 4)
pink_lbl = Label(
terminalio.FONT, text=f"Pink: {game_state['pink_count']}", color=0xFF00FF
)
pink_lbl.anchor_point = (0, 0)
pink_lbl.anchored_position = (4, 4 + 14)
main_group.append(blue_lbl)
main_group.append(pink_lbl)
# load the mouse cursor bitmap
mouse_bmp = displayio.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 = displayio.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
# add the mouse tilegrid to main_group
main_group.append(mouse_tg)
# 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)
def save_game_state():
"""
msgpack the game_state and save it to the autosave data file
:return:
"""
b = BytesIO()
msgpack.pack(game_state, b)
b.seek(0)
with open(save_to, "wb") as savefile:
savefile.write(b.read())
# 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 click is pressed
if buf[0] & (1 << 0) != 0:
# get the current cursor coordinates
coords = (mouse_tg.x, mouse_tg.y, 0)
# loop over the buttons
for button in all_buttons:
# if the current button contains the mouse coords
if button.contains(coords):
# if the button isn't already in the selected state
if not button.selected:
# enter selected state
button.selected = True
# if it is the pink button
if button == pink_button:
# increment pink count
game_state["pink_count"] += 1
# update the label
pink_lbl.text = f"Pink: {game_state['pink_count']}"
# if it is the blue button
elif button == blue_button:
# increment blue count
game_state["blue_count"] += 1
# update the label
blue_lbl.text = f"Blue: {game_state['blue_count']}"
# save the new game state
save_game_state()
# if the click is not on the current button
else:
# set this button as not selected
button.selected = False
# left click is not pressed
else:
# set all buttons as not selected
for button in all_buttons:
button.selected = False
Auto-save & Resume Functionality
Before the code can read and write files on the SD Card, it must be initialized and mounted. In version 10.0, CircuitPython will begin initializing and mounting the SD Card automatically. For any versions prior to that, the user code must handle the initialization and mounting. This demo script supports both by first checking if the SD Card is already mounted and only trying to initialize and mount it if it wasn't.
# The SD_CS pin is the chip select line.
SD_CS = board.SD_CS
# try to Connect to the sdcard card and mount the filesystem.
try:
# initialze CS pin
cs = digitalio.DigitalInOut(SD_CS)
except ValueError:
# likely the SDCard was auto-initialized by the core
sd_pins_in_use = True
try:
# if sd CS pin was not in use
if not sd_pins_in_use:
# try to initialize and mount the SDCard
sdcard = adafruit_sdcard.SDCard(busio.SPI(board.SD_SCK, board.SD_MOSI, board.SD_MISO), cs)
vfs = storage.VfsFat(sdcard)
storage.mount(vfs, "/sd")
Next the code will attempt to read the autosave_demo.dat file and load game_state from it. For storing the game_state more efficiently, the msgpack module is used.
# check for the autosave data file
if "autosave_demo.dat" in os.listdir("/sd/"):
# if the file is found read data from it into a BytesIO buffer
buffer = BytesIO()
with open("/sd/autosave_demo.dat", "rb") as f:
buffer.write(f.read())
buffer.seek(0)
# unpack the game_state object from the read data in the buffer
game_state = msgpack.unpack(buffer)
print(game_state)
If there was no autosave file, but the SD Card was mounted successfully, then the code will set up the save_to variable holding a file path to the location where the autosave file will get written.
If no game state was loaded, then a new one is initialized with 0 values for the button counts.
# if placeholder.txt file does not exist
if "placeholder.txt" not in os.listdir("/sd/"):
# if we made it to here then /sd/ exists and has a card
# so use it for save data
save_to = "/sd/autosave_demo.dat"
except OSError:
# sdcard init or mounting failed
raise OSError(
"This demo requires an SDCard. Please power off the device insert an SDCard and then plug it back in.")
# if no saved game_state was loaded
if game_state is None:
# create a new game state dictionary
game_state = {'pink_count': 0, 'blue_count': 0}
def save_game_state():
"""
msgpack the game_state and save it to the autosave data file
:return:
"""
b = BytesIO()
msgpack.pack(game_state, b)
b.seek(0)
with open(save_to, "wb") as f:
f.write(b.read())
Page last edited April 14, 2025
Text editor powered by tinymce.