CPWii (Retro Wii-style Mariokart Game on LED Matrix)

by flanaghi in Circuits > Microcontrollers

80 Views, 1 Favorites, 0 Comments

CPWii (Retro Wii-style Mariokart Game on LED Matrix)

stock.png

If you were like me growing up, you had a Nintendo Wii and spent countless hours playing video games on it. One of those games being the classic Mario Kart. To relive my glory days and test my coding skills (in Circuit Python), I decided to recreate a retro style Mario Kart on an LED matrix that behaves just like a Wii. I used two Circuit Playground Bluefruits, one to program the game and the other to control it like a Wii remote. I used Bluetooth connection between the two of them to send/receive accelerometer values to control the "car" on the LED. I will go deeper into the code at that respective step, but that is the general overview and I hope you enjoy!

Supplies

IMG_6641.jpeg

Materials:

16x16 LED Matrix (I used this one on Amazon: BTF-LIGHTING WS2812B ECO RGB Alloy Wires 5050SMD Individual Addressable 16X16 256 Pixels LED Matrix Flexible FPCB Full Color Works with K-1000C,SP107E,etc Controllers Image Video Text Display DC5V)


Velcro tape


Alligator clips


2 Circuit Playground Bluefruits from Adafruit


1/8 inch wood (box to hold led) 


Wood glue


2 battery packs and usb wires (to power CPBs)


PLA for 3D printing Wii remote 


Heat shrink tubing for wires (prevents shorting) 

Make Materials

wii_remote_tinker_cad.png

Make all materials needed:

Tinker cad file for 3D printing wii remote

Maker case file then edit in Adobe Illustrator to customize box. Then lazer cut to hold LED.


Downloads

Add to Previous Materials

IMG_6640.jpeg
IMG_6639.jpeg

Velcro tape battery pack to wii remote and make sure CPB fits in top. Velcro tape LED matrix to box and leave the back panel open to be able to work with wiring.

Wire Up LED and CPB (Receiver)

IMG_6642.jpeg
IMG_6645.jpeg

Use DIN, 5V and GND wires from LED and connect to respective parts of CPB (I used alligator clips).

Sender Code

Now is the code. I will walk through what each line does in this code.

There are two files, the sender (remote) and receiver (game).

I will walk through sender here:

1. Import Required Libraries and Modules

board: Provides access to hardware pins.

time: Allows for time-related functions like delays.

digitalio: Provides digital I/O control for GPIO pins.

busio: Provides an interface for I2C communication.

Button: Debounces button inputs to avoid multiple triggers from a single press.

BLERadio: Manages the Bluetooth Low Energy (BLE) radio interface.

ProvideServicesAdvertisement: Advertises BLE services.

UARTService: Provides a UART (Universal Asynchronous Receiver-Transmitter) service over BLE for serial data communication.

RawTextPacket: Encodes and decodes packets of plain text to send over BLE.

adafruit_lis3dh: Provides functions for handling an LIS3DH accelerometer.


2. Set Up the Accelerometer with I2C Communication

Create an I2C interface: i2c connects the microcontroller to the accelerometer using I2C pins (SCL and SDA).

Set up an interrupt pin: int1 is configured to receive interrupts from the accelerometer, allowing the microcontroller to react to motion events.

Initialize the accelerometer: accelerometer is an object representing the LIS3DH accelerometer.

Set accelerometer range: The range is set to ±8G, which controls the sensitivity and range of acceleration the sensor can measure.


3) Set up BLE and Define Device Name

receiver_name: Stores the name of the BLE device we want to connect to.

ble: Initializes the Bluetooth Low Energy (BLE) interface.

uart_connection: Sets a variable to hold the BLE connection to the device, initialized as None.


4) Define Functions to Send Packets Over BLE

send_packet(uart_connection_name, packet):


Sends a packet over the established BLE UART connection and handles possible disconnections.

send_restart(uart_connection_name)

Sends a "RESTART" command over BLE.


5. Main Loop for BLE Connection and Accelerometer Data Transmission

BLE Scanning: If not connected, it scans for nearby BLE devices.

Connection: Connects to the device and stops scanning once connected.


6. Data Transmission Loop

Read Accelerometer Data: x, y, z values from accelerometer.acceleration represent acceleration on each axis.

Format Data: Combines x and y values with a comma and newline to make a readable format.

Send Data: Uses send_packet to transmit the data to the BLE device. If send_packet fails, it clears the uart_connection.

Sleep: Adds a small delay to control data rate.



