This article is an example of writing a game. Move the character to walk in the maze to collect flags that are randomly positioned as shown in Figure 1, where the character will walk in the specified channel and can’t penetrate the wall. With a warning sound when trying to walk in an impossible location and when walking in any direction will change the image of the character to turn the face to that direction. In addition, pressing A will randomize the position of the new flag, pressing B will randomize the player’s position, and pressing D will exit the program. The board for use is still dCoreML4M as before, let’s get started.

(Figure. 1 The game in this article)

Data structure

Let’s start by designing the data collection of diagrams, walls, flags, and characters, as well as taking the information to display on the OLED to give players a visual representation of the map, flags and characters.

Map storing

The maze variable is a variable that is assigned to store map data. The map uses 8 sets of 2-byte data to store the values of walls and walkways. It is defined as follows:

  • Which position is a corridor, set to 0
  • Which position is a wall, set to 1

By defining the data of each channel as 1-bit data as it has only 2 states, and the display has a resolution of 128×64, it can display an 8×8 dot image with 16 columns and 8 rows. So it’s as follows

maze = [
    0b1111000111111101,
    0b1000010000100101,
    0b0011000100000101,
    0b1001001110010101,
    0b1000001000010001,
    0b1011110010111011,
    0b0001000010000001,
    0b110001100011111
]

An example of bringing the map data to draw is as follows, and the resulting image is as shown in Figure 2. In the drawing process, using the principle of bit-by-bit access for each row and if it is 1 A drawing of the wall will be drawn to the row r*8 and column c*8 with a blit.

    for r in range(8):
        mask = 0b1000000000000000
        for c in range(16):
            if (maze[r] & mask):
                oled.blit(wall,c*8, r*8)
            mask >>= 1
(Figure. 2 Draw a map)

Wall image storing

The created wall image is stored in the variable wall as a FrameBuffer object of 8 bytearray, representing 8 rows of data and 8 columns in each row. The conditions for bit interpretation are defined as follows:

  • The value 0 is the point that does not need to be lit.
  • The value 1 is the point that need to be lit.

When 8 sets of data are obtained representing all rows, the frame buffer size must be specified as 8×8, and storage should be framebuf.MONO_HLSB. This means that the data is stored as mono (values 0 or 1), arranged horizontally. The drawing method blits the entire data set to the display as mentioned in the article on double buffering.

wall = FrameBuffer(bytearray([
    0b00011110,
    0b11100011,
    0b01000001,
    0b10000010,
    0b01000001,
    0b10000010,
    0b10000001,
    0b01111110
]),8,8,framebuf.MONO_HLSB)

Flag image storing

The flag image is stored in the flag variable, stored like a wall. A one-member list variable flagPos is created to store the row and column values used to display flag images.

flag = FrameBuffer(bytearray([
0b01000000,
0b01111111,
0b01111110,
0b01110000,
0b01000000,
0b01000000,
0b01100000,
0b11110000
]),8,8,framebuf.MONO_HLSB)
flagPos = [0,0]

Character image storing

The characters consist of four images representing facing left, facing right, facing up and facing down. Thus, it is necessary to design 4 data collections and store the data in actor variable as 4 FrameBuffer as shown in the code below. Each set contains 8 8-bit data, stored in framebuf.MONO_HLSB format. For this reason, the drawing or blit must specify the order of the actor data in the drawing process.

actor = [
    FrameBuffer(bytearray([
        0b00000000,
        0b00001000,
        0b00011000,
        0b00111000,
        0b11111000,
        0b00111000,
        0b00011000,
        0b00001000
    ]),8,8,framebuf.MONO_HLSB),
    FrameBuffer(bytearray([
        0b00000000,
        0b00100000,
        0b00110000,
        0b00111000,
        0b00111110,
        0b00111000,
        0b00110000,
        0b00100000
    ]),8,8,framebuf.MONO_HLSB),
    FrameBuffer(bytearray([
        0b00000000,
        0b00010000,
        0b00010000,
        0b00111000,
        0b01111100,
        0b11111110,
        0b00000000,
        0b00000000
    ]),8,8,framebuf.MONO_HLSB),
    FrameBuffer(bytearray([
        0b00000000,
        0b00000000,
        0b11111110,
        0b01111100,
        0b00111000,
        0b00010000,
        0b00010000,
        0b00000000
    ]),8,8,framebuf.MONO_HLSB)
]

