BBC micro:bit
Bit:Commander - Unicorn Commander

Introduction

It's quite easy to knock up a quick project using the Bit:Commander as a controller. When the 5x5 matrix isn't quite enough, you can step up to one of the larger Neopixel matrices and rewrite your matrix games on a larger canvas. For this little project, I used the Unicorn pHAT, a Raspberry Pi accessory made by Pimoroni. I also used the 4tronix Bt:2:Pi.

This is a double micro:bit project. The commands entered on the Bit:Commander are sent by radio to the micro:bit that is connected to the Unicorn pHAT.

micro:bit circuit

Sending

from microbit import *
import radio

chnl = 10
radio.config(channel=chnl)
radio.on()

# read the buttons and use binary 4 bits to represent
# the button states
def get_btns():
    pattern = 0
    for i,p in enumerate([pin12,pin15,pin14,pin16]):
        pattern += p.read_digital() << i
    return pattern


last = 0    
while True:
    btns = get_btns()
    e = [((last >> i & 1)<<1) + (btns >> i & 1) for i in range(4)]
    if e[0]==2:
        radio.send("N")
    elif e[1]==2:
        radio.send("E")
    elif e[2]==2:
        radio.send("S")
    elif e[3]==2:
        radio.send("W")
    last = btns
    sleep(20)

Receiving

from microbit import *
import neopixel
import radio

chnl = 10
radio.config(channel=chnl)
radio.on()

# Initialise neopixels
npix = neopixel.NeoPixel(pin0, 32)

# Define some colours
red = (64,0,0)
green = (0,64,0)
blue = (0,0,64)
nocol = (0,0,0)

# light all neopixels with given colour
def light_all(col):
    for pix in range(0, len(npix)):
        npix[pix] = col
    npix.show()

# set a pixel colour using x,y coordinates  
def set_pix(x,y,col):
    npix[y*8+x] = col
    npix.show()
    
# turn them off        
light_all(nocol)
sleep(500)
x = 3
y = 1
set_pix(x,y,red)    
while True:
    s = radio.receive()
    if s is not None:
        set_pix(x,y,nocol)
        if s=="N":
            y -= 1
        elif s=="S":
            y += 1
        elif s=="E":
            x += 1
        elif s=="W": 
            x -= 1
        x = max(0,min(x,7))
        y = max(0,min(y,3))
        set_pix(x,y,red)    
    sleep(20)    

This idea is quite easy to replicate with other Neopixel matrices. The trick is to work out a formula to convert a grid position into a single number representing the position of the pixel in the chain. In this program, the set_pix function does that.

micro:bit circuit

When you use a different Neopixel matrix, you first work out how the pixels are connected, then work out a formula to turn a grid position into a pixel number. For the Unicorn HAT (the 8x8 matrix), the pixels are numbered up and down in rows. The following program is the movable dot for the larger matrix.

from microbit import *
import neopixel
import radio

chnl = 10
radio.config(channel=chnl)
radio.on()

# Initialise neopixels
npix = neopixel.NeoPixel(pin0, 64)

# Define some colours
red = (32,0,0)
green = (0,32,0)
blue = (0,0,32)
nocol = (0,0,0)

# light all neopixels with given colour
def light_all(col):
    for pix in range(0, len(npix)):
        npix[pix] = col
    npix.show()
    return

# wipe a colour across pixels one at a time    
def wipe(col, delay):
    for pix in range(0, len(npix)):
        npix[pix] = col
        npix.show()
        sleep(delay)
    return

# set a pixel colour using x,y coordinates  
def set_pix(x,y,col):
    if x%2==0:
        npix[x*8+y] = col
    else:
        npix[x*8+7-y] = col
    npix.show()

# translate coordinates to pixel number
def xy2pix(x,y):
    if x%2==0:
        return x*8+y
    else:
        return x*8+7-y

# turn them off        
light_all(nocol)
x = 4
y = 4
set_pix(x,y,red)    
while True:
    s = radio.receive()
    if s is not None:
        set_pix(x,y,nocol)
        if s=="N":
            y -= 1
        elif s=="S":
            y += 1
        elif s=="E":
            x += 1
        elif s=="W": 
            x -= 1
        x = max(0,min(x,7))
        y = max(0,min(y,7))
        set_pix(x,y,red)    
    sleep(20)    

Snake

Snake needs a little more space than the built-in matrix allows. The 8x8 grid of the Unicorn HAT is large enough to make the game interesting to play. In the image below, there is a Unicorn HAT mounted on a Bit:2:Pi. The pixels are set at quite low brightness but are still difficult to capture in a photograph.