Downloads

Receiver Code

Now I will walk through receiver code (much more complicated):


Step 1: Import Libraries

import board - Accesses the hardware pins of the board.

import neopixel - Allows control of NeoPixel (RGB) LEDs.

import digitalio - Used for digital I/O control, like enabling the speaker.

from adafruit_ble import BLERadio

from adafruit_ble.advertising.standard import ProvideServicesAdvertisement

from adafruit_ble.services.nordic import UARTService

from adafruit_bluefruit_connect.packet import Packet

from adafruit_bluefruit_connect.raw_text_packet import RawTextPacket - Everything adafruit_ble Imports modules to set up Bluetooth Low Energy (BLE) connections and services.


import time - Allows tracking of elapsed time for game events.

import random - Provides functions to randomize certain game events.

from audiomp3 import MP3Decoder

from audiopwmio import PWMAudioOut as AudioOut # For CPB & Pico (IF USING SOUND)



Step 2: Setup Bluetooth Connection


#CODE

ble = BLERadio()

uart = UARTService()

advertisement = ProvideServicesAdvertisement(uart)

advertisement.complete_name = "ACF-r"

ble.name = advertisement.complete_name

print("Running Receiver Code!")


#DESCRIPTION

Initializes the BLE module, creating a connection service and an advertisement that will be used to advertise the device.

Sets the BLE device name to "ACF-r" and starts the receiver code for BLE interactions.


Step 3: Setup NeoPixel and Game Variables


#CODE

pixel = neopixel.NeoPixel(board.A1, n=256, brightness=0.3, auto_write=False)


#DESCRIPTION

Initializes a 16x16 NeoPixel grid (256 LEDs total) on A1 with 0.3 brightness.


#CODE

car_position = [8, 15] # Starting position on a 16x16 grid


#DESCRIPTION

Sets the car's starting position in the center-bottom of the NeoPixel grid.


Step 4: Define Obstacle and Finish Line Properties


#CODE:

obstacle_speed = 0.5 # Speed for obstacles

last_obstacle_update = time.monotonic() # Track last update time

obstacle_interval = 2 # Time interval in seconds to add new obstacles

last_obstacle_spawn = time.monotonic()


#DESCRIPTION

Controls obstacle behavior, including speed, time tracking, and spawn intervals.


#CODE

obstacles = [{"x": random.randint(3, 12), "y": 0, "width": random.randint(1, 3), "original_x": random.randint(3, 12)}]


#DESCRIPTION

Initializes the obstacle list with a random obstacle position and size.


#CODE

finish_line_y = -1 # Start off-screen

finish_line_trigger_time = 15 # Time in seconds to trigger finish line

last_obstacle_hit_time = time.monotonic() # Last time an obstacle was hit


#DESCRIPTION

Defines the finish line properties and triggers, including its starting position and trigger time.



Step 5: Define Game Reset and Obstacle Reset Functions


#CODE

def reset_obstacle(obstacle):

obstacle["y"] = 0 # Reset to the top

obstacle["x"] = random.randint(3, 12) # Restrict x position within the "road" area

obstacle["original_x"] = obstacle["x"] # Set starting x position for mirroring

obstacle["width"] = random.randint(1, 3) # Random width between 1 and 3 pixels


#DESCRIPTION

Defines how obstacles reset to a random location and size at the top of the grid.


#CODE

def reset_game():

global car_position, obstacles, last_obstacle_hit_time, finish_line_y

car_position = [8, 15] # Reset car position

obstacles = [{"x": random.randint(3, 12), "y": 0, "width": random.randint(1, 3), "original_x": random.randint(3, 12)}]

last_obstacle_hit_time = time.monotonic() # Reset the timer

finish_line_y = -1 # Reset finish line off-screen


#DESCRIPTION

Resets the game state, including car position, obstacles, and timer.



Step 6: Main Loop - Advertising and BLE Connection Handling


#CODE

while True:

ble.start_advertising(advertisement) # Start advertising.

print(f"Advertising as: {advertisement.complete_name}")

was_connected = False


#DESCRIPTION

Advertises the BLE device and sets a connection flag.



Step 7: Handle Bluetooth Messages and Game Actions


#CODE

while not was_connected or ble.connected:

if ble.connected: # If BLE is connected...

was_connected = True


#DESCRIPTION

Continues the game loop while BLE is connected, allowing data reception.


#CODE

if uart.in_waiting:

try:

packet = Packet.from_stream(uart) # Create the packet object.