The direction of the character’s orientation is to move the lever left, right, up, or down, storing the direction in the actorDir variable and the position where the character is displayed in the actorPos variable.

Movement

The movement is done by moving a lever, which is written as follows:

def getInput():
...
    jx = 4095-jst[0].read()
    jy = jst[1].read()
    
    if (jx < 1200):
        left = True
    elif (jx > 3000):
        right = True
    if (jy < 1200):
        up = True
    elif (jy > 3000):
        down = True

...

        (l,r,u,d,a,b,m1,m2)=getInput()

...

        if (l):
            actorDir = 0

        if (r):
            actorDir = 1

        if (u):
            actorDir = 2
    
        if (d):
            actorDir = 3

In addition to the levers that are used to direct the character’s movement, the A, B, and m1 keys are assigned the following functions.

  • Button b to randomize the position of the flag.
  • Button b to randomize the position of the character.
  • Button m1 to exit the program.

Random flag position

The method for randomizing the position of the flag is conditioned as follows:

  • Randomize the row values.
  • Randomize the column values.
  • If the column position value of the randomized row does not use a wall, calculate the flagPos values of c*8 and r*8, otherwise, repeat the randomization.

You can write the code from the above condition as

    while True:
        r = random.getrandbits(3) # 0..7
        c = random.getrandbits(4) # 0..15
        mask = (0b1000000000000000 >> c)
        if (maze[r] & mask):
            pass
        else:
            flagPos[0] = c*8
            flagPos[1] = r*8
            break

Random character position

There is an additional condition to randomize the character’s position from the random flag, The position must not be the same as the flag. So the code of random character position is as follows.

        r = random.getrandbits(3) # 0..7
        c = random.getrandbits(4) # 0..15
        mask = (0b1000000000000000 >> c)
        if (maze[r] & mask):
            pass
        else:
            if ((c*8 != flagPos[0]) and (r*8 != flagPos[1])): 
                actorPos[0] = c*8
                actorPos[1] = r*8
                break

Collision check

In the game there are two types:

  • A collision with a flag results in a random flag repositioning.
  • Collision with a wall or edge of the map will beep and do not move.

ตัAn example of coding is as follows.

move left
            if (actorPos[0] > 0):
                if (isWall(actorPos[0]-actorSize[0],actorPos[1])):
                    beep()
                else:
                    actorPos[0] -= actorSize[0]
move right
            if (actorPos[0] < scrWidth-actorSize[0]):
                if (isWall(actorPos[0]+actorSize[0],actorPos[1])):
                    beep()
                else:
                    actorPos[0] += actorSize[0]
move up
            if (actorPos[1] > 0):
                if (isWall(actorPos[0],actorPos[1]-actorSize[1])):
                    beep()
                else:
                    actorPos[1] -= actorSize[1]
move down
            if (actorPos[1] < scrHeight-actorSize[1]):
                if (isWall(actorPos[0],actorPos[1]+actorSize[1])):
                    beep()
                else:
                    actorPos[1] += actorSize[1]

Example Code

The snippet of code is as follows, from the above data storage structure and functions can be combined into the flag runner game code as follows:

#######################################################
### sprite
### board: ML4M
### (C) 2021, JarutEx
#######################################################
from machine import Pin,I2C,ADC, DAC
import math
import machine
import gc
import ssd1306
import random
import time
import sys
import framebuf
from framebuf import FrameBuffer


