-
Boulderdash
09/13/2024 at 21:01 • 0 commentsI 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.
-
The Challenge
09/10/2024 at 22:42 • 0 commentsWhen the Tiny Games Challenge was announced, I really debated whether I should enter it or not. After all, I was explicitly called out in the announcement. It would be very unfair to the other participants if I entered one of my projects that I have been polishing for over five years. So I initially decided not to enter.
But recently I have been preparing for a workshop on becoming "full stack game dev", that is, on building your own game console and then making games for it, and while I did that, I looked through some of my old prototypes, and I realized that all of them were made to either be very minimalist in terms of parts used, or to explore a certain cost-saving technique, like making as much of the device out of PCB, for example, or seeing how many capacitors I can skip in the design before it stops working. I never really built anything "proper", because initially I simply didn't have the skill to do it correctly, and later on I was chasing interesting hacks.
So when I was ordering PCBs, I decided to quickly make another handheld game console, but this time made properly, without hacks, without requiring special batteries or having weird switch for the power, or using the minimal number of parts. Just a normal, solid design, that is not an exercise, but something to actually use as a base for further projects. And since I used a XIAO footprint for the microcontroller board, I can also use it to experiment with new microcontrollers coming out every year.
And when the PCBs arrived today, and I assembled the prototype and got it working, I decided to enter it into the contest after all. It's not going to compete with people who have build very innovative designs, or people who really focused on coming up with a fun game. I didn't even program a special game for it — I just ported the Pew library, so that all the PewPew games, such as snake, tetris, sokoban or othello will work on it. But I think that it can be used for learning, and as a base for future projects. Things like tamagotchi clones, flipper zero wannabes, or control panels for home automation.
-
Better Power Circuit
09/10/2024 at 21:41 • 0 commentsI have made similar handhelds before, and if you look through hackaday.io a bit, you will find many similar consoles made by other people, all of them using the same 128x64 OLED display, six buttons, and a variety of microcontrollers. So what's interesting in doing it one more time?
Well, I've been iterating on my handheld game consoles for a while, and I have learned some things in the process — I'm not really a professional electrical engineer, so a lot of things I use in my designs are just tricks that I have learned from somewhere (often lifted from other people's designs). This time I used some new tricks.
First and foremost, there is a boost converter on this board, so not only will it run from a regular coin cell battery (not rechargeable), but it will run on it until there is no electron left to suck out of it. And it should even run with the weaker batteries sold in some countries.
Second, the power switch is no longer switching between battery and USB power. There is now a proper power switching circuit, that cuts the battery power when USB is connected.
Third, I added a reset circuit to the OLED screen, that handles the required physical reset of the display some time after powering it on. Previously I wasted a GPIO pin just for that, but adding those simple three components lets me use the pin for a speaker instead..
Fourth, I'm back to physical tact switches, instead of touch pads, and I'm using some very nice, low-force clicky switches that I found through a lot of trial and error.
Fifth, the display is right side up this time, with a slot in the middle of the PCB for the FPC connector. It took some work to get the spacing right for this. There are even pilot holes for soldering it more easily. I might make the slot a milimeter or so longer next time, to make it easier to get the connector through it, though (right now you have to bend it to get it through the hole).
Sixth, I also added an optional footprint for a BH1/2AA battery holder, for those who want to have a battery that lasts a little bit longer, but still is pretty small.
Seventh, the speaker is without amplification, just with a protecting resistor. This is because any louder sounds would rapidly drain the battery. We only want very quiet and simple sounds here.