Maze Runner Matrix

by villelas in Craft > Digital Graphics

2 Views, 0 Favorites, 0 Comments

Maze Runner Matrix

20250426_145157 (1).jpg

In this project, we build a dynamic maze game on an 8×32 LED matrix, fully controlled by capacitive touch sensors!

You move a pulsing yellow avatar through a randomly generated maze, avoiding blue obstacles and aiming for the moving green comet border to win! 🌟

The game regenerates with a new maze every time you win or lose, and the border animates around the maze for a clean "living" effect.

This project looks super polished, runs smooth on a Raspberry Pi Pico W, and is coded entirely in CircuitPython.

Supplies

IMG_3625.jpg

Raspberry Pi Pico W (microcontroller)

8×32 NeoPixel LED Matrix

MPR121 Capacitive Touch Sensor (Adafruit)

Male-to-male jumper wires

Micro USB cable (for Pico W)

Breadboard

External 5V Power Supply (recommended for NeoPixels if >100 LEDs)

Set Up Instructions

IMG_2590.jpeg


  1. Install CircuitPython on your Pico W.
  2. (Follow: https://circuitpython.org/board/raspberry_pi_pico_w/)
  3. Install CircuitPython libraries:
  4. neopixel
  5. adafruit_mpr121
  6. audiomp3
  7. audiopwmio
  8. (You can install these by dragging the .mpy files into the lib/ folder on your CIRCUITPY drive.)
  9. Create a folder sounds/ and add:
  10. start.mp3 (a short "move" sound)
  11. loss.mp3 (a short "fail" sound)
  12. Copy the provided code.py into the root of your Pico W's CIRCUITPY drive.


How the Gameplay Works


  1. A random maze is generated each round.
  2. Walls and obstacles are drawn in blue.
  3. The border constantly animates with a slow comet effect in green.
  4. A pulsing yellow dot represents your player.
  5. You move by touching the capacitive pads.
  6. Touching a blue wall or obstacle = lose
  7. Reaching the moving green border = win 🏁

If you lose or win, a new maze is instantly generated.



code.py:

import board

import busio

import neopixel

import time

import random

import adafruit_mpr121

from audiomp3 import MP3Decoder

from audiopwmio import PWMAudioOut as AudioOut


# ————— Configuration —————

WIDTH, HEIGHT = 8, 32

NUM_LEDS = WIDTH * HEIGHT


pixels = neopixel.NeoPixel(board.GP0, NUM_LEDS, auto_write=False)

i2c = busio.I2C(board.GP3, board.GP2)

touch = adafruit_mpr121.MPR121(i2c)

speaker = AudioOut(board.GP15)


# Boost touch sensitivity

for ch in range(12):

touch[ch].threshold = 6

touch[ch].release_threshold = 3


# ————— Helpers —————

def xy_to_index(x, y):

if y & 1:

return y * WIDTH + (WIDTH - 1 - x)

return y * WIDTH + x


def play_mp3(path):

try:

with open(path, "rb") as f:

mp3 = MP3Decoder(f)

speaker.play(mp3)

while speaker.playing:

pass

except OSError as e:

print(f"❌ Could not play {path}: {e}")


def play_move_sound():

play_mp3("sounds/start.mp3")


def play_loss_sound():

play_mp3("sounds/loss.mp3")


# ————— Movement Map —————

electrode_to_dir = {

0:(1,0),1:(1,0),10:(1,0),11:(1,0),

2:(0,-1),3:(0,-1),

4:(-1,0),5:(-1,0),6:(-1,0),7:(-1,0),

8:(0,1),9:(0,1),

}


# ————— Maze Generation —————

def generate_maze():

visited = {(1,1)}

stack = [(1,1)]

while stack:

x,y = stack[-1]

nbrs = [(dx,dy) for dx,dy in ((1,0),(-1,0),(0,1),(0,-1))

if 1<=x+dx<WIDTH-1 and 1<=y+dy<HEIGHT-1 and (x+dx,y+dy) not in visited]

if nbrs:

dx,dy = random.choice(nbrs)

visited.add((x+dx,y+dy))

stack.append((x+dx,y+dy))

else:

stack.pop()


corridors = visited

walls = {(x,y) for x in range(WIDTH) for y in range(HEIGHT)} - corridors


# sprinkle ~30% obstacles inside corridors

interior = [(x,y) for x,y in corridors if 1<=x<WIDTH-1 and 1<=y<HEIGHT-1]

count = max(1, len(interior)*3//10)

shuffled = interior[:]

for i in range(len(shuffled)-1, 0, -1):

j = random.randint(0, i)

shuffled[i], shuffled[j] = shuffled[j], shuffled[i]

obstacles = set(shuffled[:count])


# Choose a safe starting spot

safe_spots = [pos for pos in corridors if pos not in obstacles]

start = random.choice(safe_spots)


print(f"🔄 Maze ready: corridors={len(corridors)}, obstacles={len(obstacles)}, start={start}")

return corridors, walls, obstacles, start


# ————— Draw Maze Base —————

def draw_maze_base(corridors, walls, obstacles):

for x in range(WIDTH):

for y in range(HEIGHT):

idx = xy_to_index(x,y)

if (x,y) in walls or (x,y) in obstacles:

pixels[idx] = (0,0,50) # blue walls/obstacles

else:

pixels[idx] = (10,10,10) # gray floor


# ————— Border Path Setup —————

def generate_border_path():

path = []

for x in range(WIDTH): path.append((x,0))

for y in range(1,HEIGHT): path.append((WIDTH-1,y))

for x in range(WIDTH-2,-1,-1): path.append((x,HEIGHT-1))

for y in range(HEIGHT-2,0,-1): path.append((0,y))

return path


BORDER_PATH = generate_border_path()

border_pos = 0


# ————— Pulse —————

YELLOW_PULSE = [80,100,120,140,120,100]

pulse_idx = 0


# ————— Main Game Loop —————

while True:

corridors, walls, obstacles, start = generate_maze()

pos = list(start)

prev = [False]*12

playing = True

print(f"😊 Starting at {pos}")


while playing:

# touch debounce

state = prev.copy()

try:

for i in range(12):

state[i] = touch[i].value

except OSError:

pass


# pulse animation

pulse_idx = (pulse_idx + 1) % len(YELLOW_PULSE)

yellow_bright = YELLOW_PULSE[pulse_idx]


# border comet movement

border_pos = (border_pos + 1) % len(BORDER_PATH)


# draw static maze first

draw_maze_base(corridors, walls, obstacles)


# draw moving comet border

for i,(x,y) in enumerate(BORDER_PATH):

idx = xy_to_index(x,y)

if i == border_pos:

pixels[idx] = (0,100,0)

else:

pixels[idx] = (0,30,0)


# draw yellow avatar

pixels[xy_to_index(*pos)] = (yellow_bright, yellow_bright, 0)

pixels.show()


moved = False

for i,pressed in enumerate(state):

if moved: break

if pressed and not prev[i]:

dx,dy = electrode_to_dir[i]

new = [pos[0]+dx, pos[1]+dy]

print(f"➡ Electrode {i} → {(dx,dy)}, target {new}")

x,y = new


if 0<=x<WIDTH and 0<=y<HEIGHT and (x in (0,WIDTH-1) or y in (0,HEIGHT-1)):

print("🏁 WIN at border", new)

play_move_sound()

for _ in range(3):

for col in [(50,0,0),(0,50,0),(0,0,50)]:

pixels[xy_to_index(*new)] = col

pixels.show()

time.sleep(0.1)

playing = False

elif (x,y) in walls or (x,y) in obstacles:

print("💥 LOSS hitting blue", new)

for _ in range(4):

pixels.fill((50,0,0)); pixels.show(); time.sleep(0.2)

draw_maze_base(corridors, walls, obstacles); time.sleep(0.2)

play_loss_sound()

playing = False

elif 0<=x<WIDTH and 0<=y<HEIGHT:

pos[:] = new

print("🏃 Moved to", pos)

play_move_sound()


# ————— INSTANT DRAW AFTER MOVING —————

draw_maze_base(corridors, walls, obstacles)

for j,(bx,by) in enumerate(BORDER_PATH):

idx = xy_to_index(bx,by)

if j == border_pos:

pixels[idx] = (0,100,0)

else:

pixels[idx] = (0,30,0)

pixels[xy_to_index(*pos)] = (yellow_bright, yellow_bright, 0)

pixels.show()

# ————————————————————————————————

moved = True

else:

print("🚧 Out of bounds", new)


prev = state

time.sleep(0.07)


continue


Gameplay Instructions

Touch electrodes on the MPR121 to move:

  1. Right (pads 0,1,10,11)
  2. Up (pads 2,3)
  3. Left (pads 4,5,6,7)
  4. Down (pads 8,9)

Goal:

Reach any green border cell by moving carefully around obstacles!

Avoid:

Touching blue walls or obstacles.

Victory:

You'll see a rainbow win effect, and the maze resets for a fresh challenge!

Downloads