This is another adaptation of the game I had already created for the Neopixel LED matrices. But instead of using the analog joystick, I used the Adafruit Matrix Portal S3's native accelerometer and 64x64 Hub 75 RGB Led Matrix.
The algorithm for procedurally generating the maze was adapted from the Python version of Orestis Zekai.
The most interesting thing about these HUB75 panels is the ability to use them as a Framebuffer through the DisplayIO library. This allows you to draw shapes, display images, write text with various fonts, use sprites, etc. In other words, you can use all the conveniences of using DisplayIO, unlike the PixelFramebuffer library.
The code is fully commented and assembling the system is very simple, not requiring any additional components.
import board
import time
import random
import gc
import rgbmatrix
import framebufferio
import displayio
import terminalio
import adafruit_lis3dh
from adafruit_display_shapes.rect import Rect
from adafruit_display_text import label
import adafruit_rtttl
from simpleio import map_range
displayio.release_displays()
# Configuração da matriz HUB75 64x64
matrix = rgbmatrix.RGBMatrix(
width=64, height=64, bit_depth=4,
rgb_pins=[board.MTX_R1, board.MTX_G1, board.MTX_B1,
board.MTX_R2, board.MTX_G2, board.MTX_B2],
addr_pins=[board.MTX_ADDRA, board.MTX_ADDRB, board.MTX_ADDRC,
board.MTX_ADDRD, board.MTX_ADDRE],
clock_pin=board.MTX_CLK, latch_pin=board.MTX_LAT, output_enable_pin=board.MTX_OE,
doublebuffer=True
)
display = framebufferio.FramebufferDisplay(matrix, auto_refresh=True)
# Accelerometer
i2c = board.I2C()
lis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c, address=0x19)
lis3dh.range = adafruit_lis3dh.RANGE_2_G
# Optional Buzzer
buzzer = board.A3
# Colors
BLACK = 0x000000
WHITE = 0xFFFFFF
RED = 0xFF0000
GREEN = 0x00FF00
CYAN = 0x00FFFF
YELLOW = 0xFFFF00
# Small Cell_Size, bigger and complex maze.
CELL_SIZE = 2 # 1, complex maze, 4 is a very easy maze
MAZE_CELLS = 64 // CELL_SIZE # Iqual Pixel Size of Screen
# Fisher-Yates Shuffle
def shuffle_list(lst):
for i in range(len(lst)-1, 0, -1):
j = random.randint(0, i)
lst[i], lst[j] = lst[j], lst[i]
return lst
class Maze:
def __init__(self):
self.cell_width = MAZE_CELLS
self.cell_height = MAZE_CELLS
self.maze = []
# Upper Left Size
self.entry_pos = (1, 1)
# Down Right Size, at end of path
self.exit_pos = (MAZE_CELLS - 2, MAZE_CELLS - 2)
self.generate()
def get_entry(self):
return self.entry_pos
def get_exit(self):
return self.exit_pos
def generate(self):
# Init Maze (1 = wall, 0 = open path, 2 = maze exit)
self.maze = [[1 for _ in range(self.cell_width)] for _ in range(self.cell_height)]
# Create maze limits (1 cell thick)
for i in range(self.cell_width):
self.maze[0][i] = 1 # Upper Border
self.maze[self.cell_height-1][i] = 1 # Down Border
for i in range(self.cell_height):
self.maze[i][0] = 1 # Left Border
self.maze[i][self.cell_width-1] = 1 # Right Border
# Create entry point
entry_x, entry_y = self.entry_pos
self.maze[entry_y][entry_x] = 0
# Create exit, without connection
exit_x, exit_y = self.exit_pos
self.maze[exit_y][exit_x] = 2
# Recursive Backtracking - Creating Maze
stack = [(entry_x, entry_y)]
self.maze[entry_y][entry_x] = 0
# Possible Directions, in Cells
directions = [(0, 2), (2, 0), (0, -2), (-2, 0)]
while stack:
current_x, current_y = stack[-1]
shuffled_directions = shuffle_list(directions.copy())
found = False
for dx, dy in shuffled_directions:
nx, ny = current_x + dx, current_y + dy
if (1 <= nx < self.cell_width-1 and 1 <= ny < self.cell_height-1 and
self.maze[ny][nx] == 1):
# Remove wall between cells
self.maze[current_y + dy//2][current_x + dx//2] = 0
self.maze[ny][nx] = 0
stack.append((nx, ny))
found = True
break
if not found:
stack.pop()
# Connects exit to path
self.connect_exit_to_maze()
def connect_exit_to_maze(self):
exit_x, exit_y = self.exit_pos
# Try to connect exit using any of near cells
connection_points = [
(exit_x - 1, exit_y), # Left
(exit_x, exit_y - 1), # Up
(exit_x - 1, exit_y - 1) # Diagonal (if other fails)
]
for cx, cy in connection_points:
if (1 <= cx < self.cell_width-1 and 1 <= cy < self.cell_height-1):
self.maze[cy][cx] = 0 # Create Path
break
def is_wall(self, cell_x, cell_y):
"""Verifica se a célula na posição dada é uma parede"""
if 0 <= cell_x < self.cell_width and 0 <= cell_y < self.cell_height:
return self.maze[cell_y][cell_x] == 1
return True # Fora dos limites é considerado parede
class Game:
def __init__(self):
self.level = 1
self.maze = Maze()
self.player_pos = self.maze.get_entry()
self.main_group = displayio.Group()
self.player = None
self.setup_display()
# Histórico para suavizar o movimento
self.move_history = []
self.history_size = 3
def show_next_level_message(self):
# Limpa a tela
while len(self.main_group) > 0:
self.main_group.pop()
text_area = label.Label(terminalio.FONT, text="NEXT LEVEL", color=YELLOW)
text_width = len("NEXT LEVEL") * 6
text_area.x = (64 - text_width) // 2
text_area.y = 32
self.main_group.append(text_area)
display.root_group = self.main_group
display.refresh()
# Show 2s
time.sleep(2.0)
def setup_display(self):
# Clean Main Group
while len(self.main_group) > 0:
self.main_group.pop()
# Creates a bitmap for Maze (64x64 pixels)
bitmap = displayio.Bitmap(64, 64, 4)
palette = displayio.Palette(4)
palette[0] = BLACK # Path
palette[1] = CYAN # Wall
palette[2] = GREEN # Exit
palette[3] = RED # Player (Entry point)
# Fills the bitmap based on the maze of cells
for cell_y in range(self.maze.cell_height):
for cell_x in range(self.maze.cell_width):
cell_value = self.maze.maze[cell_y][cell_x]
color_index = 0 # Default: Empty Path
if cell_value == 1: # Wall
color_index = 1
elif cell_value == 2: # Exit
color_index = 2
# Input is drawn as normal path (black)
# Fills the CELL_SIZExCELL_SIZE block of pixels
pixel_x = cell_x * CELL_SIZE
pixel_y = cell_y * CELL_SIZE
for dy in range(CELL_SIZE):
for dx in range(CELL_SIZE):
if (0 <= pixel_x + dx < 64 and 0 <= pixel_y + dy < 64):
bitmap[pixel_x + dx, pixel_y + dy] = color_index
# Creates a TileGrid with the bitmap
tilegrid = displayio.TileGrid(bitmap, pixel_shader=palette)
self.main_group.append(tilegrid)
# Creates the player with size proportional to CELL_SIZE
player_pixel_x = self.player_pos[0] * CELL_SIZE
player_pixel_y = self.player_pos[1] * CELL_SIZE
self.player = Rect(
x=player_pixel_x,
y=player_pixel_y,
width=CELL_SIZE,
height=CELL_SIZE,
fill=RED
)
self.main_group.append(self.player)
display.root_group = self.main_group
def get_tilt_direction(self):
# Reads accelerometer values and converts them to G
x, y, z = (value / adafruit_lis3dh.STANDARD_GRAVITY for value in lis3dh.acceleration)
# Calculates the magnitude of the tilt on each axis
x_mag = abs(x)
y_mag = abs(y)
# Determine which axis has the greatest inclination
if x_mag > y_mag:
# X Axis (Left/Right)
if x > 0.1:
dx = 1 # Right
elif x < -0.01:
dx = -1 # Left
else:
dx = 0
dy = 0 # Block Y
else:
# Y Axis (UP/Down)
if y > 0.2:
dy = 1 # Down
elif y < -0.2:
dy = -1 # UP
else:
dy = 0
dx = 0 # Block X
return dx, dy
def move_player(self):
dx, dy = self.get_tilt_direction()
if dx != 0 or dy != 0:
new_cell_x = self.player_pos[0] + dx
new_cell_y = self.player_pos[1] + dy
# Valid Move? (Not Wall)
if not self.maze.is_wall(new_cell_x, new_cell_y):
self.player_pos = (new_cell_x, new_cell_y)
# Update player position
player_pixel_x = self.player_pos[0] * CELL_SIZE
player_pixel_y = self.player_pos[1] * CELL_SIZE
self.player.x = player_pixel_x
self.player.y = player_pixel_y
# Check Exit
if self.player_pos == self.maze.get_exit():
self.next_level()
def next_level(self):
# Play Buzzer Sound (if connected)
try:
adafruit_rtttl.play(buzzer, "success:d=8,o=5,b=330:8e6,8g6")
except:
pass
# Show Next level Message
self.show_next_level_message()
# New Level and new maze
self.level += 1
self.maze = Maze()
self.player_pos = self.maze.get_entry()
self.setup_display()
time.sleep(0.5)
def run(self):
last_update = time.monotonic()
update_interval = 0.2 # Update at 200ms
while True:
current_time = time.monotonic()
if current_time - last_update >= update_interval:
self.move_player()
last_update = current_time
time.sleep(0.05)
# Start Game as Class
game = Game()
game.run()
Comments