#######################################################
gc.enable()
gc.collect()
machine.freq(240000000)

#######################################################
sclPin = Pin(22)
sdaPin = Pin(21)
spkPin = DAC(Pin(25, Pin.OUT))
i2c = I2C(0,scl=sclPin, sda=sdaPin, freq=400000)
oled = ssd1306.SSD1306_I2C(128,64,i2c)
oled.poweron()
oled.contrast(255)
oled.init_display()
oled.fill(0)
oled.show()

#######################################################
swA = Pin(32, Pin.IN)
swB = Pin(33, Pin.IN)
swC = Pin(34, Pin.IN, Pin.PULL_UP) # select
swD = Pin(35, Pin.IN) # start
jst = (ADC(Pin(39)), ADC(Pin(36))) # X,Y
jst[0].width( ADC.WIDTH_12BIT ) # 12bit
jst[0].atten( ADC.ATTN_11DB ) # 3.3V
jst[1].width( ADC.WIDTH_12BIT ) # 12bit
jst[1].atten( ADC.ATTN_11DB ) # 3.3V

#######################################################
actor = [
    FrameBuffer(bytearray([
        0b00000000,
        0b00001000,
        0b00011000,
        0b00111000,
        0b11111000,
        0b00111000,
        0b00011000,
        0b00001000
    ]),8,8,framebuf.MONO_HLSB),
    FrameBuffer(bytearray([
        0b00000000,
        0b00100000,
        0b00110000,
        0b00111000,
        0b00111110,
        0b00111000,
        0b00110000,
        0b00100000
    ]),8,8,framebuf.MONO_HLSB),
    FrameBuffer(bytearray([
        0b00000000,
        0b00010000,
        0b00010000,
        0b00111000,
        0b01111100,
        0b11111110,
        0b00000000,
        0b00000000
    ]),8,8,framebuf.MONO_HLSB),
    FrameBuffer(bytearray([
        0b00000000,
        0b00000000,
        0b11111110,
        0b01111100,
        0b00111000,
        0b00010000,
        0b00010000,
        0b00000000
    ]),8,8,framebuf.MONO_HLSB)
]
scrWidth = 128
scrHeight = 64
actorSize = (8,8)
actorPos = [scrWidth//2-4,scrHeight//2-4]
actorDir = 0 #0,1,2,3:left,right,up,down

flag = FrameBuffer(bytearray([
0b01000000,
0b01111111,
0b01111110,
0b01110000,
0b01000000,
0b01000000,
0b01100000,
0b11110000
]),8,8,framebuf.MONO_HLSB)
flagPos = [0,0]

wall = FrameBuffer(bytearray([
    0b00011110,
    0b11100011,
    0b01000001,
    0b10000010,
    0b01000001,
    0b10000010,
    0b10000001,
    0b01111110
]),8,8,framebuf.MONO_HLSB)

maze = [
    0b1111000111111101,
    0b1000010000100101,
    0b0011000100000101,
    0b1001001110010101,
    0b1000001000010001,
    0b1011110010111011,
    0b0001000010000001,
    0b110001100011111
]

#######################################################
def beep():
    spkPin.write(255)
    time.sleep_ms(20)
    spkPin.write(0)
    
#######################################################
def getInput():
    left = False
    right = False
    up = False
    down = False
    button1 = False
    button2 = False
    button3 = False
    button4 = False
    #### Joystick
    jx = 4095-jst[0].read()
    jy = jst[1].read()
    
    if (jx < 1200):
        left = True
    elif (jx > 3000):
        right = True
    if (jy < 1200):
        up = True
    elif (jy > 3000):
        down = True
    # switch
    a = swA.value()
    b = swB.value()
    c = 1-swC.value()
    d = swD.value()
    if (a):
        t0 = time.ticks_ms()
        time.sleep_ms(25)
        a2 = swA.value()
        if (a == a2):
            button1 = True
    if (b):
        t0 = time.ticks_ms()
        time.sleep_ms(25)
        b2 = swB.value()
        if (b == b2):
            button2 = True
    if (c):
        t0 = time.ticks_ms()
        time.sleep_ms(25)
        c2 = swC.value()
        if (c == c2):
            button3 = True
    if (d):
        t0 = time.ticks_ms()
        time.sleep_ms(25)
        d2 = swD.value()
        if (d == d2):
            button4 = True
    return (left,right,up,down,button1, button2, button3, button4)

#######################################################
def drawMaze():
    for r in range(8):
        mask = 0b1000000000000000
        for c in range(16):
            if (maze[r] & mask):
                oled.blit(wall,c*8, r*8)
            mask >>= 1
    oled.blit(flag, flagPos[0],flagPos[1])

#######################################################
def randFlag():
    while True:
        r = random.getrandbits(3) # 0..7
        c = random.getrandbits(4) # 0..15
        mask = (0b1000000000000000 >> c)
        if (maze[r] & mask):
            pass
        else:
            flagPos[0] = c*8
            flagPos[1] = r*8
            break
    
#######################################################
def randActor():
    while True:
        r = random.getrandbits(3) # 0..7
        c = random.getrandbits(4) # 0..15
        mask = (0b1000000000000000 >> c)
        if (maze[r] & mask):
            pass
        else:
            if ((c*8 != flagPos[0]) and (r*8 != flagPos[1])): # ต้องไม่ใช่ที่เดียวกับธง
                actorPos[0] = c*8
                actorPos[1] = r*8
                break
            

#######################################################
def isWall(c,r):
    c //= 8
    r //= 8
    mask = (0b1000000000000000 >> c)
    if (maze[r] & mask):
        return True
    return False

#######################################################
### Main program
#######################################################
beep()
try:
    randFlag()
    randActor()
    while True:
        (l,r,u,d,a,b,m1,m2)=getInput()
        if (m1):
            break
        if (a):
            randFlag()
        if (b):
            randActor()
        if (l):
            actorDir = 0
            if (actorPos[0] > 0):
                if (isWall(actorPos[0]-actorSize[0],actorPos[1])):
                    beep()
                else:
                    actorPos[0] -= actorSize[0]

        if (r):
            actorDir = 1
            if (actorPos[0] < scrWidth-actorSize[0]):
                if (isWall(actorPos[0]+actorSize[0],actorPos[1])):
                    beep()
                else:
                    actorPos[0] += actorSize[0]

        if (u):
            actorDir = 2
            if (actorPos[1] > 0):
                if (isWall(actorPos[0],actorPos[1]-actorSize[1])):
                    beep()
                else:
                    actorPos[1] -= actorSize[1]
    
        if (d):
            actorDir = 3
            if (actorPos[1] < scrHeight-actorSize[1]):
                if (isWall(actorPos[0],actorPos[1]+actorSize[1])):
                    beep()
                else:
                    actorPos[1] += actorSize[1]

        oled.fill(0)
        drawMaze()
        oled.blit(actor[actorDir], actorPos[0], actorPos[1])
        oled.show()
        time.sleep_ms(100)
except KeyboardInterrupt:
    pass
beep()
oled.poweroff()

Conclusion

From this article, you will find that the basics of using switches and readings from the ADC can be used to determine direction or selection conditions within the game. And from the graphical display when drawing maps, characters and flags along with setting the conditions of movement and events can create a simple game of collecting flags. A good game will have a lot of other things to add, such as character or environment animations, sound effects, sound effects. and the important thing is conditions for passing scenes that must create challenges for players.

Finally, We hope that this article was helpful and gave some ideas to those who are interested in developing simple and easy-to-make games on their own. Have fun programming.

If you want to talk with us, feel free to leave comments below!!

(C) 2020-2021, By Jarut Busarathid and Danai Jedsadathitikul
Updated 2021-12-12