Close

Boulderdash

A project log for Tiny OLED

Tiny handheld with OLED and XIAO.

dehipudeʃhipu 09/13/2024 at 21:010 Comments

I while ago I was trying to make a game shield for the micro:bit, #Micro:Boy, but trying to write a simple game for it in MicroPython I quickly ran out of memory – not for the game itself, but for compiling it source code. Minifying the code helped somewhat, but not enough to get something playable,

But this time, with CitcuitPython, I have orders of magnitude more memory,  and can even split the code into multiple files if I have to. It's also somewhat faster, using SPI instead of I2C for communication. So I ported the code, and got something like this:

import board
import busdisplay
import busio
import displayio
import fourwire
import keypad
import time
import keypad
import supervisor


_TICKS_PERIOD = const(1<<29)
_TICKS_MAX = const(_TICKS_PERIOD-1)
_TICKS_HALFPERIOD = const(_TICKS_PERIOD//2)


class Blitty:
    UP = 0x01
    DOWN = 0x02
    LEFT = 0x04
    RIGHT = 0x08
    BUTTON_O = 0x10
    BUTTON_X = 0x20

    def __init__(self, delay_ms=100):
        self.buffer = bytearray(1024)
        self.dmin = bytearray(8)
        self.dmax = bytearray(128 for i in range(8))

        displayio.release_displays()
        self.bus = fourwire.FourWire(busio.SPI(board.SCK, board.MOSI),
             command=board.D7, chip_select=board.D9, baudrate=10_000_000)
        self.display = busdisplay.BusDisplay(
            self.bus,
            (b"\xae\x00\xd5\x01\x80\xa8\x01\x3f\xd3\x01\x00\x40\x00\xad\x01"
             b"\x8b\xa1\x00\xc8\x00\xda\x01\x12\x81\x01\xff\xd9\x01\x1f\xdb"
             b"\x01\x40\x20\x01\x20\x33\x00\xa6\x00\xa4\x00\xaf\x00"),
            width=128,
            height=64,
            colstart=2,
            rowstart=0,
            color_depth=1,
            grayscale=True,
            pixels_in_byte_share_row=False,
            data_as_commands=True,
            brightness_command=0x81,
            SH1107_addressing=True,
            auto_refresh=False,
        )
        self.delay = delay_ms
        self.next_tick = (supervisor.ticks_ms() + self.delay) % _TICKS_PERIOD
        self.keypad = keypad.Keys((board.D5, board.D1, board.D2, board.D3,
            board.D4, board.D0), value_when_pressed=False, interval=0.01)
        self.last_buttons = 0
        self.event = keypad.Event(0, False)

    def buttons(self):
        buttons = self.last_buttons
        events = self.keypad.events
        while events:
            if events.get_into(self.event):
                bit = 1 << self.event.key_number
                if self.event.pressed:
                    buttons |= bit
                    self.last_buttons |= bit
                else:
                    self.last_buttons &= ~bit
        return buttons

    def tick(self):
        diff = (self.next_tick - supervisor.ticks_ms()) & _TICKS_MAX
        diff = ((diff + _TICKS_HALFPERIOD) & _TICKS_MAX) - _TICKS_HALFPERIOD
        time.sleep(diff / 1000)
        self.next_tick = (self.next_tick + self.delay) % _TICKS_PERIOD

    def update(self):
        b = memoryview(self.buffer)
        addr = 0
        for page in range(8):
            dmin = self.dmin[page]
            dmax = self.dmax[page]
            if dmax:
                self.bus.send(0xb0 | page, b'')
                self.bus.send(0x00 | ((2 + dmin) & 0x0f), b'')
                self.bus.send(0x10 | ((2 + dmin) >> 4) & 0x0f, b'')
                self.bus.send(0x40, b[addr + dmin:addr + dmax])
            addr += 128
            self.dmax[page] = 0
            self.dmin[page] = 127

    def blit(self, x, y, data, mask=b''):
        if not 0 <= x <= 120:
            return
        b = memoryview(self.buffer)
        page = y // 8
        shift = y % 8
        if 0 <= page <= 7:
            addr = x + 128 * page
            for byte in mask:
                b[addr] &= ~(byte << shift)
                addr += 1
            addr = x + 128 * page
            for byte in data:
                b[addr] ^= (byte << shift) & 0xff
                addr += 1
            self.dmin[page] = min(self.dmin[page], x)
            self.dmax[page] = max(self.dmax[page], x + 7)
        page += 1
        if 0 <= page <= 7 and shift:
            shift = 8 - shift
            addr = x + 128 * page
            for byte in mask:
                b[addr] &= ~(byte >> shift)
                addr += 1
            addr = x + 128 * page
            for byte in data:
                b[addr] ^= byte >> shift
                addr += 1
            self.dmin[page] = min(self.dmin[page], x)
            self.dmax[page] = max(self.dmax[page], x + 7)


MAN_MASK = bytes((
    0b00111100,
    0b11111110,
    0b11111111,
    0b11111111,
    0b11111111,
    0b11111111,
    0b11111110,
    0b00111000,
))
MAN = (bytes((
    0b00000000,
    0b00010000,
    0b01111010,
    0b00111110,
    0b00111010,
    0b01111110,
    0b00100000,
    0b00000000,
)), bytes((
    0b00000000,
    0b00100000,
    0b00011010,
    0b01111110,
    0b01111010,
    0b00111110,
    0b00010000,
    0b00000000,
)))
DIRT = bytes((
    0b10001010,
    0b00100000,
    0b00000101,
    0b01010000,
    0b00000101,
    0b00100000,
    0b10001010,
    0b00100000,
))
DARK = bytes((
    0b00000000,
    0b00000000,
    0b00000000,
    0b00000000,
    0b00000000,
    0b00000000,
    0b00000000,
    0b00000000,
))
ROCK_MASK = bytes((
    0b00111100,
    0b01111110,
    0b11111111,
    0b11111111,
    0b11111111,
    0b11111111,
    0b01111110,
    0b00111100,
))
ROCK = bytes((
    0b00000000,
    0b00110100,
    0b01011010,
    0b00111110,
    0b01010110,
    0b00101010,
    0b00010100,
    0b00000000,
))


blitty = Blitty()
dirt = bytearray(16)
for y in range(8):
    for x in range(16):
        blitty.blit(x * 8, y * 8, DIRT)
rocks = {(0, 0), (3, 3), (3, 4), (5, 2), (10, 1)}
for x, y in rocks:
    blitty.blit(x * 8, y * 8, ROCK, ROCK_MASK)
x, y = 0, 8
dx = dy = steps = 0


while True:
    for rx, ry in rocks:
        if dirt[rx] & (1 << (ry + 1)) and not (
                (y // 8 == ry + 1) and
                (rx * 8 - 6 <= x <= rx * 8 + 6)
            ) and (rx, ry + 1) not in rocks:
            dirt[rx] |= 1 << ry
            blitty.blit(rx * 8, ry * 8, DARK, ROCK_MASK)
            blitty.blit(rx * 8, ry * 8 + 8, ROCK, ROCK_MASK)
            rocks.remove((rx, ry))
            rocks.add((rx, ry + 1))
    key = blitty.buttons()
    if steps == 0:
        dirt[x // 8] |= 1 << (y // 8)
        dx = dy = 0
        if key & blitty.RIGHT and x < 120:
            dx = 1
        elif key & blitty.LEFT and x > 0:
            dx = -1
        elif key & blitty.DOWN and y < 56:
            dy = 1
        elif key & blitty.UP and y > 0:
            dy = -1
        if (x // 8 + dx, y // 8 + dy) in rocks:
            if dx:
                rx = x // 8 + dx
                ry = y // 8
                if (rx + dx, ry) in rocks or not dirt[rx + dx] & (1 << ry):
                    dx = 0
                else:
                    blitty.blit(rx * 8, ry * 8, DARK, ROCK_MASK)
                    blitty.blit(rx * 8 + dx * 8, ry * 8, ROCK, ROCK_MASK)
                    rocks.remove((rx, ry))
                    rocks.add((rx + dx, ry))
            dy = 0
        if dx or dy:
            steps = 7
    else:
        steps -= 1
    x += dx
    y += dy
    man = MAN[(steps // 2) % 2]
    if dx + dy > 0:
        man = bytes(reversed(man))
    blitty.blit(x, y, man, MAN_MASK)
    blitty.update()
    blitty.blit(x, y, DARK, MAN_MASK)
    blitty.tick()

It's not a complete game now, but you can dig around, push the boulders, and they fall down when not supported. All I need is to make them slip sideways when on top of another boulder, and add some diamonds for collecting, then design a few levels.

But testing this game also made me make a small change in the hardware. While it ran the game menu and the snake game on battery without problems, as soon as a large portion of the screen was filled, it refused to run on the battery anymore. A quick measurement showed that the voltage of the battery simply drops too much for the boost converter to cope. The whole thing is using too much current. The solution turned out to be simple: replace the 330k IREF resistor with a 680k one. Of course that lowers the brightness of the display, but it also makes it possible to use it with the battery, so I will take it.

Discussions