micro:bit circuit

The program receives the same signals from the Bit:Commander. The pushbuttons are used for directional control and to start the game. You might want to change the first example to look for a 1 instead of a 2. That way, button presses, rather than releases, will trigger the movements.

This is a far from perfect way to implement the game of snake but it worked well enough to prove the concept.

from microbit import *
import neopixel
import radio
import random

chnl = 10
radio.config(channel=chnl)
radio.on()

# Initialise neopixels
npix = neopixel.NeoPixel(pin0, 64)

# Define some colours
red = (32,0,0)
green = (0,32,0)
blue = (0,0,32)
nocol = (0,0,0)

# light all neopixels with given colour
def light_all(col):
    for pix in range(0, len(npix)):
        npix[pix] = col

def light_snake(on=True):
        # light head blue
        hx,hy = snake[0]
        if on:
            set_pix(hx,hy,blue)
        else:
            set_pix(hx,hy,nocol)
        # light rest of snake red
        for i in range(1,len(snake)):
            xx,yy = snake[i]
            if on:
                set_pix(xx,yy,red)
            else:
                set_pix(xx,yy,nocol)

def update_snake(x,y):
    for i in range(len(snake)-1,0,-1):
        snake[i] = snake[i-1]
    snake[0] = (x,y)
          
# set a pixel colour using x,y coordinates  
def set_pix(x,y,col):
    if x%2==0:
        npix[x*8+y] = col
    else:
        npix[x*8+7-y] = col

def place_munch():
    p = random.randint(0,63)
    while npix[p] != nocol:
        p = random.randint(0,63)
    return p
    
# translate coordinates to pixel number
def xy2pix(x,y):
    if x%2==0:
        return x*8+y
    else:
        return x*8+7-y

def play_game():
    global snake
    snake = [(4,4)]   
    light_snake(True)
    munch = place_munch()
    npix[munch] = green
    npix.show()
    dx = -1
    dy = 0
    delay = 500
    last = running_time()
    playing = True
    s = radio.receive()
    while playing:
        s = radio.receive()
        if s is not None:               
            if s=="N":
                dy = -1
                dx = 0
            elif s=="S":
                dy = 1
                dx = 0
            elif s=="E":
                dx = 1
                dy = 0
            elif s=="W": 
                dx = -1
                dy = 0
        if running_time()-last>delay:            
            # undraw snake
            light_snake(False)
            # update head position
            x,y = snake[0]
            x = x + dx
            y = y + dy
            # keep head on screen
            if x<0: x = 7
            if x>7: x = 0
            if y<0: y = 7
            if y>7: y = 0        
            # check for munch
            if xy2pix(x,y)==munch:
                snake.append((0,0))
                # update snake parts
                update_snake(x,y)
                munch = place_munch()
                npix[munch] = green                   
            elif (x,y) in snake:
                # collision with body
                playing = False               
                display.show(Image.SAD)               
                sleep(3000)              
                light_all(nocol)
                npix.show()
                s = radio.receive()
                display.show(Image.ARROW_N)                                
            else:
                # update snake parts
                update_snake(x,y)           
                light_snake(True)
                npix[munch] = green
                npix.show()               
                last = running_time()
        

       
light_all(nocol)
npix.show()
snake = [(4,4)]
display.show(Image.ARROW_N)
while True:    
    s = radio.receive()
    if s is not None:
        if s=="N":
            display.show(Image.HAPPY)
            play_game()                        

The Neopixel matrix is accessed via a one-dimensional index. This program translates those indices into two dimensional co-ordinates to make the game logic easier. The only exception to this is with the positioning of the thing that the snake eats. On the Unicorn HAT, the pixels are connected down,up,down,... in columns, making it a little more complex to encode grid movements according to a change in index. This would halve the memory required to store the snake parts and reduce the operations needed to draw the snake.

The game does need some sound. Since the Bit:Commander is being used to play the game, it makes sense to use its buzzer for the sound effects. To do that, messages need to be sent back from the receiver. If the sound effects are playing on the controller, the game code needs only to send a message to trigger a sound effect. That avoids any timing conflicts with the code that keeps the game display running at the correct rate. The obvious sound effects are a beep every time the game updates the screen, a power-up noise when a green blob is eaten, and a nasty tone or power-down noise when a snake crashes into itself.

With a bit of a tidy up in the game logic, you may wish to improve the progression of the game. The delay variable can be adjusted when a treat is eaten, making the screen update a little quicker.