Merge pull request #3019 from FoamyGuy/metro_match3_game
Metro match3 game
251
Metro/Metro_RP2350_Match3/autosave_resume_demo/code.py
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
# 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
|
||||
BIN
Metro/Metro_RP2350_Match3/autosave_resume_demo/mouse_cursor.bmp
Normal file
|
After Width: | Height: | Size: 198 B |
BIN
Metro/Metro_RP2350_Match3/match3_game/btn_exit.bmp
Normal file
|
After Width: | Height: | Size: 922 B |
BIN
Metro/Metro_RP2350_Match3/match3_game/btn_no_set.bmp
Normal file
|
After Width: | Height: | Size: 922 B |
BIN
Metro/Metro_RP2350_Match3/match3_game/btn_play_again.bmp
Normal file
|
After Width: | Height: | Size: 922 B |
458
Metro/Metro_RP2350_Match3/match3_game/code.py
Normal file
|
|
@ -0,0 +1,458 @@
|
|||
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""
|
||||
Match3 game inspired by the Set card game. Two players compete
|
||||
to find sets of cards that share matching or mis-matching traits.
|
||||
"""
|
||||
import array
|
||||
import atexit
|
||||
import io
|
||||
import os
|
||||
import time
|
||||
|
||||
import board
|
||||
import busio
|
||||
import digitalio
|
||||
import supervisor
|
||||
import terminalio
|
||||
import usb
|
||||
from tilepalettemapper import TilePaletteMapper
|
||||
from displayio import TileGrid, Group, Palette, OnDiskBitmap, Bitmap
|
||||
from adafruit_display_text.text_box import TextBox
|
||||
import adafruit_usb_host_descriptors
|
||||
from adafruit_debouncer import Debouncer
|
||||
import adafruit_sdcard
|
||||
import msgpack
|
||||
import storage
|
||||
from match3_game_helpers import (
|
||||
Match3Game,
|
||||
STATE_GAMEOVER,
|
||||
STATE_PLAYING_SETCALLED,
|
||||
GameOverException,
|
||||
)
|
||||
|
||||
original_autoreload_val = supervisor.runtime.autoreload
|
||||
supervisor.runtime.autoreload = False
|
||||
|
||||
AUTOSAVE_FILENAME = "match3_game_autosave.dat"
|
||||
|
||||
main_group = Group()
|
||||
display = supervisor.runtime.display
|
||||
|
||||
# set up scale factor of 2 for larger display resolution
|
||||
scale_factor = 1
|
||||
if display.width > 360:
|
||||
scale_factor = 2
|
||||
main_group.scale = scale_factor
|
||||
|
||||
save_to = None
|
||||
game_state = None
|
||||
try:
|
||||
# check for autosave file on CPSAVES drive
|
||||
if AUTOSAVE_FILENAME in os.listdir("/saves/"):
|
||||
savegame_buffer = io.BytesIO()
|
||||
with open(f"/saves/{AUTOSAVE_FILENAME}", "rb") as f:
|
||||
savegame_buffer.write(f.read())
|
||||
savegame_buffer.seek(0)
|
||||
game_state = msgpack.unpack(savegame_buffer)
|
||||
print(game_state)
|
||||
|
||||
# if we made it to here then /saves/ exist so use it for
|
||||
# save data
|
||||
save_to = f"/saves/{AUTOSAVE_FILENAME}"
|
||||
except OSError as e:
|
||||
# no /saves/ dir likely means no CPSAVES
|
||||
pass
|
||||
|
||||
sd_pins_in_use = False
|
||||
|
||||
if game_state is None:
|
||||
# try to use sdcard for saves
|
||||
# The SD_CS pin is the chip select line.
|
||||
SD_CS = board.SD_CS
|
||||
|
||||
# Connect to the card and mount the filesystem.
|
||||
try:
|
||||
cs = digitalio.DigitalInOut(SD_CS)
|
||||
except ValueError:
|
||||
sd_pins_in_use = True
|
||||
|
||||
print(f"sd pins in use: {sd_pins_in_use}")
|
||||
try:
|
||||
if not sd_pins_in_use:
|
||||
sdcard = adafruit_sdcard.SDCard(
|
||||
busio.SPI(board.SD_SCK, board.SD_MOSI, board.SD_MISO), cs
|
||||
)
|
||||
vfs = storage.VfsFat(sdcard)
|
||||
storage.mount(vfs, "/sd")
|
||||
|
||||
if "set_game_autosave.dat" in os.listdir("/sd/"):
|
||||
savegame_buffer = io.BytesIO()
|
||||
with open("/sd/set_game_autosave.dat", "rb") as f:
|
||||
savegame_buffer.write(f.read())
|
||||
savegame_buffer.seek(0)
|
||||
game_state = msgpack.unpack(savegame_buffer)
|
||||
print(game_state)
|
||||
|
||||
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/set_game_autosave.dat"
|
||||
except OSError:
|
||||
# no SDcard
|
||||
pass
|
||||
|
||||
# background color
|
||||
bg_bmp = Bitmap(
|
||||
display.width // scale_factor // 10, display.height // scale_factor // 10, 1
|
||||
)
|
||||
bg_palette = Palette(1)
|
||||
bg_palette[0] = 0x888888
|
||||
bg_tg = TileGrid(bg_bmp, pixel_shader=bg_palette)
|
||||
bg_group = Group(scale=10)
|
||||
bg_group.append(bg_tg)
|
||||
main_group.append(bg_group)
|
||||
|
||||
# create Game helper object
|
||||
match3_game = Match3Game(
|
||||
game_state=game_state,
|
||||
display_size=(display.width // scale_factor, display.height // scale_factor),
|
||||
save_location=save_to,
|
||||
)
|
||||
main_group.append(match3_game)
|
||||
|
||||
# create a group to hold the game over elements
|
||||
game_over_group = Group()
|
||||
|
||||
# create a TextBox to hold the game over message
|
||||
game_over_label = TextBox(
|
||||
terminalio.FONT,
|
||||
text="",
|
||||
color=0xFFFFFF,
|
||||
background_color=0x111111,
|
||||
width=display.width // scale_factor // 2,
|
||||
height=110,
|
||||
align=TextBox.ALIGN_CENTER,
|
||||
)
|
||||
# move it to the center top of the display
|
||||
game_over_label.anchor_point = (0, 0)
|
||||
game_over_label.anchored_position = (
|
||||
display.width // scale_factor // 2 - (game_over_label.width) // 2,
|
||||
40,
|
||||
)
|
||||
|
||||
# make it hidden, we'll show it when the game is over.
|
||||
game_over_group.hidden = True
|
||||
|
||||
# add the game over lable to the game over group
|
||||
game_over_group.append(game_over_label)
|
||||
|
||||
# load the play again, and exit button bitmaps
|
||||
play_again_btn_bmp = OnDiskBitmap("btn_play_again.bmp")
|
||||
exit_btn_bmp = OnDiskBitmap("btn_exit.bmp")
|
||||
|
||||
# create TileGrid for the play again button
|
||||
play_again_btn = TileGrid(
|
||||
bitmap=play_again_btn_bmp, pixel_shader=play_again_btn_bmp.pixel_shader
|
||||
)
|
||||
|
||||
# transparent pixels in the corners for the rounded corner effect
|
||||
play_again_btn_bmp.pixel_shader.make_transparent(0)
|
||||
|
||||
# centered within the display, offset to the left
|
||||
play_again_btn.x = (
|
||||
display.width // scale_factor // 2 - (play_again_btn_bmp.width) // 2 - 30
|
||||
)
|
||||
|
||||
# inside the bounds of the game over label, so it looks like a dialog visually
|
||||
play_again_btn.y = 100
|
||||
|
||||
# create TileGrid for the exit button
|
||||
exit_btn = TileGrid(bitmap=exit_btn_bmp, pixel_shader=exit_btn_bmp.pixel_shader)
|
||||
|
||||
# transparent pixels in the corners for the rounded corner effect
|
||||
exit_btn_bmp.pixel_shader.make_transparent(0)
|
||||
|
||||
# centered within the display, offset to the right
|
||||
exit_btn.x = display.width // scale_factor // 2 - (exit_btn_bmp.width) // 2 + 30
|
||||
|
||||
# inside the bounds of the game over label, so it looks like a dialog visually
|
||||
exit_btn.y = 100
|
||||
|
||||
# add the play again and exit buttons to the game over group
|
||||
game_over_group.append(play_again_btn)
|
||||
game_over_group.append(exit_btn)
|
||||
main_group.append(game_over_group)
|
||||
|
||||
# wait a second for USB devices to be ready
|
||||
time.sleep(1)
|
||||
|
||||
# load the mouse bitmap
|
||||
mouse_bmp = OnDiskBitmap("mouse_cursor.bmp")
|
||||
|
||||
# make the background pink pixels transparent
|
||||
mouse_bmp.pixel_shader.make_transparent(0)
|
||||
|
||||
# list for mouse tilegrids
|
||||
mouse_tgs = []
|
||||
# list for palette mappers, one for each mouse
|
||||
palette_mappers = []
|
||||
# list for mouse colors
|
||||
colors = [0x2244FF, 0xFFFF00]
|
||||
|
||||
# remap palette will have the 3 colors from mouse bitmap
|
||||
# and the two colors from the mouse colors list
|
||||
remap_palette = Palette(3 + len(colors))
|
||||
# index 0 is transparent
|
||||
remap_palette.make_transparent(0)
|
||||
|
||||
# copy the 3 colors from the mouse bitmap palette
|
||||
for i in range(3):
|
||||
remap_palette[i] = mouse_bmp.pixel_shader[i]
|
||||
|
||||
# copy the 2 colors from the mouse colors list
|
||||
for i in range(2):
|
||||
remap_palette[i + 3] = colors[i]
|
||||
|
||||
# create tile palette mappers
|
||||
for i in range(2):
|
||||
palette_mapper = TilePaletteMapper(remap_palette, 3, 1, 1)
|
||||
# remap index 2 to each of the colors in mouse colors list
|
||||
palette_mapper[0] = [0, 1, i + 3]
|
||||
palette_mappers.append(palette_mapper)
|
||||
|
||||
# create tilegrid for each mouse
|
||||
mouse_tg = TileGrid(mouse_bmp, pixel_shader=palette_mapper)
|
||||
mouse_tg.x = display.width // scale_factor // 2 - (i * 12)
|
||||
mouse_tg.y = display.height // scale_factor // 2
|
||||
mouse_tgs.append(mouse_tg)
|
||||
|
||||
# USB info lists
|
||||
mouse_interface_indexes = []
|
||||
mouse_endpoint_addresses = []
|
||||
kernel_driver_active_flags = []
|
||||
# USB device object instance list
|
||||
mice = []
|
||||
# buffers list for mouse packet data
|
||||
mouse_bufs = []
|
||||
# debouncers list for debouncing mouse left clicks
|
||||
mouse_debouncers = []
|
||||
|
||||
# scan for connected USB devices
|
||||
for device in usb.core.find(find_all=True):
|
||||
# check if current device is has a boot mouse endpoint
|
||||
mouse_interface_index, mouse_endpoint_address = (
|
||||
adafruit_usb_host_descriptors.find_boot_mouse_endpoint(device)
|
||||
)
|
||||
if mouse_interface_index is not None and mouse_endpoint_address is not None:
|
||||
# if it does have a boot mouse endpoint then add information to the
|
||||
# usb info lists
|
||||
mouse_interface_indexes.append(mouse_interface_index)
|
||||
mouse_endpoint_addresses.append(mouse_endpoint_address)
|
||||
|
||||
# add the mouse device instance to list
|
||||
mice.append(device)
|
||||
print(
|
||||
f"mouse interface: {mouse_interface_index} "
|
||||
+ f"endpoint_address: {hex(mouse_endpoint_address)}"
|
||||
)
|
||||
|
||||
# detach kernel driver if needed
|
||||
kernel_driver_active_flags.append(device.is_kernel_driver_active(0))
|
||||
if device.is_kernel_driver_active(0):
|
||||
device.detach_kernel_driver(0)
|
||||
|
||||
# set the mouse configuration so it can be used
|
||||
device.set_configuration()
|
||||
|
||||
|
||||
def is_mouse1_left_clicked():
|
||||
"""
|
||||
Check if mouse 1 left click is pressed
|
||||
:return: True if mouse 1 left click is pressed
|
||||
"""
|
||||
return is_left_mouse_clicked(mouse_bufs[0])
|
||||
|
||||
|
||||
def is_mouse2_left_clicked():
|
||||
"""
|
||||
Check if mouse 2 left click is pressed
|
||||
:return: True if mouse 2 left click is pressed
|
||||
"""
|
||||
return is_left_mouse_clicked(mouse_bufs[1])
|
||||
|
||||
|
||||
def is_left_mouse_clicked(buf):
|
||||
"""
|
||||
Check if a mouse is pressed given its packet buffer
|
||||
filled with read data
|
||||
:param buf: the buffer containing the packet data
|
||||
:return: True if mouse left click is pressed
|
||||
"""
|
||||
val = buf[0] & (1 << 0) != 0
|
||||
return val
|
||||
|
||||
|
||||
def is_right_mouse_clicked(buf):
|
||||
"""
|
||||
check if a mouse right click is pressed given its packet buffer
|
||||
:param buf: the buffer containing the packet data
|
||||
:return: True if mouse right click is pressed
|
||||
"""
|
||||
val = buf[0] & (1 << 1) != 0
|
||||
return val
|
||||
|
||||
|
||||
# print(f"addresses: {mouse_endpoint_addresses}")
|
||||
# print(f"indexes: {mouse_interface_indexes}")
|
||||
|
||||
for mouse_tg in mouse_tgs:
|
||||
# add the mouse to the main group
|
||||
main_group.append(mouse_tg)
|
||||
|
||||
# Buffer to hold data read from the mouse
|
||||
# Boot mice have 4 byte reports
|
||||
mouse_bufs.append(array.array("b", [0] * 8))
|
||||
|
||||
# create debouncer objects for left click functions
|
||||
mouse_debouncers.append(Debouncer(is_mouse1_left_clicked))
|
||||
mouse_debouncers.append(Debouncer(is_mouse2_left_clicked))
|
||||
|
||||
# set main_group as root_group, so it is visible on the display
|
||||
display.root_group = main_group
|
||||
|
||||
# variable to hold winning player
|
||||
winner = None
|
||||
|
||||
|
||||
def get_mouse_deltas(buffer, read_count):
|
||||
"""
|
||||
Given a mouse packet buffer and a read count of number of bytes read,
|
||||
return the delta x and y values of the mouse.
|
||||
:param buffer: the buffer containing the packet data
|
||||
:param read_count: the number of bytes read from the mouse
|
||||
:return: tuple containing x and y delta values
|
||||
"""
|
||||
if read_count == 4:
|
||||
delta_x = buffer[1]
|
||||
delta_y = buffer[2]
|
||||
elif read_count == 8:
|
||||
delta_x = buffer[2]
|
||||
delta_y = buffer[4]
|
||||
else:
|
||||
raise ValueError(f"Unsupported mouse packet size: {read_count}, must be 4 or 8")
|
||||
return delta_x, delta_y
|
||||
|
||||
|
||||
def atexit_callback():
|
||||
"""
|
||||
re-attach USB devices to kernel if needed, and set
|
||||
autoreload back to the original state.
|
||||
:return:
|
||||
"""
|
||||
for _i, _mouse in enumerate(mice):
|
||||
if kernel_driver_active_flags[_i]:
|
||||
if not _mouse.is_kernel_driver_active(0):
|
||||
_mouse.attach_kernel_driver(0)
|
||||
supervisor.runtime.autoreload = original_autoreload_val
|
||||
|
||||
|
||||
atexit.register(atexit_callback)
|
||||
|
||||
# main loop
|
||||
while True:
|
||||
|
||||
# if set has been called
|
||||
if match3_game.cur_state == STATE_PLAYING_SETCALLED:
|
||||
# update the progress bar ticking down
|
||||
match3_game.update_active_turn_progress()
|
||||
|
||||
# loop over the mice objects
|
||||
for i, mouse in enumerate(mice):
|
||||
mouse_tg = mouse_tgs[i]
|
||||
# attempt mouse read
|
||||
try:
|
||||
# read data from the mouse, small timeout so we move on
|
||||
# quickly if there is no data
|
||||
data_len = mouse.read(
|
||||
mouse_endpoint_addresses[i], mouse_bufs[i], timeout=10
|
||||
)
|
||||
mouse_deltas = get_mouse_deltas(mouse_bufs[i], data_len)
|
||||
# if we got data, then update the mouse cursor on the display
|
||||
# using min and max to keep it within the bounds of the display
|
||||
mouse_tg.x = max(
|
||||
0,
|
||||
min(
|
||||
display.width // scale_factor - 1, mouse_tg.x + mouse_deltas[0] // 2
|
||||
),
|
||||
)
|
||||
mouse_tg.y = max(
|
||||
0,
|
||||
min(
|
||||
display.height // scale_factor - 1,
|
||||
mouse_tg.y + mouse_deltas[1] // 2,
|
||||
),
|
||||
)
|
||||
|
||||
# timeout error is raised if no data was read within the allotted timeout
|
||||
except usb.core.USBTimeoutError:
|
||||
pass
|
||||
|
||||
# update the mouse debouncers
|
||||
mouse_debouncers[i].update()
|
||||
|
||||
try:
|
||||
# if the current mouse is right-clicking
|
||||
if is_right_mouse_clicked(mouse_bufs[i]):
|
||||
# let the game object handle the right-click
|
||||
match3_game.handle_right_click(i)
|
||||
|
||||
# if the current mouse left-clicked
|
||||
if mouse_debouncers[i].rose:
|
||||
# get the current mouse coordinates
|
||||
coords = (mouse_tg.x, mouse_tg.y, 0)
|
||||
|
||||
# if the current state is GAMEOVER
|
||||
if match3_game.cur_state != STATE_GAMEOVER:
|
||||
# let the game object handle the click event
|
||||
match3_game.handle_left_click(i, coords)
|
||||
else:
|
||||
# if the mouse point is within the play again
|
||||
# button bounding box
|
||||
if play_again_btn.contains(coords):
|
||||
# set next code file to this one
|
||||
supervisor.set_next_code_file(__file__)
|
||||
# reload
|
||||
supervisor.reload()
|
||||
|
||||
# if the mouse point is within the exit
|
||||
# button bounding box
|
||||
if exit_btn.contains(coords):
|
||||
supervisor.reload()
|
||||
|
||||
# if the game is over
|
||||
except GameOverException:
|
||||
# check for a winner
|
||||
winner = None
|
||||
if match3_game.scores[0] > match3_game.scores[1]:
|
||||
winner = 0
|
||||
elif match3_game.scores[0] < match3_game.scores[1]:
|
||||
winner = 1
|
||||
|
||||
# if there was a winner
|
||||
if winner is not None:
|
||||
# show a message with the winning player
|
||||
message = f"\nGame Over\nPlayer{winner + 1} Wins!"
|
||||
game_over_label.color = colors[winner]
|
||||
game_over_label.text = message
|
||||
|
||||
else: # there wasn't a winner
|
||||
# show a tie game message
|
||||
message = "\nGame Over\nTie Game Everyone Wins!"
|
||||
|
||||
# make the gameover group visible
|
||||
game_over_group.hidden = False
|
||||
|
||||
# delete the autosave file.
|
||||
os.remove(save_to)
|
||||
|
After Width: | Height: | Size: 11 KiB |
779
Metro/Metro_RP2350_Match3/match3_game/match3_game_helpers.py
Normal file
|
|
@ -0,0 +1,779 @@
|
|||
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
from io import BytesIO
|
||||
|
||||
import terminalio
|
||||
|
||||
import adafruit_imageload
|
||||
from displayio import Group, TileGrid, OnDiskBitmap, Palette, Bitmap
|
||||
import bitmaptools
|
||||
import msgpack
|
||||
from tilepalettemapper import TilePaletteMapper
|
||||
import ulab.numpy as np
|
||||
|
||||
from adafruit_display_text.bitmap_label import Label
|
||||
from adafruit_displayio_layout.layouts.grid_layout import GridLayout
|
||||
from adafruit_button import Button
|
||||
from adafruit_progressbar.horizontalprogressbar import (
|
||||
HorizontalProgressBar,
|
||||
HorizontalFillDirection,
|
||||
)
|
||||
|
||||
# pylint: disable=too-many-locals, too-many-nested-blocks, too-many-branches, too-many-statements
|
||||
colors = [0x2244FF, 0xFFFF00]
|
||||
STATE_TITLE = 0
|
||||
STATE_PLAYING_OPEN = 1
|
||||
STATE_PLAYING_SETCALLED = 2
|
||||
STATE_GAMEOVER = 3
|
||||
|
||||
ACTIVE_TURN_TIME_LIMIT = 10.0
|
||||
|
||||
|
||||
def random_selection(lst, count):
|
||||
"""
|
||||
Select items randomly from a list of items.
|
||||
|
||||
returns a list of length count containing the selected items.
|
||||
"""
|
||||
if len(lst) < count:
|
||||
raise ValueError("Count must be less than or equal to length of list")
|
||||
selection = []
|
||||
while len(selection) < count:
|
||||
selection.append(lst.pop(random.randrange(len(lst))))
|
||||
return selection
|
||||
|
||||
|
||||
def validate_set(card_1, card_2, card_3):
|
||||
"""
|
||||
Check if a set of 3 cards is valid
|
||||
:param card_1: the first card
|
||||
:param card_2: the second card
|
||||
:param card_3: the third card
|
||||
:return: True if they are a valid set, False otherwise
|
||||
"""
|
||||
matrix_sums = card_1.tuple + card_2.tuple + card_3.tuple
|
||||
for val in matrix_sums:
|
||||
if val % 3 != 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class Match3Card(Group):
|
||||
"""
|
||||
Class representing a Match3 Card. Keeps track of shape, count, color, and fill.
|
||||
|
||||
tuple value mappings:
|
||||
|
||||
color, shape, fill, count
|
||||
0 , 1 , 2 , 1
|
||||
|
||||
colors
|
||||
purple: 0
|
||||
red: 1
|
||||
green: 2
|
||||
|
||||
shapes
|
||||
rectangle: 0
|
||||
triangle: 1
|
||||
circle: 2
|
||||
|
||||
fill
|
||||
outline: 0
|
||||
filled: 1
|
||||
striped: 2
|
||||
|
||||
count
|
||||
one: 0
|
||||
two: 1
|
||||
three: 2
|
||||
"""
|
||||
|
||||
TUPLE_VALUE_TO_TILE_INDEX_LUT = {
|
||||
# rectangle filled
|
||||
(0, 1, 0): 0,
|
||||
(0, 1, 1): 1,
|
||||
(0, 1, 2): 2,
|
||||
# triangle filled
|
||||
(1, 1, 0): 3,
|
||||
(1, 1, 1): 4,
|
||||
(1, 1, 2): 5,
|
||||
# circle filled
|
||||
(2, 1, 0): 6,
|
||||
(2, 1, 1): 13,
|
||||
(2, 1, 2): 20,
|
||||
# rectangle outline
|
||||
(0, 0, 0): 7,
|
||||
(0, 0, 1): 8,
|
||||
(0, 0, 2): 9,
|
||||
# triangle outline
|
||||
(1, 0, 0): 10,
|
||||
(1, 0, 1): 11,
|
||||
(1, 0, 2): 12,
|
||||
# circle outline
|
||||
(2, 0, 0): 21,
|
||||
(2, 0, 1): 22,
|
||||
(2, 0, 2): 23,
|
||||
# rectangle striped
|
||||
(0, 2, 0): 14,
|
||||
(0, 2, 1): 15,
|
||||
(0, 2, 2): 16,
|
||||
# triangle striped
|
||||
(1, 2, 0): 17,
|
||||
(1, 2, 1): 18,
|
||||
(1, 2, 2): 19,
|
||||
# circle striped
|
||||
(2, 2, 0): 24,
|
||||
(2, 2, 1): 25,
|
||||
(2, 2, 2): 26,
|
||||
}
|
||||
|
||||
def __init__(self, card_tuple, **kwargs):
|
||||
# tile palette mapper to color the card
|
||||
self._mapper = TilePaletteMapper(kwargs["pixel_shader"], 5, 1, 1)
|
||||
kwargs["pixel_shader"] = self._mapper
|
||||
# tile grid to for the visible sprite
|
||||
self._tilegrid = TileGrid(**kwargs)
|
||||
self._tilegrid.x = 4
|
||||
self._tilegrid.y = 4
|
||||
# initialize super class Group
|
||||
super().__init__()
|
||||
# add tilegrid to self instance Group
|
||||
self.append(self._tilegrid)
|
||||
# numpy array of the card tuple values
|
||||
self._tuple = np.array(list(card_tuple), dtype=np.uint8)
|
||||
# set the sprite and color based on card attributes
|
||||
self._update_card_attributes()
|
||||
|
||||
def _update_card_attributes(self):
|
||||
"""
|
||||
set the sprite and color based on card attributes
|
||||
:return: None
|
||||
"""
|
||||
# set color
|
||||
color_tuple_val = self._tuple[0]
|
||||
self._mapper[0] = [0, color_tuple_val + 2, 2, 3, 4]
|
||||
|
||||
# set tile
|
||||
self._tilegrid[0] = Match3Card.TUPLE_VALUE_TO_TILE_INDEX_LUT[
|
||||
(self._tuple[1], self._tuple[2], self._tuple[3])
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.tuple
|
||||
|
||||
def __repr__(self):
|
||||
return self.tuple
|
||||
|
||||
@property
|
||||
def tuple(self):
|
||||
"""
|
||||
The tuple containing attributes values for this card.
|
||||
"""
|
||||
return self._tuple
|
||||
|
||||
def contains(self, coordinates):
|
||||
"""
|
||||
Check if the cards bounding box contains the given coordinates.
|
||||
:param coordinates: the coordinates to check
|
||||
:return: True if the bounding box contains the given coordinates, False otherwise
|
||||
"""
|
||||
return (
|
||||
self.x <= coordinates[0] <= self.x + self._tilegrid.tile_width
|
||||
and self.y <= coordinates[1] <= self.y + self._tilegrid.tile_height
|
||||
)
|
||||
|
||||
|
||||
class Match3Game(Group):
|
||||
"""
|
||||
Match3 Game helper class
|
||||
|
||||
Holds visual elements, manages state machine.
|
||||
"""
|
||||
|
||||
def __init__(self, game_state=None, display_size=None, save_location=None):
|
||||
# initialize super Group instance
|
||||
super().__init__()
|
||||
self.game_state = game_state
|
||||
self.display_size = display_size
|
||||
|
||||
# list of Match3Card instances representing the current deck
|
||||
self.play_deck = []
|
||||
|
||||
# load the spritesheet
|
||||
self.card_spritesheet, self.card_palette = adafruit_imageload.load(
|
||||
"match3_cards_spritesheet.bmp"
|
||||
)
|
||||
|
||||
# start in the TITLE state
|
||||
self.cur_state = STATE_TITLE
|
||||
|
||||
# create a grid layout to help place cards neatly
|
||||
# into a grid on the display
|
||||
grid_size = (6, 3)
|
||||
self.card_grid = GridLayout(
|
||||
x=10, y=10, width=260, height=200, grid_size=grid_size
|
||||
)
|
||||
|
||||
# no set button in the bottom right
|
||||
self.no_set_btn_bmp = OnDiskBitmap("btn_no_set.bmp")
|
||||
self.no_set_btn_bmp.pixel_shader.make_transparent(0)
|
||||
self.no_set_btn = TileGrid(
|
||||
bitmap=self.no_set_btn_bmp, pixel_shader=self.no_set_btn_bmp.pixel_shader
|
||||
)
|
||||
self.no_set_btn.x = display_size[0] - self.no_set_btn.tile_width
|
||||
self.no_set_btn.y = display_size[1] - self.no_set_btn.tile_height
|
||||
self.append(self.no_set_btn)
|
||||
|
||||
# list to hold score labels, one for each player
|
||||
self.score_lbls = []
|
||||
|
||||
# player scores start at 0
|
||||
self.scores = [0, 0]
|
||||
|
||||
self.save_location = save_location
|
||||
|
||||
# initialize and position the score labels
|
||||
for i in range(2):
|
||||
score_lbl = Label(terminalio.FONT, text=f"P{i + 1}: 0", color=colors[i])
|
||||
self.score_lbls.append(score_lbl)
|
||||
score_lbl.anchor_point = (1.0, 0.0)
|
||||
score_lbl.anchored_position = (display_size[0] - 2, 2 + i * 12)
|
||||
self.append(score_lbl)
|
||||
|
||||
# deck count label in the bottom left
|
||||
self.deck_count_lbl = Label(
|
||||
terminalio.FONT, text=f"Deck: {len(self.play_deck)}"
|
||||
)
|
||||
self.deck_count_lbl.anchor_point = (0.0, 1.0)
|
||||
self.deck_count_lbl.anchored_position = (2, display_size[1] - 2)
|
||||
self.append(self.deck_count_lbl)
|
||||
|
||||
# will hold active player index
|
||||
self.active_player = None
|
||||
|
||||
# list of player index who have called no set
|
||||
self.no_set_called_player_indexes = []
|
||||
|
||||
# active turn countdown progress bar
|
||||
# below the score labels
|
||||
self.active_turn_countdown = HorizontalProgressBar(
|
||||
(display_size[0] - 64, 30),
|
||||
(60, 6),
|
||||
direction=HorizontalFillDirection.LEFT_TO_RIGHT,
|
||||
min_value=0,
|
||||
max_value=ACTIVE_TURN_TIME_LIMIT * 10,
|
||||
)
|
||||
self.active_turn_countdown.hidden = True
|
||||
self.append(self.active_turn_countdown)
|
||||
|
||||
# will hold the timestamp when active turn began
|
||||
self.active_turn_start_time = None
|
||||
|
||||
# add the card grid to self instance Group
|
||||
self.append(self.card_grid)
|
||||
|
||||
# list of card objects that have been clicked
|
||||
self.clicked_cards = []
|
||||
|
||||
# list of coordinates that have been clicked
|
||||
self.clicked_coordinates = []
|
||||
|
||||
# initialize title screen
|
||||
self.title_screen = Match3TitleScreen(display_size)
|
||||
self.append(self.title_screen)
|
||||
|
||||
# set up the clicked card indicator borders
|
||||
self.clicked_card_indicator_palette = Palette(2)
|
||||
self.clicked_card_indicator_palette[0] = 0x000000
|
||||
self.clicked_card_indicator_palette.make_transparent(0)
|
||||
self.clicked_card_indicator_palette[1] = colors[0]
|
||||
self.clicked_card_indicator_bmp = Bitmap(24 + 8, 32 + 8, 2)
|
||||
self.clicked_card_indicator_bmp.fill(1)
|
||||
bitmaptools.fill_region(
|
||||
self.clicked_card_indicator_bmp,
|
||||
2,
|
||||
2,
|
||||
self.clicked_card_indicator_bmp.width - 2,
|
||||
self.clicked_card_indicator_bmp.height - 2,
|
||||
value=0,
|
||||
)
|
||||
self.clicked_card_indicators = []
|
||||
for _ in range(3):
|
||||
self.clicked_card_indicators.append(
|
||||
TileGrid(
|
||||
bitmap=self.clicked_card_indicator_bmp,
|
||||
pixel_shader=self.clicked_card_indicator_palette,
|
||||
)
|
||||
)
|
||||
|
||||
def update_scores(self):
|
||||
"""
|
||||
Update the score labels to reflect the current player scores
|
||||
:return: None
|
||||
"""
|
||||
for player_index in range(2):
|
||||
prefix = ""
|
||||
if player_index == self.active_player:
|
||||
prefix = ">"
|
||||
if player_index in self.no_set_called_player_indexes:
|
||||
prefix = "*"
|
||||
self.score_lbls[player_index].text = (
|
||||
f"{prefix}P{player_index + 1}: {self.scores[player_index]}"
|
||||
)
|
||||
|
||||
def save_game_state(self):
|
||||
"""
|
||||
Save the game state to a file
|
||||
:return: None
|
||||
"""
|
||||
# if there is a valid save location
|
||||
if self.save_location is not None:
|
||||
# create a dictionary object to store the game state
|
||||
game_state = {"scores": self.scores, "board": {}, "deck": []}
|
||||
# read the current board state into the dictionary object
|
||||
for _y in range(3):
|
||||
for _x in range(6):
|
||||
try:
|
||||
content = self.card_grid.get_content((_x, _y))
|
||||
game_state["board"][f"{_x},{_y}"] = tuple(content.tuple)
|
||||
except KeyError:
|
||||
pass
|
||||
# read the current deck state into the dictionary object
|
||||
for card in self.play_deck:
|
||||
game_state["deck"].append(tuple(card.tuple))
|
||||
|
||||
# msgpack the object and write it to a file
|
||||
b = BytesIO()
|
||||
msgpack.pack(game_state, b)
|
||||
b.seek(0)
|
||||
with open(self.save_location, "wb") as f:
|
||||
f.write(b.read())
|
||||
|
||||
def load_from_game_state(self, game_state):
|
||||
"""
|
||||
Load game state from a dictionary.
|
||||
:param game_state: The dictionary of game state to load
|
||||
:return: None
|
||||
"""
|
||||
# loop over cards in the deck
|
||||
for card_tuple in game_state["deck"]:
|
||||
# create a card instance and add it to the deck
|
||||
self.play_deck.append(
|
||||
Match3Card(
|
||||
card_tuple,
|
||||
bitmap=self.card_spritesheet,
|
||||
pixel_shader=self.card_palette,
|
||||
tile_width=24,
|
||||
tile_height=32,
|
||||
)
|
||||
)
|
||||
|
||||
# loop over grid cells
|
||||
for y in range(3):
|
||||
for x in range(6):
|
||||
# if the current cell is in the board state of the save game
|
||||
if f"{x},{y}" in game_state["board"]:
|
||||
# create a card instance and put it in the grid here
|
||||
card_tuple = game_state["board"][f"{x},{y}"]
|
||||
self.card_grid.add_content(
|
||||
Match3Card(
|
||||
card_tuple,
|
||||
bitmap=self.card_spritesheet,
|
||||
pixel_shader=self.card_palette,
|
||||
tile_width=24,
|
||||
tile_height=32,
|
||||
),
|
||||
(x, y),
|
||||
(1, 1),
|
||||
)
|
||||
# set the scores from the game state
|
||||
self.scores = game_state["scores"]
|
||||
# update the visible score labels
|
||||
self.update_scores()
|
||||
# update the deck count label
|
||||
self.deck_count_lbl.text = f"Deck: {len(self.play_deck)}"
|
||||
|
||||
def init_new_game(self):
|
||||
"""
|
||||
Initialize a new game state.
|
||||
:return: None
|
||||
"""
|
||||
self.play_deck = []
|
||||
# loop over the 3 possibilities in each of the 4 attributes
|
||||
for _color in range(0, 3):
|
||||
for _shape in range(0, 3):
|
||||
for _fill in range(0, 3):
|
||||
for _count in range(0, 3):
|
||||
# create a card instance with the current attributes
|
||||
self.play_deck.append(
|
||||
Match3Card(
|
||||
(_color, _shape, _fill, _count),
|
||||
bitmap=self.card_spritesheet,
|
||||
pixel_shader=self.card_palette,
|
||||
tile_width=24,
|
||||
tile_height=32,
|
||||
)
|
||||
)
|
||||
|
||||
# draw the starting cards at random
|
||||
starting_pool = random_selection(self.play_deck, 12)
|
||||
|
||||
# put the starting cards into the grid layout
|
||||
for y in range(3):
|
||||
for x in range(4):
|
||||
self.card_grid.add_content(starting_pool[y * 4 + x], (x, y), (1, 1))
|
||||
|
||||
# update the deck count label
|
||||
self.deck_count_lbl.text = f"Deck: {len(self.play_deck)}"
|
||||
|
||||
def handle_right_click(self, player_index):
|
||||
"""
|
||||
Handle right click event
|
||||
:param player_index: the index of the player who clicked
|
||||
:return: None
|
||||
"""
|
||||
# if the current state is open play
|
||||
if self.cur_state == STATE_PLAYING_OPEN:
|
||||
# if there is no active player
|
||||
if self.active_player is None:
|
||||
# if the player who right clicked is in the no set called list
|
||||
if player_index in self.no_set_called_player_indexes:
|
||||
# remove them from the no set called list
|
||||
self.no_set_called_player_indexes.remove(player_index)
|
||||
# set the active player to the player that clicked
|
||||
self.active_player = player_index
|
||||
# set the clicked card indicators to the active player's color
|
||||
self.clicked_card_indicator_palette[1] = colors[player_index]
|
||||
# set the current state to the set called state
|
||||
self.cur_state = STATE_PLAYING_SETCALLED
|
||||
# store timestamp of when the active turn began
|
||||
self.active_turn_start_time = time.monotonic()
|
||||
# make the countdown progress bar visible
|
||||
self.active_turn_countdown.hidden = False
|
||||
# set the value to the maximum of the progress bar
|
||||
self.active_turn_countdown.value = 60
|
||||
# update the score labels to show the active player indicator
|
||||
self.update_scores()
|
||||
|
||||
def handle_left_click(self, player_index, coords):
|
||||
"""
|
||||
Handle left click events
|
||||
:param player_index: the index of the player who clicked
|
||||
:param coords: the coordinates where the mouse clicked
|
||||
:return: None
|
||||
"""
|
||||
# if the current state is open playing
|
||||
if self.cur_state == STATE_PLAYING_OPEN:
|
||||
# if the click is on the no set button
|
||||
if self.no_set_btn.contains(coords):
|
||||
# if the player that clicked is not in the net set called list
|
||||
if player_index not in self.no_set_called_player_indexes:
|
||||
# add them to the no set called list
|
||||
self.no_set_called_player_indexes.append(player_index)
|
||||
|
||||
# if both players have called no set
|
||||
if len(self.no_set_called_player_indexes) == 2:
|
||||
# if there are no cards left in the deck
|
||||
if len(self.play_deck) == 0:
|
||||
# set the state to game over
|
||||
self.cur_state = STATE_GAMEOVER
|
||||
raise GameOverException()
|
||||
|
||||
# empty the no set called list
|
||||
self.no_set_called_player_indexes = []
|
||||
|
||||
# find the empty cells in the card grid
|
||||
empty_cells = self.find_empty_cells()
|
||||
# if there are more than 3 empty cells
|
||||
if len(empty_cells) >= 3:
|
||||
# draw 3 new cards
|
||||
_new_cards = random_selection(self.play_deck, 3)
|
||||
# place them in 3 of the empty cells
|
||||
for i, _new_card in enumerate(_new_cards):
|
||||
self.card_grid.add_content(
|
||||
_new_card, empty_cells[i], (1, 1)
|
||||
)
|
||||
|
||||
else: # there are no 3 empty cells
|
||||
# redraw the original grid with 12 new cards
|
||||
|
||||
# remove existing cards from the grid and
|
||||
# return them to the deck.
|
||||
for _y in range(3):
|
||||
for _x in range(6):
|
||||
try:
|
||||
_remove_card = self.card_grid.pop_content(
|
||||
(_x, _y)
|
||||
)
|
||||
print(f"remove_card: {_remove_card}")
|
||||
self.play_deck.append(_remove_card)
|
||||
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
# draw 12 new cards from the deck
|
||||
starting_pool = random_selection(self.play_deck, 12)
|
||||
# place them into the grid
|
||||
for y in range(3):
|
||||
for x in range(4):
|
||||
self.card_grid.add_content(
|
||||
starting_pool[y * 4 + x], (x + 1, y), (1, 1)
|
||||
)
|
||||
|
||||
# update the deck count label
|
||||
self.deck_count_lbl.text = f"Deck: {len(self.play_deck)}"
|
||||
# save the game state
|
||||
self.save_game_state()
|
||||
|
||||
# update the score labels to show the no set called indicator(s)
|
||||
self.update_scores()
|
||||
|
||||
# if the current state is set called
|
||||
elif self.cur_state == STATE_PLAYING_SETCALLED:
|
||||
# if the player that clicked is the active player
|
||||
if player_index == self.active_player:
|
||||
# get the coordinates that were clicked adjusting for the card_grid position
|
||||
adjusted_coords = (
|
||||
coords[0] - self.card_grid.x,
|
||||
coords[1] - self.card_grid.y,
|
||||
0,
|
||||
)
|
||||
# check which cell contains the clicked coordinates
|
||||
clicked_grid_cell_coordinates = self.card_grid.which_cell_contains(
|
||||
coords
|
||||
)
|
||||
# print(clicked_grid_cell_coordinates)
|
||||
|
||||
# if a cell in the grid was clicked
|
||||
if clicked_grid_cell_coordinates is not None:
|
||||
# try to get the content of the clicked cell, a Card instance potentially
|
||||
try:
|
||||
clicked_cell_content = self.card_grid.get_content(
|
||||
clicked_grid_cell_coordinates
|
||||
)
|
||||
except KeyError:
|
||||
# if no content is in the cell just return
|
||||
return
|
||||
|
||||
# check if the Card instance was clicked, and if the card
|
||||
# isn't already in the list of clicked cards
|
||||
if (
|
||||
clicked_cell_content.contains(adjusted_coords)
|
||||
and clicked_cell_content not in self.clicked_cards
|
||||
):
|
||||
|
||||
clicked_card = clicked_cell_content
|
||||
# show the clicked card indicator in this cell
|
||||
clicked_cell_content.insert(
|
||||
0, self.clicked_card_indicators[len(self.clicked_cards)]
|
||||
)
|
||||
# add the card instance to the clicked cards list
|
||||
self.clicked_cards.append(clicked_card)
|
||||
|
||||
# add the coordinates to the clicked coordinates list
|
||||
self.clicked_coordinates.append(clicked_grid_cell_coordinates)
|
||||
|
||||
# if 3 cards have been clicked
|
||||
if len(self.clicked_cards) == 3:
|
||||
# check if the 3 cards make a valid set
|
||||
valid_set = validate_set(self.clicked_cards[0],
|
||||
self.clicked_cards[1],
|
||||
self.clicked_cards[2])
|
||||
|
||||
# if they are a valid set
|
||||
if valid_set:
|
||||
# award a point to the active player
|
||||
self.scores[self.active_player] += 1
|
||||
|
||||
# loop over the clicked coordinates
|
||||
for coord in self.clicked_coordinates:
|
||||
# remove the old card from this cell
|
||||
_remove_card = self.card_grid.pop_content(coord)
|
||||
# remove border from Match3Card group
|
||||
_remove_card.pop(0)
|
||||
|
||||
# find empty cells in the grid
|
||||
empty_cells = self.find_empty_cells()
|
||||
|
||||
# if there are at least 3 cards in the deck and
|
||||
# at least 6 empty cells in the grid
|
||||
if len(self.play_deck) >= 3 and len(empty_cells) > 6:
|
||||
# deal 3 new cards to empty spots in the grid
|
||||
for i in range(3):
|
||||
_new_card = random_selection(self.play_deck, 1)[
|
||||
0
|
||||
]
|
||||
self.card_grid.add_content(
|
||||
_new_card, empty_cells[i], (1, 1)
|
||||
)
|
||||
# update the deck count label
|
||||
self.deck_count_lbl.text = (
|
||||
f"Deck: {len(self.play_deck)}"
|
||||
)
|
||||
|
||||
# there are not at least 3 cards in the deck
|
||||
# and at least 6 empty cells
|
||||
else:
|
||||
# if there are no empty cells
|
||||
if len(self.find_empty_cells()) == 0:
|
||||
# set the current state to game over
|
||||
self.cur_state = STATE_GAMEOVER
|
||||
raise GameOverException()
|
||||
|
||||
else: # the 3 clicked cards are not a valid set
|
||||
|
||||
# remove the clicked card indicators
|
||||
for _ in range(3):
|
||||
coords = self.clicked_coordinates.pop()
|
||||
self.card_grid.get_content(coords).pop(0)
|
||||
|
||||
# subtract a point from the active player
|
||||
self.scores[self.active_player] -= 1
|
||||
|
||||
# save the game state
|
||||
self.save_game_state()
|
||||
# reset the clicked cards and coordinates lists
|
||||
self.clicked_cards = []
|
||||
self.clicked_coordinates = []
|
||||
|
||||
# set the current state to open play
|
||||
self.cur_state = STATE_PLAYING_OPEN
|
||||
# set active player and active turn vars
|
||||
self.active_player = None
|
||||
self.active_turn_start_time = None
|
||||
self.active_turn_countdown.hidden = True
|
||||
# update the score labels
|
||||
self.update_scores()
|
||||
|
||||
# if the current state is title state
|
||||
elif self.cur_state == STATE_TITLE:
|
||||
# if the resume button is visible and was clicked
|
||||
if (
|
||||
not self.title_screen.resume_btn.hidden
|
||||
and self.title_screen.resume_btn.contains(coords)
|
||||
):
|
||||
|
||||
# load the game from the given game state
|
||||
self.load_from_game_state(self.game_state)
|
||||
# hide the title screen
|
||||
self.title_screen.hidden = True # pylint: disable=attribute-defined-outside-init
|
||||
# set the current state to open play
|
||||
self.cur_state = STATE_PLAYING_OPEN
|
||||
|
||||
# if the new game button was clicked
|
||||
elif self.title_screen.new_game_btn.contains(coords):
|
||||
self.game_state = None
|
||||
# delete the autosave file
|
||||
try:
|
||||
os.remove(self.save_location)
|
||||
print("removed old game save file")
|
||||
except OSError:
|
||||
pass
|
||||
# initialize a new game
|
||||
self.init_new_game()
|
||||
# hide the title screen
|
||||
self.title_screen.hidden = True # pylint: disable=attribute-defined-outside-init
|
||||
# set the current state to open play
|
||||
self.cur_state = STATE_PLAYING_OPEN
|
||||
|
||||
def find_empty_cells(self):
|
||||
"""
|
||||
find the cells within the card grid that are empty
|
||||
:return: list of empty cell coordinate tuples.
|
||||
"""
|
||||
empty_cells = []
|
||||
for x in range(6):
|
||||
for y in range(3):
|
||||
try:
|
||||
_content = self.card_grid.get_content((x, y))
|
||||
except KeyError:
|
||||
empty_cells.append((x, y))
|
||||
return empty_cells
|
||||
|
||||
def update_active_turn_progress(self):
|
||||
"""
|
||||
update the active turn progress bar countdown
|
||||
:return:
|
||||
"""
|
||||
if self.cur_state == STATE_PLAYING_SETCALLED:
|
||||
time_diff = time.monotonic() - self.active_turn_start_time
|
||||
if time_diff > ACTIVE_TURN_TIME_LIMIT:
|
||||
self.scores[self.active_player] -= 1
|
||||
self.active_player = None
|
||||
self.update_scores()
|
||||
self.cur_state = STATE_PLAYING_OPEN
|
||||
self.active_turn_countdown.hidden = True
|
||||
else:
|
||||
self.active_turn_countdown.value = int(
|
||||
(ACTIVE_TURN_TIME_LIMIT - time_diff) * 10
|
||||
)
|
||||
|
||||
|
||||
class GameOverException(Exception):
|
||||
"""
|
||||
Exception that indicates the game is over.
|
||||
Message will contain the reason.
|
||||
"""
|
||||
|
||||
|
||||
class Match3TitleScreen(Group):
|
||||
"""
|
||||
Title screen for the Match3 game.
|
||||
"""
|
||||
|
||||
def __init__(self, display_size):
|
||||
super().__init__()
|
||||
self.display_size = display_size
|
||||
# background bitmap color
|
||||
bg_bmp = Bitmap(display_size[0] // 10, display_size[1] // 10, 1)
|
||||
bg_palette = Palette(1)
|
||||
bg_palette[0] = 0xFFFFFF
|
||||
bg_tg = TileGrid(bg_bmp, pixel_shader=bg_palette)
|
||||
bg_group = Group(scale=10)
|
||||
bg_group.append(bg_tg)
|
||||
self.append(bg_group)
|
||||
|
||||
# load title card bitmap
|
||||
title_card_bmp = OnDiskBitmap("title_card_match3.bmp")
|
||||
title_card_tg = TileGrid(
|
||||
title_card_bmp, pixel_shader=title_card_bmp.pixel_shader
|
||||
)
|
||||
title_card_tg.x = 2
|
||||
if display_size[1] > 200:
|
||||
title_card_tg.y = 20
|
||||
self.append(title_card_tg)
|
||||
|
||||
# make resume and new game buttons
|
||||
BUTTON_X = display_size[0] - 90
|
||||
BUTTON_WIDTH = 70
|
||||
BUTTON_HEIGHT = 20
|
||||
self.resume_btn = Button(
|
||||
x=BUTTON_X,
|
||||
y=40,
|
||||
width=BUTTON_WIDTH,
|
||||
height=BUTTON_HEIGHT,
|
||||
style=Button.ROUNDRECT,
|
||||
fill_color=0x6D2EDC,
|
||||
outline_color=0x888888,
|
||||
label="Resume",
|
||||
label_font=terminalio.FONT,
|
||||
label_color=0xFFFFFF,
|
||||
)
|
||||
self.append(self.resume_btn)
|
||||
self.new_game_btn = Button(
|
||||
x=BUTTON_X,
|
||||
y=40 + BUTTON_HEIGHT + 10,
|
||||
width=BUTTON_WIDTH,
|
||||
height=BUTTON_HEIGHT,
|
||||
style=Button.RECT,
|
||||
fill_color=0x0C9F0C,
|
||||
outline_color=0x111111,
|
||||
label="New Game",
|
||||
label_font=terminalio.FONT,
|
||||
label_color=0xFFFFFF,
|
||||
)
|
||||
self.append(self.new_game_btn)
|
||||
BIN
Metro/Metro_RP2350_Match3/match3_game/mouse_cursor.bmp
Normal file
|
After Width: | Height: | Size: 198 B |
BIN
Metro/Metro_RP2350_Match3/match3_game/title_card_match3.bmp
Normal file
|
After Width: | Height: | Size: 16 KiB |
50
Metro/Metro_RP2350_Match3/tilepalettemapper_demo/code.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import supervisor
|
||||
from displayio import Group, OnDiskBitmap, TileGrid
|
||||
from tilepalettemapper import TilePaletteMapper
|
||||
|
||||
# use the default built-in display,
|
||||
# the HSTX / PicoDVI display for the Metro RP2350
|
||||
display = supervisor.runtime.display
|
||||
|
||||
# a group to hold all other visual elements
|
||||
main_group = Group(scale=4, x=30, y=30)
|
||||
|
||||
# set the main group to show on the display
|
||||
display.root_group = main_group
|
||||
|
||||
# load the sprite sheet bitmap
|
||||
spritesheet_bmp = OnDiskBitmap("match3_cards_spritesheet.bmp")
|
||||
|
||||
# create a TilePaletteMapper
|
||||
tile_palette_mapper = TilePaletteMapper(
|
||||
spritesheet_bmp.pixel_shader, # input pixel_shader
|
||||
5, # input color count
|
||||
3, # grid width
|
||||
1 # grid height
|
||||
)
|
||||
|
||||
# create a TileGrid to show some cards
|
||||
cards_tilegrid = TileGrid(spritesheet_bmp, pixel_shader=tile_palette_mapper,
|
||||
width=3, height=1, tile_width=24, tile_height=32)
|
||||
|
||||
# set each tile in the grid to a different sprite index
|
||||
cards_tilegrid[0, 0] = 10
|
||||
cards_tilegrid[1, 0] = 25
|
||||
cards_tilegrid[2, 0] = 2
|
||||
|
||||
# re-map each tile in the grid to use a different color for index 1
|
||||
# all other indexes remain their default values
|
||||
tile_palette_mapper[0, 0] = [0, 2, 2, 3, 4]
|
||||
tile_palette_mapper[1, 0] = [0, 3, 2, 3, 4]
|
||||
tile_palette_mapper[2, 0] = [0, 4, 2, 3, 4]
|
||||
|
||||
# add the tilegrid to the main group
|
||||
main_group.append(cards_tilegrid)
|
||||
|
||||
# wait forever so it remains visible on the display
|
||||
while True:
|
||||
pass
|
||||
|
After Width: | Height: | Size: 11 KiB |
184
Metro/Metro_RP2350_Match3/two_mice_demo/code.py
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
import array
|
||||
import supervisor
|
||||
import terminalio
|
||||
import usb.core
|
||||
from adafruit_display_text.bitmap_label import Label
|
||||
from displayio import Group, OnDiskBitmap, TileGrid, Palette
|
||||
|
||||
import adafruit_usb_host_descriptors
|
||||
|
||||
# use the default built-in display,
|
||||
# the HSTX / PicoDVI display for the Metro RP2350
|
||||
display = supervisor.runtime.display
|
||||
|
||||
# a group to hold all other visual elements
|
||||
main_group = Group()
|
||||
|
||||
# set the main group to show on the display
|
||||
display.root_group = main_group
|
||||
|
||||
# load the cursor bitmap file
|
||||
mouse_bmp = OnDiskBitmap("mouse_cursor.bmp")
|
||||
|
||||
# lists for labels, mouse tilegrids, and palettes.
|
||||
# each mouse will get 1 of each item. All lists
|
||||
# will end up with length 2.
|
||||
output_lbls = []
|
||||
mouse_tgs = []
|
||||
palettes = []
|
||||
|
||||
# the different colors to use for each mouse cursor
|
||||
# and labels
|
||||
colors = [0xFF00FF, 0x00FF00]
|
||||
|
||||
for i in range(2):
|
||||
# create a palette for this mouse
|
||||
mouse_palette = Palette(3)
|
||||
# index zero is used for transparency
|
||||
mouse_palette.make_transparent(0)
|
||||
# add the palette to the list of palettes
|
||||
palettes.append(mouse_palette)
|
||||
|
||||
# copy the first two colors from mouse palette
|
||||
for palette_color_index in range(2):
|
||||
mouse_palette[palette_color_index] = mouse_bmp.pixel_shader[palette_color_index]
|
||||
|
||||
# replace the last color with different color for each mouse
|
||||
mouse_palette[2] = colors[i]
|
||||
|
||||
# create a TileGrid for this mouse cursor.
|
||||
# use the palette created above
|
||||
mouse_tg = TileGrid(mouse_bmp, pixel_shader=mouse_palette)
|
||||
|
||||
# move the mouse tilegrid to near the center of the display
|
||||
mouse_tg.x = display.width // 2 - (i * 12)
|
||||
mouse_tg.y = display.height // 2
|
||||
|
||||
# add this mouse tilegrid to the list of mouse tilegrids
|
||||
mouse_tgs.append(mouse_tg)
|
||||
|
||||
# add this mouse tilegrid to the main group so it will show
|
||||
# on the display
|
||||
main_group.append(mouse_tg)
|
||||
|
||||
# create a label for this mouse
|
||||
output_lbl = Label(terminalio.FONT, text=f"{mouse_tg.x},{mouse_tg.y}", color=colors[i], scale=1)
|
||||
# anchored to the top left corner of the label
|
||||
output_lbl.anchor_point = (0, 0)
|
||||
|
||||
# move to op left corner of the display, moving
|
||||
# down by a static amount to static the two labels
|
||||
# one below the other
|
||||
output_lbl.anchored_position = (1, 1 + i * 13)
|
||||
|
||||
# add the label to the list of labels
|
||||
output_lbls.append(output_lbl)
|
||||
|
||||
# add the label to the main group so it will show
|
||||
# on the display
|
||||
main_group.append(output_lbl)
|
||||
|
||||
# lists for mouse interface indexes, endpoint addresses, and USB Device instances
|
||||
# each of these will end up with length 2 once we find both mice
|
||||
mouse_interface_indexes = []
|
||||
mouse_endpoint_addresses = []
|
||||
mice = []
|
||||
|
||||
# scan for connected USB devices
|
||||
for device in usb.core.find(find_all=True):
|
||||
# check for boot mouse endpoints on this device
|
||||
mouse_interface_index, mouse_endpoint_address = (
|
||||
adafruit_usb_host_descriptors.find_boot_mouse_endpoint(device)
|
||||
)
|
||||
# if a boot mouse interface index and endpoint address were found
|
||||
if mouse_interface_index is not None and mouse_endpoint_address is not None:
|
||||
# add the interface index to the list of indexes
|
||||
mouse_interface_indexes.append(mouse_interface_index)
|
||||
# add the endpoint address to the list of addresses
|
||||
mouse_endpoint_addresses.append(mouse_endpoint_address)
|
||||
# add the device instance to the list of mice
|
||||
mice.append(device)
|
||||
|
||||
# print details to the console
|
||||
print(f"mouse interface: {mouse_interface_index} ", end="")
|
||||
print(f"endpoint_address: {hex(mouse_endpoint_address)}")
|
||||
|
||||
# detach device from kernel if needed
|
||||
if device.is_kernel_driver_active(0):
|
||||
device.detach_kernel_driver(0)
|
||||
|
||||
# set the mouse configuration so it can be used
|
||||
device.set_configuration()
|
||||
|
||||
# This is ordered by bit position.
|
||||
BUTTONS = ["left", "right", "middle"]
|
||||
|
||||
# list of buffers, will hold one buffer for each mouse
|
||||
mouse_bufs = []
|
||||
for i in range(2):
|
||||
# Buffer to hold data read from the mouse
|
||||
mouse_bufs.append(array.array("b", [0] * 8))
|
||||
|
||||
|
||||
def get_mouse_deltas(buffer, read_count):
|
||||
"""
|
||||
Given a buffer and read_count return the x and y delta values
|
||||
:param buffer: A buffer containing data read from the mouse
|
||||
:param read_count: How many bytes of data were read from the mouse
|
||||
:return: tuple x,y delta values
|
||||
"""
|
||||
if read_count == 4:
|
||||
delta_x = buffer[1]
|
||||
delta_y = buffer[2]
|
||||
elif read_count == 8:
|
||||
delta_x = buffer[2]
|
||||
delta_y = buffer[4]
|
||||
else:
|
||||
raise ValueError(f"Unsupported mouse packet size: {read_count}, must be 4 or 8")
|
||||
return delta_x, delta_y
|
||||
|
||||
# main loop
|
||||
while True:
|
||||
# for each mouse instance
|
||||
for mouse_index, mouse in enumerate(mice):
|
||||
# try to read data from the mouse
|
||||
try:
|
||||
count = mouse.read(
|
||||
mouse_endpoint_addresses[mouse_index], mouse_bufs[mouse_index], timeout=10
|
||||
)
|
||||
|
||||
# if there is no data it will raise USBTimeoutError
|
||||
except usb.core.USBTimeoutError:
|
||||
# Nothing to do if there is no data for this mouse
|
||||
continue
|
||||
|
||||
# there was mouse data, so get the delta x and y values from it
|
||||
mouse_deltas = get_mouse_deltas(mouse_bufs[mouse_index], count)
|
||||
|
||||
# update the x position of this mouse cursor using the delta value
|
||||
# clamped to the display size
|
||||
mouse_tgs[mouse_index].x = max(
|
||||
0, min(display.width - 1, mouse_tgs[mouse_index].x + mouse_deltas[0])
|
||||
)
|
||||
# update the y position of this mouse cursor using the delta value
|
||||
# clamped to the display size
|
||||
mouse_tgs[mouse_index].y = max(
|
||||
0, min(display.height - 1, mouse_tgs[mouse_index].y + mouse_deltas[1])
|
||||
)
|
||||
|
||||
# output string with the new cursor position
|
||||
out_str = f"{mouse_tgs[mouse_index].x},{mouse_tgs[mouse_index].y}"
|
||||
|
||||
# loop over possible button bit indexes
|
||||
for i, button in enumerate(BUTTONS):
|
||||
# check each bit index to determin if the button was pressed
|
||||
if mouse_bufs[mouse_index][0] & (1 << i) != 0:
|
||||
# if it was pressed, add the button to the output string
|
||||
out_str += f" {button}"
|
||||
|
||||
# set the output string into text of the label
|
||||
# to show it on the display
|
||||
output_lbls[mouse_index].text = out_str
|
||||
BIN
Metro/Metro_RP2350_Match3/two_mice_demo/mouse_cursor.bmp
Normal file
|
After Width: | Height: | Size: 198 B |