print(f"packet: {packet}")

except ValueError:

continue


#DESCRIPTION

Checks for incoming BLE data packets and prints them.


#CODE

if isinstance(packet, RawTextPacket): # If the packet is a RawTextPacket

message = packet.text.decode().strip()

print(f"Message Received: {message}")

#DESCRIPTION

Confirms the packet is raw text, then decodes and prints it.


#CODE

values = message.split(',')

x_accel = float(values[0])

print(f"Accelerometer values: X: {x_accel}")


#DESCRIPTION

Reads accelerometer values sent from the BLE-connected device.


#CODE

if x_accel < -2.5 and car_position[0] < 12: # Tilted left

car_position[0] += 1 # Move right

elif x_accel > 2.5 and car_position[0] > 3: # Tilted right

car_position[0] -= 1 # Move left


#DESCRIPTION

Moves the car left or right based on accelerometer input.


Step 8: Obstacle and Finish Line Update Logic


#CODE

current_time = time.monotonic()

if current_time - last_obstacle_update >= obstacle_speed:


#DESCRIPTION

Checks if it’s time to move the obstacles down by comparing elapsed time.


#CODE

if finish_line_y >= 0 and finish_line_y < 15:

finish_line_y += 1 # Move finish line down by 1 row

#DESCRIPTION

Moves the finish line down if it’s been triggered.


#CODE

for obstacle in obstacles:

if obstacle["y"] % 2 == 0:

obstacle["x"] = obstacle["original_x"]

else:

obstacle["x"] = 15 - obstacle["original_x"]

obstacle["y"] += 1 # Move down one row


#DESCRIPTION

Adjusts each obstacle’s position, mirroring them to simulate lane shifts.


#CODE

for i in range(obstacle["width"]):

if obstacle["x"] + i == car_position[0] and obstacle["y"] == car_position[1]:

print("Game Over!")

reset_game() # Automatically restart the game

break


# If the obstacle reaches the bottom, reset it to the top with a new random x position

if obstacle["y"] > 15:

reset_obstacle(obstacle)


last_obstacle_update = current_time



#DESCRIPTION

Checks for collisions between obstacles and the car.


Step 9: Add New Obstacles


#CODE

if current_time - last_obstacle_spawn >= obstacle_interval:

obstacles.append({

"x": random.randint(3, 12),

"y": 0,

"width": random.randint(1, 3),

"original_x": random.randint(3, 12)

})

last_obstacle_spawn = current_time


#DESCRIPTION

Spawns new obstacles periodically.


#CODE


# Check for finish line condition

if current_time - last_obstacle_hit_time >= finish_line_trigger_time and finish_line_y == -1:

finish_line_y = 0 # Start falling only once

print("Finish line started falling") # Debug statement


#DESCRIPTION

Makes the finish line start falling after 15 seconds of not hitting an obstacle




Step 10: Draw Car, Obstacles, and Finish Line on LED Matrix


#CODE

pixel.fill((0, 0, 0)) # Clear all pixels

for y in range(16):

pixel[y * 16 + 2] = (128, 128, 128) # Left road line in gray

pixel[y * 16 + 13] = (128, 128, 128) # Right road line in gray

car_index = car_position[1] * 16 + car_position[0]

pixel[car_index] = (0, 255, 0) # Car in green


#DESCRIPTION

Clears the screen, draws road lines, and places the car in green.


#CODE

for obstacle in obstacles:

for i in range(obstacle["width"]):

if 3 <= obstacle["x"] + i <= 12 and 0 <= obstacle["y"] < 16:

obstacle_index = obstacle["y"] * 16 + (obstacle["x"] + i)

pixel[obstacle_index] = (255, 0, 0) # Obstacles in red


#DESCRIPTION

Draws each obstacle as red pixels on the grid.


#CODE

if finish_line_y >= 0 and finish_line_y < 16:

for x in range(3, 13): # Draw the finish line across the road

pixel[int(finish_line_y) * 16 + x] = (255, 255, 0) # Finish line in yellow


#DESCRIPTION

Draws the finish line if it’s triggered, moving it down one row each cycle.


#CODE

pixel.show() # Update the LED matrix

#DESCRIPTION

Updates the NeoPixel display, refreshing the visual game state.



Downloads

Run Program

It is important that you start the receiver code before the sender and wait a few seconds before running the sender because of the delay in Bluetooth connection. The program should automatically start running once the receiver is connected to the sender, and the accelerometer values will be transmitted, and you can move the car.

Downloads