From 054d2264b07069dbf54815e09c0de84faaff9dac Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Fri, 26 Apr 2019 21:47:04 +0200 Subject: [PATCH 01/59] Fix docs --- docs/audioio.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docs/audioio.py diff --git a/docs/audioio.py b/docs/audioio.py new file mode 100644 index 0000000..3a1213c --- /dev/null +++ b/docs/audioio.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + From 66bdcc7d39df4c5e7e891bf912ec6b2be2d8976c Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Fri, 26 Apr 2019 22:03:16 +0200 Subject: [PATCH 02/59] Update docs --- README.rst | 19 +++++++++++++++++-- stage.py | 20 ++++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 53dd0bb..bb6f733 100644 --- a/README.rst +++ b/README.rst @@ -28,6 +28,21 @@ st7735r ugame ======= -.. automodule:: ugame - :members: +.. module:: ugame + +.. data:: display + + An initialized display, that can be used for creating Stage objects. + +.. data:: buttons + + An instance of ``GamePad`` or other similar class, that has a + ``get_pressed`` method for retrieving a bit mask of pressed buttons. That + value can be then checked with & operator against the constants: ``K_UP``, + ``K_DOWN``, ``K_LEFT``, ``K_RIGHT``, ``K_X``, ``K_O`` and on some platforms + also: ``K_START`` and ``K_SELECT``. + +.. data:: audio + And instance of the ``Audio`` or other similar class, that has ``play``, + ``stop`` and ``mute`` methods. diff --git a/stage.py b/stage.py index f337173..5c020a7 100644 --- a/stage.py +++ b/stage.py @@ -145,6 +145,7 @@ def collide(ax0, ay0, ax1, ay1, bx0, by0, bx1=None, by1=None): class Audio: + """Play sounds.""" last_audio = None def __init__(self, speaker_pin, mute_pin=None): @@ -156,14 +157,21 @@ def __init__(self, speaker_pin, mute_pin=None): self.audio = audioio.AudioOut(speaker_pin) def play(self, audio_file, loop=False): + """ + Start playing an open file ``audio_file``. If ``loop`` is ``True``, + repeat until stopped. This function doesn't block, the sound is + played in the background. + """ self.stop() wave = audioio.WaveFile(audio_file) self.audio.play(wave, loop=loop) def stop(self): + """Stop playing whatever sound is playing.""" self.audio.stop() def mute(self, value=True): + """Enable or disable all sounds.""" if self.mute_pin: self.mute_pin.value = not value @@ -289,7 +297,8 @@ def move(self, x, y, z=None): class WallGrid(Grid): """ A special grid, shifted from its parents by half a tile, useful for making - nice-looking corners of walls and similar structures.""" + nice-looking corners of walls and similar structures. + """ def __init__(self, grid, walls, bank, palette=None): super().__init__(bank, grid.width + 1, grid.height + 1, palette) @@ -341,7 +350,14 @@ def move(self, x, y, z=None): self.layer.move(int(x), int(y)) def set_frame(self, frame=None, rotation=None): - """Set the current graphic and rotation of the sprite.""" + """ + Set the current graphic and rotation of the sprite. + + The possible values for rotation are: 0 - none, 1 - 90° clockwise, + 2 - 180°, 3 - 90° counter-clockwise, 4 - mirrored, 5 - 90° clockwise + and mirrored, 6 - 180° and mirrored, 7 - 90° counter-clockwise and + mirrored. + """ if frame is not None: self.frame = frame From 2999a02ea80d0fa45438df81badeb3ffa7f28abb Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Tue, 30 Apr 2019 16:57:30 +0200 Subject: [PATCH 03/59] Fix non-ascii characters in docs --- README.rst | 4 ---- stage.py | 9 ++++----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index bb6f733..dfadce4 100644 --- a/README.rst +++ b/README.rst @@ -21,10 +21,6 @@ stage .. automodule:: stage :members: -st7735r -======= -.. automodule:: st7735r - :members: ugame ======= diff --git a/stage.py b/stage.py index 5c020a7..dad27e3 100644 --- a/stage.py +++ b/stage.py @@ -353,11 +353,10 @@ def set_frame(self, frame=None, rotation=None): """ Set the current graphic and rotation of the sprite. - The possible values for rotation are: 0 - none, 1 - 90° clockwise, - 2 - 180°, 3 - 90° counter-clockwise, 4 - mirrored, 5 - 90° clockwise - and mirrored, 6 - 180° and mirrored, 7 - 90° counter-clockwise and - mirrored. - """ + The possible values for rotation are: 0 - none, 1 - 90 degrees + clockwise, 2 - 180 degrees, 3 - 90 degrees counter-clockwise, 4 - + mirrored, 5 - 90 degrees clockwise and mirrored, 6 - 180 degrees and + mirrored, 7 - 90 degrees counter-clockwise and mirrored. """ if frame is not None: self.frame = frame From 7744365d887df33a04e25fdec72cc610a5cb0d08 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Wed, 8 May 2019 16:36:46 +0200 Subject: [PATCH 04/59] Do a full reinitialization of the TFT for pybadge --- pybadge/ugame.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/pybadge/ugame.py b/pybadge/ugame.py index 51490ef..f71310d 100644 --- a/pybadge/ugame.py +++ b/pybadge/ugame.py @@ -21,15 +21,40 @@ K_SELECT = 0x08 # re-initialize the display for correct rotation and RGB mode + +_TFT_INIT = ( + b"\x01\x80\x96" # SWRESET and Delay 150ms + b"\x11\x80\xff" # SLPOUT and Delay + b"\xb1\x03\x01\x2C\x2D" # _FRMCTR1 + b"\xb2\x03\x01\x2C\x2D" # _FRMCTR2 + b"\xb3\x06\x01\x2C\x2D\x01\x2C\x2D" # _FRMCTR3 + b"\xb4\x01\x07" # _INVCTR line inversion + b"\xc0\x03\xa2\x02\x84" # _PWCTR1 GVDD = 4.7V, 1.0uA + b"\xc1\x01\xc5" # _PWCTR2 VGH=14.7V, VGL=-7.35V + b"\xc2\x02\x0a\x00" # _PWCTR3 Opamp current small, Boost frequency + b"\xc3\x02\x8a\x2a" + b"\xc4\x02\x8a\xee" + b"\xc5\x01\x0e" # _VMCTR1 VCOMH = 4V, VOML = -1.1V + b"\x20\x00" # _INVOFF + b"\x36\x01\xa8" # _MADCTL + # 1 clk cycle nonoverlap, 2 cycle gate rise, 3 sycle osc equalie, + # fix on VTL + b"\x3a\x01\x05" # COLMOD - 16bit color + b"\xe0\x10\x02\x1c\x07\x12\x37\x32\x29\x2d\x29\x25\x2B\x39\x00\x01\x03\x10" # _GMCTRP1 Gamma + b"\xe1\x10\x03\x1d\x07\x06\x2E\x2C\x29\x2D\x2E\x2E\x37\x3F\x00\x00\x02\x10" # _GMCTRN1 + b"\x13\x80\x0a" # _NORON + b"\x29\x80\x64" # _DISPON +) displayio.release_displays() _tft_spi = busio.SPI(clock=board.TFT_SCK, MOSI=board.TFT_MOSI) _tft_spi.try_lock() _tft_spi.configure(baudrate=24000000) _tft_spi.unlock() _fourwire = displayio.FourWire(_tft_spi, command=board.TFT_DC, - chip_select=board.TFT_CS) -display = displayio.Display(_fourwire, b'\x36\x01\xa8', width=160, height=128, + chip_select=board.TFT_CS, reset=board.TFT_RST) +display = displayio.Display(_fourwire, _TFT_INIT, width=160, height=128, rotation=0, backlight_pin=board.TFT_LITE) +del _TFT_INIT display.auto_brightness = True buttons = gamepadshift.GamePadShift( From bae3a0952f28e7e8ba36400eb724e76068d91456 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Wed, 8 May 2019 17:28:19 +0200 Subject: [PATCH 05/59] Reset the display manually in pybadge --- pybadge/ugame.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pybadge/ugame.py b/pybadge/ugame.py index f71310d..77614e9 100644 --- a/pybadge/ugame.py +++ b/pybadge/ugame.py @@ -9,6 +9,7 @@ import stage import displayio import busio +import time K_X = 0x02 @@ -51,7 +52,11 @@ _tft_spi.configure(baudrate=24000000) _tft_spi.unlock() _fourwire = displayio.FourWire(_tft_spi, command=board.TFT_DC, - chip_select=board.TFT_CS, reset=board.TFT_RST) + chip_select=board.TFT_CS) +_reset = digitalio.DigitalInOut(board.TFT_RST) +_reset.switch_to_output(value=0) +time.sleep(0.05) +_reset.value = 1 display = displayio.Display(_fourwire, _TFT_INIT, width=160, height=128, rotation=0, backlight_pin=board.TFT_LITE) del _TFT_INIT From 733cdf591639f9dbb5d9187a21411f759e1ba82f Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Wed, 8 May 2019 17:28:55 +0200 Subject: [PATCH 06/59] Sleep after reset --- pybadge/ugame.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pybadge/ugame.py b/pybadge/ugame.py index 77614e9..8306f45 100644 --- a/pybadge/ugame.py +++ b/pybadge/ugame.py @@ -57,6 +57,7 @@ _reset.switch_to_output(value=0) time.sleep(0.05) _reset.value = 1 +time.sleep(0.05) display = displayio.Display(_fourwire, _TFT_INIT, width=160, height=128, rotation=0, backlight_pin=board.TFT_LITE) del _TFT_INIT From 397badae5863a1ce932213ef527e82314ac498a1 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Mon, 20 May 2019 22:02:30 +0200 Subject: [PATCH 07/59] Fix OverflowError in read_palette CircuitPython now has overflow checks, so the old code crashes. --- stage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stage.py b/stage.py index dad27e3..e1d0ff2 100644 --- a/stage.py +++ b/stage.py @@ -206,7 +206,7 @@ def read_palette(self): for color in range(self.colors): buffer = f.read(4) c = color565(buffer[0], buffer[1], buffer[2]) - palette[color] = (c << 8) | (c >> 8) + palette[color] = ((c << 8) | (c >> 8)) & 0xffff return palette def read_data(self, offset=0, buffer=None): From baed2ee402985dcfe1022d713e140b3bcb268327 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Mon, 17 Jun 2019 19:55:28 +0200 Subject: [PATCH 08/59] Add support for PyGamer --- pygamer/stage.py | 1 + pygamer/ugame.py | 95 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 120000 pygamer/stage.py create mode 100644 pygamer/ugame.py diff --git a/pygamer/stage.py b/pygamer/stage.py new file mode 120000 index 0000000..2dedc93 --- /dev/null +++ b/pygamer/stage.py @@ -0,0 +1 @@ +../stage.py \ No newline at end of file diff --git a/pygamer/ugame.py b/pygamer/ugame.py new file mode 100644 index 0000000..7fdde13 --- /dev/null +++ b/pygamer/ugame.py @@ -0,0 +1,95 @@ +""" +A helper module that initializes the display and buttons for the uGame +game console. See https://hackaday.io/project/27629-game +""" + +import board +import digitalio +import gamepadshift +import stage +import displayio +import busio +import time + + +K_X = 0x01 +K_O = 0x02 +K_START = 0x04 +K_SELECT = 0x08 +K_DOWN = 0x10 +K_LEFT = 0x20 +K_RIGHT = 0x40 +K_UP = 0x80 + +# re-initialize the display for correct rotation and RGB mode + +_TFT_INIT = ( + b"\x01\x80\x96" # SWRESET and Delay 150ms + b"\x11\x80\xff" # SLPOUT and Delay + b"\xb1\x03\x01\x2C\x2D" # _FRMCTR1 + b"\xb2\x03\x01\x2C\x2D" # _FRMCTR2 + b"\xb3\x06\x01\x2C\x2D\x01\x2C\x2D" # _FRMCTR3 + b"\xb4\x01\x07" # _INVCTR line inversion + b"\xc0\x03\xa2\x02\x84" # _PWCTR1 GVDD = 4.7V, 1.0uA + b"\xc1\x01\xc5" # _PWCTR2 VGH=14.7V, VGL=-7.35V + b"\xc2\x02\x0a\x00" # _PWCTR3 Opamp current small, Boost frequency + b"\xc3\x02\x8a\x2a" + b"\xc4\x02\x8a\xee" + b"\xc5\x01\x0e" # _VMCTR1 VCOMH = 4V, VOML = -1.1V + b"\x20\x00" # _INVOFF + b"\x36\x01\xa8" # _MADCTL + # 1 clk cycle nonoverlap, 2 cycle gate rise, 3 sycle osc equalie, + # fix on VTL + b"\x3a\x01\x05" # COLMOD - 16bit color + b"\xe0\x10\x02\x1c\x07\x12\x37\x32\x29\x2d\x29\x25\x2B\x39\x00\x01\x03\x10" # _GMCTRP1 Gamma + b"\xe1\x10\x03\x1d\x07\x06\x2E\x2C\x29\x2D\x2E\x2E\x37\x3F\x00\x00\x02\x10" # _GMCTRN1 + b"\x13\x80\x0a" # _NORON + b"\x29\x80\x64" # _DISPON +) +displayio.release_displays() +_tft_spi = busio.SPI(clock=board.TFT_SCK, MOSI=board.TFT_MOSI) +_tft_spi.try_lock() +_tft_spi.configure(baudrate=24000000) +_tft_spi.unlock() +_fourwire = displayio.FourWire(_tft_spi, command=board.TFT_DC, + chip_select=board.TFT_CS) +_reset = digitalio.DigitalInOut(board.TFT_RST) +_reset.switch_to_output(value=0) +time.sleep(0.05) +_reset.value = 1 +time.sleep(0.05) +display = displayio.Display(_fourwire, _TFT_INIT, width=160, height=128, + rotation=0, backlight_pin=board.TFT_LITE) +del _TFT_INIT +display.auto_brightness = True + + +class Buttons: + def __init__(self): + self.buttons = gamepadshift.GamePadShift( + digitalio.DigitalInOut(board.BUTTON_CLOCK), + digitalio.DigitalInOut(board.BUTTON_OUT), + digitalio.DigitalInOut(board.BUTTON_LATCH), + ) + self.joy_x = analogio.AnalogIn(board.JOYSTICK_X) + self.joy_y = analogio.AnalogIn(board.JOYSTICK_Y) + + def get_pressed(self): + pressed = self.buttons.get_pressed() + dead = 500 + center = 32767 + x = self.joy_x.value + if x < center - dead: + pressed |= K_RIGHT + elif x > center + dead: + pressed |= K_LEFT + y = self.joy_y.value + if y < center - dead: + pressed |= K_UP + elif y > center + dead: + pressed |= K_DOWN + return pressed + + +buttons = Buttons() +audio = stage.Audio(board.SPEAKER, board.SPEAKER_ENABLE) From 88a616398078be4ddc770d3609bdb264287fcc10 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Mon, 17 Jun 2019 20:10:53 +0200 Subject: [PATCH 09/59] Fix pygamer for analogio --- pygamer/ugame.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pygamer/ugame.py b/pygamer/ugame.py index 7fdde13..0f59c55 100644 --- a/pygamer/ugame.py +++ b/pygamer/ugame.py @@ -5,6 +5,7 @@ import board import digitalio +import analogio import gamepadshift import stage import displayio From 0dce331e849a9459dbd1f99b254f6739e35a4ca1 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Mon, 17 Jun 2019 20:35:37 +0200 Subject: [PATCH 10/59] Fine-tuning the pygamer joystick --- pygamer/ugame.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/pygamer/ugame.py b/pygamer/ugame.py index 0f59c55..0482226 100644 --- a/pygamer/ugame.py +++ b/pygamer/ugame.py @@ -72,22 +72,21 @@ def __init__(self): digitalio.DigitalInOut(board.BUTTON_OUT), digitalio.DigitalInOut(board.BUTTON_LATCH), ) - self.joy_x = analogio.AnalogIn(board.JOYSTICK_X) - self.joy_y = analogio.AnalogIn(board.JOYSTICK_Y) + self.joy_x = analogio.AnalogIn(board.JOYSTICK_Y) + self.joy_y = analogio.AnalogIn(board.JOYSTICK_X) def get_pressed(self): pressed = self.buttons.get_pressed() - dead = 500 - center = 32767 - x = self.joy_x.value - if x < center - dead: - pressed |= K_RIGHT - elif x > center + dead: + dead = 15000 + x = self.joy_x.value - 32767 + if x < -dead: pressed |= K_LEFT - y = self.joy_y.value - if y < center - dead: + elif x > dead: + pressed |= K_RIGHT + y = self.joy_y.value - 32767 + if y < -dead: pressed |= K_UP - elif y > center + dead: + elif y > dead: pressed |= K_DOWN return pressed From 6d1ae72916cf240ea86185c45f844d59f56d8ec3 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Wed, 19 Jun 2019 21:28:11 +0200 Subject: [PATCH 11/59] Reverse joystick x and y in PyGamer --- pygamer/ugame.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygamer/ugame.py b/pygamer/ugame.py index 0482226..336a1eb 100644 --- a/pygamer/ugame.py +++ b/pygamer/ugame.py @@ -72,8 +72,8 @@ def __init__(self): digitalio.DigitalInOut(board.BUTTON_OUT), digitalio.DigitalInOut(board.BUTTON_LATCH), ) - self.joy_x = analogio.AnalogIn(board.JOYSTICK_Y) - self.joy_y = analogio.AnalogIn(board.JOYSTICK_X) + self.joy_x = analogio.AnalogIn(board.JOYSTICK_X) + self.joy_y = analogio.AnalogIn(board.JOYSTICK_Y) def get_pressed(self): pressed = self.buttons.get_pressed() From 0b8e6b0f03247d00f888246b7ef4c6b206fe8a33 Mon Sep 17 00:00:00 2001 From: hexthat Date: Mon, 29 Jul 2019 14:06:01 -0700 Subject: [PATCH 12/59] Support new audiocore+audioio modules In CircuitPython 4.1.x the audiio module got split into audioio and audiocore modules, so now we need to import both to support sound. --- stage.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stage.py b/stage.py index e1d0ff2..170118a 100644 --- a/stage.py +++ b/stage.py @@ -1,6 +1,7 @@ import time import array import digitalio +import audiocore import audioio import _stage @@ -163,7 +164,7 @@ def play(self, audio_file, loop=False): played in the background. """ self.stop() - wave = audioio.WaveFile(audio_file) + wave = audiocore.WaveFile(audio_file) self.audio.play(wave, loop=loop) def stop(self): From 0d2d660e886de8a1b96778c865c7fa48df5f4ea6 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Tue, 30 Jul 2019 23:32:13 +0200 Subject: [PATCH 13/59] Make audiocore import work on older CircuitPython If there is no audiocore, use audioio instead. --- stage.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/stage.py b/stage.py index 170118a..442d45d 100644 --- a/stage.py +++ b/stage.py @@ -1,8 +1,12 @@ import time import array import digitalio -import audiocore import audioio +try: + import audiocore +except ImportError: + audiocore = audioio + import _stage From 6ca2d9228e2e7e6fc116a915c0cf3b3aea6998f3 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Fri, 2 Aug 2019 14:11:30 +0200 Subject: [PATCH 14/59] Add support for GIF files Also fix a bug in the WallGrid, allow any size of the grid. --- stage.py | 172 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 161 insertions(+), 11 deletions(-) diff --git a/stage.py b/stage.py index 442d45d..f22bad1 100644 --- a/stage.py +++ b/stage.py @@ -2,6 +2,7 @@ import array import digitalio import audioio +import struct try: import audiocore except ImportError: @@ -214,14 +215,14 @@ def read_palette(self): palette[color] = ((c << 8) | (c >> 8)) & 0xffff return palette - def read_data(self, offset=0, buffer=None): + def read_data(self, buffer=None): """Read the image data.""" line_size = self.width >> 1 if buffer is None: buffer = bytearray(line_size * self.height) with open(self.filename, 'rb') as f: - f.seek(self.data + offset) + f.seek(self.data) index = (self.height - 1) * line_size for line in range(self.height): chunk = f.read(line_size) @@ -230,6 +231,144 @@ def read_data(self, offset=0, buffer=None): return buffer +def read_blockstream(f): + while True: + size = f.read(1)[0] + if size == 0: + break + for i in range(size): + yield f.read(1)[0] + + +class EndOfData(Exception): + pass + + +class LZWDict: + def __init__(self, code_size): + self.code_size = code_size + self.clear_code = 1 << code_size + self.end_code = self.clear_code + 1 + self.codes = [] + self.clear() + + def clear(self): + self.last = b'' + self.code_len = self.code_size + 1 + self.codes[:] = [] + + def decode(self, code): + if code == self.clear_code: + self.clear() + return b'' + elif code == self.end_code: + raise EndOfData() + elif code < self.clear_code: + value = bytes([code]) + elif code <= len(self.codes) + self.end_code: + value = self.codes[code - self.end_code - 1] + else: + value = self.last + self.last[0:1] + if self.last: + self.codes.append(self.last + value[0:1]) + if (len(self.codes) + self.end_code + 1 >= 1 << self.code_len and + self.code_len < 12): + self.code_len += 1 + self.last = value + return value + + +def lzw_decode(data, code_size): + dictionary = LZWDict(code_size) + bit = 0 + try: + byte = next(data) + try: + while True: + code = 0 + for i in range(dictionary.code_len): + code |= ((byte >> bit) & 0x01) << i + bit += 1 + if bit >= 8: + bit = 0 + byte = next(data) + yield dictionary.decode(code) + except EndOfData: + while True: + next(data) + except StopIteration: + return + finally: + del dictionary + + +class GIF16: + def __init__(self, filename): + self.filename = filename + + def read_header(self): + with open(self.filename, 'rb') as f: + header = f.read(6) + if header not in {b'GIF87a', b'GIF89a'}: + raise ValueError("Not GIF file") + self.width, self.height, flags, self.background, self.aspect = ( + struct.unpack(' 16: + raise ValueError("16-color GIF expected") + + def read_palette(self): + palette = array.array('H', (0 for i in range(16))) + with open(self.filename, 'rb') as f: + f.seek(13) + for color in range(self.palette_size): + buffer = f.read(3) + c = color565(buffer[2], buffer[1], buffer[0]) + palette[color] = ((c << 8) | (c >> 8)) & 0xffff + return palette + + def read_data(self, buffer=None): + line_size = (self.width + 1) >> 1 + if buffer is None: + buffer = bytearray(line_size * self.height) + with open(self.filename, 'rb') as f: + f.seek(13 + self.palette_size * 3) + while True: # skip to first frame + block_type = f.read(1)[0] + if block_type == 0x2c: + break + elif block_type == 0x21: # skip extension + extension_type = f.read(1)[0] + while True: + size = f.read(1)[0] + if size == 0: + break + f.seek(1, size) + elif block_type == 0x3b: + raise ValueError("no frames") + x, y, w, h, flags = struct.unpack('> 1) + y * line_size] |= pixel + else: + buffer[(x >> 1) + y * line_size] = pixel << 4 + x += 1 + if (x >= self.width): + x = 0 + y += 1 + return buffer + + class Bank: """ Store graphics for the tiles and sprites. @@ -245,13 +384,24 @@ def __init__(self, buffer=None, palette=None): @classmethod def from_bmp16(cls, filename): - """Read the palette from a file.""" - bmp = BMP16(filename) - bmp.read_header() - if bmp.width != 16 or bmp.height != 256: - raise ValueError("Not 16x256!") - palette = bmp.read_palette() - buffer = bmp.read_data(0) + """Read the bank from a BMP file.""" + return cls.from_image(filename) + + + @classmethod + def from_image(cls, filename): + """Read the bank from an image file.""" + if filename.lower().endswith(".gif"): + image = GIF16(filename) + elif filename.lower().endswith(".bmp"): + image = BMP16(filename) + else: + raise ValueError("Unsupported format") + image.read_header() + if image.width != 16 or image.height != 256: + raise ValueError("Image size not 16x256") + palette = image.read_palette() + buffer = image.read_data() return cls(buffer, palette) @@ -313,8 +463,8 @@ def __init__(self, grid, walls, bank, palette=None): self.move(self.x - 8, self.y - 8) def update(self): - for y in range(9): - for x in range(9): + for y in range(self.height): + for x in range(self.width): t = 0 bit = 1 for dy in (-1, 0): From a9bc140047c90d79c7b4d60b98d6195ac781b9a3 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Sat, 31 Aug 2019 23:52:56 +0200 Subject: [PATCH 15/59] Add support for PewPew M4 --- pewpew_m4/stage.py | 1 + pewpew_m4/ugame.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 120000 pewpew_m4/stage.py create mode 100644 pewpew_m4/ugame.py diff --git a/pewpew_m4/stage.py b/pewpew_m4/stage.py new file mode 120000 index 0000000..2dedc93 --- /dev/null +++ b/pewpew_m4/stage.py @@ -0,0 +1 @@ +../stage.py \ No newline at end of file diff --git a/pewpew_m4/ugame.py b/pewpew_m4/ugame.py new file mode 100644 index 0000000..f4db933 --- /dev/null +++ b/pewpew_m4/ugame.py @@ -0,0 +1,32 @@ +""" +A helper module that initializes the display and buttons for the PewPew M4 +game console. +""" + +import board +import digitalio +import gamepad +import stage + + +K_X = 0x01 +K_DOWN = 0x02 +K_LEFT = 0x04 +K_RIGHT = 0x08 +K_UP = 0x10 +K_O = 0x20 +K_START = 0x40 +K_SELECT = 0x00 + + +display = board.DISPLAY +buttons = gamepad.GamePad( + digitalio.DigitalInOut(board.BUTTON_X), + digitalio.DigitalInOut(board.BUTTON_DOWN), + digitalio.DigitalInOut(board.BUTTON_LEFT), + digitalio.DigitalInOut(board.BUTTON_RIGHT), + digitalio.DigitalInOut(board.BUTTON_UP), + digitalio.DigitalInOut(board.BUTTON_O), + digitalio.DigitalInOut(board.BUTTON_Z), +) +audio = stage.Audio(board.P5) From c1d8e1d645cbc83d857e12cf4ba67549b988a4e7 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Thu, 5 Sep 2019 01:25:04 +0200 Subject: [PATCH 16/59] Optimize out GIF errors --- pewpew_m4/ugame.py | 6 +----- stage.py | 15 +++++---------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/pewpew_m4/ugame.py b/pewpew_m4/ugame.py index f4db933..7037913 100644 --- a/pewpew_m4/ugame.py +++ b/pewpew_m4/ugame.py @@ -1,8 +1,3 @@ -""" -A helper module that initializes the display and buttons for the PewPew M4 -game console. -""" - import board import digitalio import gamepad @@ -16,6 +11,7 @@ K_UP = 0x10 K_O = 0x20 K_START = 0x40 +K_Z = 0x40 K_SELECT = 0x00 diff --git a/stage.py b/stage.py index f22bad1..0eb585e 100644 --- a/stage.py +++ b/stage.py @@ -298,8 +298,6 @@ def lzw_decode(data, code_size): next(data) except StopIteration: return - finally: - del dictionary class GIF16: @@ -315,7 +313,7 @@ def read_header(self): struct.unpack(' 16: - raise ValueError("16-color GIF expected") + raise NotImplementedError() def read_palette(self): palette = array.array('H', (0 for i in range(16))) @@ -345,14 +343,11 @@ def read_data(self, buffer=None): break f.seek(1, size) elif block_type == 0x3b: - raise ValueError("no frames") + raise NotImplementedError() x, y, w, h, flags = struct.unpack(' Date: Mon, 23 Dec 2019 13:23:52 +0100 Subject: [PATCH 17/59] Handle muting properly --- stage.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/stage.py b/stage.py index f22bad1..453a323 100644 --- a/stage.py +++ b/stage.py @@ -155,9 +155,11 @@ class Audio: last_audio = None def __init__(self, speaker_pin, mute_pin=None): + self.muted = True + self.buffer = bytearray(64) if mute_pin: self.mute_pin = digitalio.DigitalInOut(mute_pin) - self.mute_pin.switch_to_output(value=0) + self.mute_pin.switch_to_output(value=not self.muted) else: self.mute_pin = None self.audio = audioio.AudioOut(speaker_pin) @@ -168,8 +170,10 @@ def play(self, audio_file, loop=False): repeat until stopped. This function doesn't block, the sound is played in the background. """ + if self.muted: + return self.stop() - wave = audiocore.WaveFile(audio_file) + wave = audiocore.WaveFile(audio_file, self.buffer) self.audio.play(wave, loop=loop) def stop(self): @@ -178,6 +182,7 @@ def stop(self): def mute(self, value=True): """Enable or disable all sounds.""" + self.muted = value if self.mute_pin: self.mute_pin.value = not value From aa7f47af31c92fd4efb208ec5249fb0ef3d8c7bb Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Mon, 23 Dec 2019 13:24:42 +0100 Subject: [PATCH 18/59] Add PewPew emulation support to PewPew M4 --- pewpew_m4/pew.py | 189 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 pewpew_m4/pew.py diff --git a/pewpew_m4/pew.py b/pewpew_m4/pew.py new file mode 100644 index 0000000..b352e67 --- /dev/null +++ b/pewpew_m4/pew.py @@ -0,0 +1,189 @@ +from micropython import const +import board +import busio +import digitalio +import time +import ugame +import stage +import array + + +_FONT = ( + b'{{{{{{wws{w{HY{{{{YDYDY{sUtGUsH[wyH{uHgHE{ws{{{{vyxyv{g[K[g{{]f]{{{wDw{{' + b'{{{wy{{{D{{{{{{{w{K_w}x{VHLHe{wuwww{`KfyD{UKgKU{w}XDK{DxTKT{VxUHU{D[wyx{' + b'UHfHU{UHEKe{{w{w{{{w{wy{KwxwK{{D{D{{xwKwx{eKg{w{VIHyB{fYH@H{dHdHd{FyxyF{' + b'`XHX`{DxtxD{Dxtxx{FyxIF{HHDHH{wwwww{KKKHU{HXpXH{xxxxD{Y@DLH{IL@LX{fYHYf{' + b'`HH`x{fYHIF{`HH`H{UxUKU{Dwwww{HHHIR{HHH]w{HHLD@{HYsYH{HYbww{D[wyD{txxxt{' + b'x}w_K{GKKKG{wLY{{{{{{{{Dxs{{{{{BIIB{x`XX`{{ByyB{KBIIB{{WIpF{OwUwww{`YB[`' + b'x`XHH{w{vwc{K{OKHUxHpXH{vwws_{{dD@H{{`XHH{{fYYf{{`XX`x{bYIBK{Ipxx{{F}_d{' + b'wUws_{{HHIV{{HH]s{{HLD@{{HbbH{{HHV[a{D_}D{Cw|wC{wwwwwwpwOwp{WKfxu{@YYY@{' +) +_SALT = const(132) +_BANK = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00""""""\x00\x02"""""" \x02\x02\x02\x02\x02\x02\x02\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x02\x02\x02\x02\x02\x02\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00 \x00 \x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02h\xb8\xb8\xb8\xb8\xb6 \x08\x88\x88\x88\x88\x88\x88\x80\x08hhhhhh`\x06\x86\x86\x86\x86\x86\x86\x80\x06ffffff`\x08hhhhhh`\x06ffffff`\x06FFFFFF@\x04dddddd`\x04DDDDDD@\x06FFFFFF@\x04DDDDDD@\x04dDdDdD`\x02DDDDDD \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x029\x99\x99\x99\x99\x93 \t\x99\x99\x99\x99\x99\x99\x90\t9999990\x03\x93\x93\x93\x93\x93\x93\x90\x033333330\t9999990\x033333330\x03\x13\x13\x13\x13\x13\x13\x10\x011111110\x01\x11\x11\x11\x11\x11\x11\x10\x03\x13\x13\x13\x13\x13\x13\x10\x01\x11\x11\x11\x11\x11\x11\x10\x011\x111\x111\x110\x02\x11\x11\x11\x11\x11\x11 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xce\xee\xee\xee\xee\xec \x0e\xee\xee\xee\xee\xee\xee\xe0\x0c\xec\xec\xec\xec\xec\xec\xe0\x0e\xee\xee\xee\xee\xee\xee\xe0\x0e\xce\xce\xce\xce\xce\xce\xc0\x0c\xec\xec\xec\xec\xec\xec\xe0\x0c\xcc\xcc\xcc\xcc\xcc\xcc\xc0\x0e\xce\xce\xce\xce\xce\xce\xc0\x0c\xcc\xcc\xcc\xcc\xcc\xcc\xc0\x0c\xac\xac\xac\xac\xac\xac\xa0\n\xca\xca\xca\xca\xca\xca\xc0\n\xaa\xaa\xaa\xaa\xaa\xaa\xa0\x0c\xac\xac\xac\xac\xac\xac\xa0\x02\xaa\xaa\xaa\xaa\xaa\xaa \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\'wwwwr\x00\x07wwwwwwp\x02rrrrrrp\x07wwwwwwp\x07\'\'\'\'\'\' \x02rrrrrrp\x02"""""" \x07\'\'\'\'\'\' \x02"""""" \x02\x02\x02\x02\x02\x02\x02\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x02\x02\x02\x02\x02\x02\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x8b\xbb\xbb\xbb\xbb\xb8 \x0b\xbb\xbb\xbb\xbb\xbb\xbb\xb0\x08\xb8\xb8\xb8\xb8\xb8\xb8\xb0\x0b\xbb\xbb\xbb\xbb\xbb\xbb\xb0\x0b\x8b\x8b\x8b\x8b\x8b\x8b\x80\x08\xb8\xb8\xb8\xb8\xb8\xb8\xb0\x08\x88\x88\x88\x88\x88\x88\x80\x0b\x8b\x8b\x8b\x8b\x8b\x8b\x80\x08\x88\x88\x88\x88\x88\x88\x80\x06\x86\x86\x86\x86\x86\x86\x80\x08hhhhhh`\x06ffffff`\x06\x86\x86\x86\x86\x86\x86\x80\x02ffffff \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x9a\xaa\xaa\xaa\xaa\xa9 \n\xaa\xaa\xaa\xaa\xaa\xaa\xa0\t\xa9\xa9\xa9\xa9\xa9\xa9\xa0\n\xaa\xaa\xaa\xaa\xaa\xaa\xa0\n\x9a\x9a\x9a\x9a\x9a\x9a\x90\t\xa9\xa9\xa9\xa9\xa9\xa9\xa0\t\x99\x99\x99\x99\x99\x99\x90\n\x9a\x9a\x9a\x9a\x9a\x9a\x90\t\x99\x99\x99\x99\x99\x99\x90\t9999990\x03\x93\x93\x93\x93\x93\x93\x90\x033333330\t9999990\x02333333 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xee\xee\xee\xee\xee\xee \x0e\xee\xee\xee\xee\xee\xee\xe0\x0e\xee\xce\xee\xce\xee\xce\xe0\x0e\xee\xee\xee\xee\xee\xee\xe0\x0c\xec\xec\xec\xec\xec\xec\xe0\x0e\xee\xee\xee\xee\xee\xee\xe0\x0e\xce\xce\xce\xce\xce\xce\xc0\x0c\xec\xec\xec\xec\xec\xec\xe0\x0c\xcc\xcc\xcc\xcc\xcc\xcc\xc0\x0e\xce\xce\xce\xce\xce\xce\xc0\x0c\xcc\xcc\xcc\xcc\xcc\xcc\xc0\x0c\xcc\xec\xcc\xec\xcc\xec\xc0\x0c\xcc\xcc\xcc\xcc\xcc\xcc\xc0\x02\xcc\xcc\xcc\xcc\xcc\xcc \x00\x00\x00\x00\x00\x00\x00\x00\x88f\x88f\x88f\x88f\x88f\x88f\x88f\x88ff\x88f\x88f\x88f\x88\x00\x00\x00\x00\x00\x00\x00\x00\xee\xee\xee\xee\xee\xee\xee\xee\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\x00\x00\x00\x00\x00\x00\x00\x00\'\'\'\'\'\'\'\'wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'\x88f\x88f\x88f\x88f\x88f\x88f\x88f\x88ff\x88f\x88f\x88f\x88f\x88f\x88f\x88d\x00\x88f\x88f\x88f\x02\xde\x88f\x88f\x88d.\xedf\x88f\x88f\x80\xde\xd7f\x88f\x88f\x80\xedp\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`\xder\x88f\x88f\x88`~\xedf\x88f\x88f\x84-\xdef\x88f\x88f\x86\x02}\x88f\x88f\x88fd\x00\x88f\x88f\x88f\x86Df\x88f\x88f\x88fff\x88f\x88f\x88f\x88wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'wwwwwwww\x00\x00\x00\x00\x00\x00\x00\x00\xee\xee\xee\xee\xee\xee\xee\xee\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\x00\x00\x00\x00\x00\x00\x00\x00\x88f\x88f\x88f\x88ff\x88f\x88f\x88f\x88f\x88f\x88f\x88f\x88wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'UUUUUUUUUUUUUUUUUU \x02UUUU\x02R\xde\xee \x00\x00%\r\x0e\xee\xde\xe7\xee\xee\xd2\x0e~\xee}\xe7\xdd\xdd\xd2\x0e\xdd\xee}\xe7p\x00%\x0e\xdd\xdd}\xd7\xe0UU\x0e\xddw\xe7~\xd0UU\x0e\xde\xdd~\xe7pUU\x0e~\xd7\xe7\xde\xd2UU\r\r\xd7\xdew\x05UU\x02R\xdd}\xed%UUUU \x00\x02UUUUUUUUUUUUUUUUUUU' +_PALETTE = array.array('H', (0, 5632, 10081, 7936, 17410, 8184, 10243, 53226, + 2820, 32586, 48964, 62213, 40710, 47830, 57311, 65535)) + + +K_X = 0x01 +K_DOWN = 0x02 +K_LEFT = 0x04 +K_RIGHT = 0x08 +K_UP = 0x10 +K_O = 0x20 + +_tick = None +_display = None + + +def brightness(level): + pass + + +def show(pix): + for y in range(8): + for x in range(8): + _grid.tile(x + 1, y, 1 + (pix.pixel(x, y) & 0x03)) + _game.render_block(16, 0, 144, 128) + +keys = ugame.buttons.get_pressed + + +def tick(delay): + global _tick + + _tick += delay + time.sleep(max(0, _tick - time.monotonic())) + + +class GameOver(Exception): + pass + + +class Pix: + __slots__ = ('buffer', 'width', 'height') + + def __init__(self, width=8, height=8, buffer=None): + if buffer is None: + buffer = bytearray(width * height) + self.buffer = buffer + self.width = width + self.height = height + + @classmethod + def from_text(cls, string, color=None, bgcolor=0, colors=None): + pix = cls(4 * len(string), 6) + font = memoryview(_FONT) + if colors is None: + if color is None: + colors = (3, 2, bgcolor, bgcolor) + else: + colors = (color, color, bgcolor, bgcolor) + x = 0 + for c in string: + index = ord(c) - 0x20 + if not 0 <= index <= 95: + continue + row = 0 + for byte in font[index * 6:index * 6 + 6]: + unsalted = byte ^ _SALT + for col in range(4): + pix.pixel(x + col, row, colors[unsalted & 0x03]) + unsalted >>= 2 + row += 1 + x += 4 + return pix + + @classmethod + def from_iter(cls, lines): + pix = cls(len(lines[0]), len(lines)) + y = 0 + for line in lines: + x = 0 + for pixel in line: + pix.pixel(x, y, pixel) + x += 1 + y += 1 + return pix + + def pixel(self, x, y, color=None): + if not 0 <= x < self.width or not 0 <= y < self.height: + return 0 + if color is None: + return self.buffer[x + y * self.width] + self.buffer[x + y * self.width] = color + + def box(self, color, x=0, y=0, width=None, height=None): + x = min(max(x, 0), self.width - 1) + y = min(max(y, 0), self.height - 1) + width = max(0, min(width or self.width, self.width - x)) + height = max(0, min(height or self.height, self.height - y)) + for y in range(y, y + height): + xx = y * self.width + x + for i in range(width): + self.buffer[xx] = color + xx += 1 + + def blit(self, source, dx=0, dy=0, x=0, y=0, + width=None, height=None, key=None): + if dx < 0: + x -= dx + dx = 0 + if x < 0: + dx -= x + x = 0 + if dy < 0: + y -= dy + dy = 0 + if y < 0: + dy -= y + y = 0 + width = min(min(width or source.width, source.width - x), + self.width - dx) + height = min(min(height or source.height, source.height - y), + self.height - dy) + source_buffer = memoryview(source.buffer) + self_buffer = self.buffer + if key is None: + for row in range(height): + xx = y * source.width + x + dxx = dy * self.width + dx + self_buffer[dxx:dxx + width] = source_buffer[xx:xx + width] + y += 1 + dy += 1 + else: + for row in range(height): + xx = y * source.width + x + dxx = dy * self.width + dx + for col in range(width): + color = source_buffer[xx] + if color != key: + self_buffer[dxx] = color + dxx += 1 + xx += 1 + y += 1 + dy += 1 + + def __str__(self): + return "\n".join( + "".join( + ('.', '+', '*', '@')[self.pixel(x, y)] + for x in range(self.width) + ) + for y in range(self.height) + ) + + +def init(): + global _tick, _display, _gamepad, _bitmap, _grid, _game + + if _tick is not None: + return + + _tick = time.monotonic() + + _game = stage.Stage(ugame.display, 12) + tiles = stage.Bank(_BANK, _PALETTE) + _grid = stage.Grid(tiles, 10, 8) + _grid.move(0, 0) + _game.layers = [_grid] + _game.render_block() From 89850618dc7b30ee9e2dc11c843dfe4e13d90517 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Mon, 23 Dec 2019 15:36:42 +0100 Subject: [PATCH 19/59] Clean up pew.py --- pewpew_m4/pew.py | 86 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/pewpew_m4/pew.py b/pewpew_m4/pew.py index b352e67..05d4263 100644 --- a/pewpew_m4/pew.py +++ b/pewpew_m4/pew.py @@ -19,7 +19,91 @@ b'wUws_{{HHIV{{HH]s{{HLD@{{HbbH{{HHV[a{D_}D{Cw|wC{wwwwwwpwOwp{WKfxu{@YYY@{' ) _SALT = const(132) -_BANK = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00""""""\x00\x02"""""" \x02\x02\x02\x02\x02\x02\x02\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x02\x02\x02\x02\x02\x02\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00 \x00 \x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02h\xb8\xb8\xb8\xb8\xb6 \x08\x88\x88\x88\x88\x88\x88\x80\x08hhhhhh`\x06\x86\x86\x86\x86\x86\x86\x80\x06ffffff`\x08hhhhhh`\x06ffffff`\x06FFFFFF@\x04dddddd`\x04DDDDDD@\x06FFFFFF@\x04DDDDDD@\x04dDdDdD`\x02DDDDDD \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x029\x99\x99\x99\x99\x93 \t\x99\x99\x99\x99\x99\x99\x90\t9999990\x03\x93\x93\x93\x93\x93\x93\x90\x033333330\t9999990\x033333330\x03\x13\x13\x13\x13\x13\x13\x10\x011111110\x01\x11\x11\x11\x11\x11\x11\x10\x03\x13\x13\x13\x13\x13\x13\x10\x01\x11\x11\x11\x11\x11\x11\x10\x011\x111\x111\x110\x02\x11\x11\x11\x11\x11\x11 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xce\xee\xee\xee\xee\xec \x0e\xee\xee\xee\xee\xee\xee\xe0\x0c\xec\xec\xec\xec\xec\xec\xe0\x0e\xee\xee\xee\xee\xee\xee\xe0\x0e\xce\xce\xce\xce\xce\xce\xc0\x0c\xec\xec\xec\xec\xec\xec\xe0\x0c\xcc\xcc\xcc\xcc\xcc\xcc\xc0\x0e\xce\xce\xce\xce\xce\xce\xc0\x0c\xcc\xcc\xcc\xcc\xcc\xcc\xc0\x0c\xac\xac\xac\xac\xac\xac\xa0\n\xca\xca\xca\xca\xca\xca\xc0\n\xaa\xaa\xaa\xaa\xaa\xaa\xa0\x0c\xac\xac\xac\xac\xac\xac\xa0\x02\xaa\xaa\xaa\xaa\xaa\xaa \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\'wwwwr\x00\x07wwwwwwp\x02rrrrrrp\x07wwwwwwp\x07\'\'\'\'\'\' \x02rrrrrrp\x02"""""" \x07\'\'\'\'\'\' \x02"""""" \x02\x02\x02\x02\x02\x02\x02\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x02\x02\x02\x02\x02\x02\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x8b\xbb\xbb\xbb\xbb\xb8 \x0b\xbb\xbb\xbb\xbb\xbb\xbb\xb0\x08\xb8\xb8\xb8\xb8\xb8\xb8\xb0\x0b\xbb\xbb\xbb\xbb\xbb\xbb\xb0\x0b\x8b\x8b\x8b\x8b\x8b\x8b\x80\x08\xb8\xb8\xb8\xb8\xb8\xb8\xb0\x08\x88\x88\x88\x88\x88\x88\x80\x0b\x8b\x8b\x8b\x8b\x8b\x8b\x80\x08\x88\x88\x88\x88\x88\x88\x80\x06\x86\x86\x86\x86\x86\x86\x80\x08hhhhhh`\x06ffffff`\x06\x86\x86\x86\x86\x86\x86\x80\x02ffffff \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x9a\xaa\xaa\xaa\xaa\xa9 \n\xaa\xaa\xaa\xaa\xaa\xaa\xa0\t\xa9\xa9\xa9\xa9\xa9\xa9\xa0\n\xaa\xaa\xaa\xaa\xaa\xaa\xa0\n\x9a\x9a\x9a\x9a\x9a\x9a\x90\t\xa9\xa9\xa9\xa9\xa9\xa9\xa0\t\x99\x99\x99\x99\x99\x99\x90\n\x9a\x9a\x9a\x9a\x9a\x9a\x90\t\x99\x99\x99\x99\x99\x99\x90\t9999990\x03\x93\x93\x93\x93\x93\x93\x90\x033333330\t9999990\x02333333 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xee\xee\xee\xee\xee\xee \x0e\xee\xee\xee\xee\xee\xee\xe0\x0e\xee\xce\xee\xce\xee\xce\xe0\x0e\xee\xee\xee\xee\xee\xee\xe0\x0c\xec\xec\xec\xec\xec\xec\xe0\x0e\xee\xee\xee\xee\xee\xee\xe0\x0e\xce\xce\xce\xce\xce\xce\xc0\x0c\xec\xec\xec\xec\xec\xec\xe0\x0c\xcc\xcc\xcc\xcc\xcc\xcc\xc0\x0e\xce\xce\xce\xce\xce\xce\xc0\x0c\xcc\xcc\xcc\xcc\xcc\xcc\xc0\x0c\xcc\xec\xcc\xec\xcc\xec\xc0\x0c\xcc\xcc\xcc\xcc\xcc\xcc\xc0\x02\xcc\xcc\xcc\xcc\xcc\xcc \x00\x00\x00\x00\x00\x00\x00\x00\x88f\x88f\x88f\x88f\x88f\x88f\x88f\x88ff\x88f\x88f\x88f\x88\x00\x00\x00\x00\x00\x00\x00\x00\xee\xee\xee\xee\xee\xee\xee\xee\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\x00\x00\x00\x00\x00\x00\x00\x00\'\'\'\'\'\'\'\'wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'\x88f\x88f\x88f\x88f\x88f\x88f\x88f\x88ff\x88f\x88f\x88f\x88f\x88f\x88f\x88d\x00\x88f\x88f\x88f\x02\xde\x88f\x88f\x88d.\xedf\x88f\x88f\x80\xde\xd7f\x88f\x88f\x80\xedp\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`\xder\x88f\x88f\x88`~\xedf\x88f\x88f\x84-\xdef\x88f\x88f\x86\x02}\x88f\x88f\x88fd\x00\x88f\x88f\x88f\x86Df\x88f\x88f\x88fff\x88f\x88f\x88f\x88wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'wwwwwwww\x00\x00\x00\x00\x00\x00\x00\x00\xee\xee\xee\xee\xee\xee\xee\xee\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\x00\x00\x00\x00\x00\x00\x00\x00\x88f\x88f\x88f\x88ff\x88f\x88f\x88f\x88f\x88f\x88f\x88f\x88wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'UUUUUUUUUUUUUUUUUU \x02UUUU\x02R\xde\xee \x00\x00%\r\x0e\xee\xde\xe7\xee\xee\xd2\x0e~\xee}\xe7\xdd\xdd\xd2\x0e\xdd\xee}\xe7p\x00%\x0e\xdd\xdd}\xd7\xe0UU\x0e\xddw\xe7~\xd0UU\x0e\xde\xdd~\xe7pUU\x0e~\xd7\xe7\xde\xd2UU\r\r\xd7\xdew\x05UU\x02R\xdd}\xed%UUUU \x00\x02UUUUUUUUUUUUUUUUUUU' +_BANK = ( + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00""""""\x00\x02"""""" \x02' + b'\x02\x02\x02\x02\x02\x02\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00' + b'\x02\x02\x02\x02\x02\x02\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00 \x00 \x00 \x00 \x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00 \x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02h\xb8\xb8\xb8\xb8' + b'\xb6 \x08\x88\x88\x88\x88\x88\x88\x80\x08hhhhhh`\x06\x86\x86\x86\x86' + b'\x86\x86\x80\x06ffffff`\x08hhhhhh`\x06ffffff`\x06FFFFFF@\x04dddddd`\x04D' + b'DDDDD@\x06FFFFFF@\x04DDDDDD@\x04dDdDdD`\x02DDDDDD \x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x029\x99\x99\x99\x99\x93 ' + b'\t\x99\x99\x99\x99\x99\x99\x90\t9999990\x03\x93\x93\x93\x93\x93\x93\x90' + b'\x033333330\t9999990\x033333330\x03\x13\x13\x13\x13\x13\x13\x10\x0111111' + b'10\x01\x11\x11\x11\x11\x11\x11\x10\x03\x13\x13\x13\x13\x13\x13\x10\x01' + b'\x11\x11\x11\x11\x11\x11\x10\x011\x111\x111\x110\x02\x11\x11\x11\x11\x11' + b'\x11 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x02\xce\xee\xee\xee\xee\xec \x0e\xee\xee\xee\xee\xee\xee\xe0\x0c\xec' + b'\xec\xec\xec\xec\xec\xe0\x0e\xee\xee\xee\xee\xee\xee\xe0\x0e\xce\xce\xce' + b'\xce\xce\xce\xc0\x0c\xec\xec\xec\xec\xec\xec\xe0\x0c\xcc\xcc\xcc\xcc\xcc' + b'\xcc\xc0\x0e\xce\xce\xce\xce\xce\xce\xc0\x0c\xcc\xcc\xcc\xcc\xcc\xcc\xc0' + b'\x0c\xac\xac\xac\xac\xac\xac\xa0\n\xca\xca\xca\xca\xca\xca\xc0\n\xaa\xaa' + b'\xaa\xaa\xaa\xaa\xa0\x0c\xac\xac\xac\xac\xac\xac\xa0\x02\xaa\xaa\xaa\xaa' + b'\xaa\xaa \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\'wwwwr\x00\x07wwwwwwp\x02rrrrrrp\x07wwwwwwp\x07\'\'\'\'\'\' ' + b'\x02rrrrrrp\x02"""""" \x07\'\'\'\'\'\' \x02"""""" \x02\x02\x02\x02\x02' + b'\x02\x02\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x02\x02\x02\x02' + b'\x02\x02\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x8b\xbb\xbb\xbb\xbb\xb8 ' + b'\x0b\xbb\xbb\xbb\xbb\xbb\xbb\xb0\x08\xb8\xb8\xb8\xb8\xb8\xb8\xb0\x0b' + b'\xbb\xbb\xbb\xbb\xbb\xbb\xb0\x0b\x8b\x8b\x8b\x8b\x8b\x8b\x80\x08\xb8' + b'\xb8\xb8\xb8\xb8\xb8\xb0\x08\x88\x88\x88\x88\x88\x88\x80\x0b\x8b\x8b' + b'\x8b\x8b\x8b\x8b\x80\x08\x88\x88\x88\x88\x88\x88\x80\x06\x86\x86\x86' + b'\x86\x86\x86\x80\x08hhhhhh`\x06ffffff`\x06\x86\x86\x86\x86\x86\x86\x80' + b'\x02ffffff \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x02\x9a\xaa\xaa\xaa\xaa\xa9 \n\xaa\xaa\xaa\xaa\xaa\xaa\xa0\t\xa9' + b'\xa9\xa9\xa9\xa9\xa9\xa0\n\xaa\xaa\xaa\xaa\xaa\xaa\xa0\n\x9a\x9a\x9a\x9a' + b'\x9a\x9a\x90\t\xa9\xa9\xa9\xa9\xa9\xa9\xa0\t\x99\x99\x99\x99\x99\x99\x90' + b'\n\x9a\x9a\x9a\x9a\x9a\x9a\x90\t\x99\x99\x99\x99\x99\x99\x90\t9999990' + b'\x03\x93\x93\x93\x93\x93\x93\x90\x033333330\t9999990\x02333333 \x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xee\xee\xee' + b'\xee\xee\xee \x0e\xee\xee\xee\xee\xee\xee\xe0\x0e\xee\xce\xee\xce\xee' + b'\xce\xe0\x0e\xee\xee\xee\xee\xee\xee\xe0\x0c\xec\xec\xec\xec\xec\xec\xe0' + b'\x0e\xee\xee\xee\xee\xee\xee\xe0\x0e\xce\xce\xce\xce\xce\xce\xc0\x0c\xec' + b'\xec\xec\xec\xec\xec\xe0\x0c\xcc\xcc\xcc\xcc\xcc\xcc\xc0\x0e\xce\xce\xce' + b'\xce\xce\xce\xc0\x0c\xcc\xcc\xcc\xcc\xcc\xcc\xc0\x0c\xcc\xec\xcc\xec\xcc' + b'\xec\xc0\x0c\xcc\xcc\xcc\xcc\xcc\xcc\xc0\x02\xcc\xcc\xcc\xcc\xcc\xcc ' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x88f\x88f\x88f\x88f\x88f\x88f\x88f\x88' + b'ff\x88f\x88f\x88f\x88\x00\x00\x00\x00\x00\x00\x00\x00\xee\xee\xee\xee' + b'\xee\xee\xee\xee\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\x00\x00\x00\x00\x00\x00' + b'\x00\x00\'\'\'\'\'\'\'\'wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'wwwwwwww' + b'rrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'\x88f\x88f\x88f\x88f\x88f\x88f\x88f\x88f' + b'f\x88f\x88f\x88f\x88f\x88f\x88f\x88d\x00\x88f\x88f\x88f\x02\xde\x88f\x88' + b'f\x88d.\xedf\x88f\x88f\x80\xde\xd7f\x88f\x88f\x80\xedp\x88f\x88f\x88`' + b'\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80' + b'\xed\x07\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80' + b'\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`' + b'\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`' + b'\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80' + b'\xed\x07\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80' + b'\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`' + b'\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`' + b'\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80\xed\x07f\x88f\x88f\x80' + b'\xed\x07\x88f\x88f\x88`\xed\x07\x88f\x88f\x88`\xed\x02f\x88f\x88f\x80' + b'\xed\x07f\x88f\x88f\x80\xed\x07\x88f\x88f\x88`\xder\x88f\x88f\x88`~\xedf' + b'\x88f\x88f\x84-\xdef\x88f\x88f\x86\x02}\x88f\x88f\x88fd\x00\x88f\x88f' + b'\x88f\x86Df\x88f\x88f\x88fff\x88f\x88f\x88f\x88wwwwwwwwrrrrrrrrwwwwwwww' + b'\'\'\'\'\'\'\'\'wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'wwwwwwww\x00\x00' + b'\x00\x00\x00\x00\x00\x00\xee\xee\xee\xee\xee\xee\xee\xee\xdd\xdd\xdd\xdd' + b'\xdd\xdd\xdd\xdd\x00\x00\x00\x00\x00\x00\x00\x00\x88f\x88f\x88f\x88ff' + b'\x88f\x88f\x88f\x88f\x88f\x88f\x88f\x88wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'' + b'\'\'\'\'wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'wwwwwwwwrrrrrrrrwwwwwwww' + b'\'\'\'\'\'\'\'\'wwwwwwwwrrrrrrrrwwwwwwww\'\'\'\'\'\'\'\'UUUUUUUUUUUUUUUU' + b'UU \x02UUUU\x02R\xde\xee \x00\x00%\r\x0e\xee\xde\xe7\xee\xee\xd2\x0e~' + b'\xee}\xe7\xdd\xdd\xd2\x0e\xdd\xee}\xe7p\x00%\x0e\xdd\xdd}\xd7\xe0UU\x0e' + b'\xddw\xe7~\xd0UU\x0e\xde\xdd~\xe7pUU\x0e~\xd7\xe7\xde\xd2UU\r\r\xd7\xdew' + b'\x05UU\x02R\xdd}\xed%UUUU \x00\x02UUUUUUUUUUUUUUUUUUU') + _PALETTE = array.array('H', (0, 5632, 10081, 7936, 17410, 8184, 10243, 53226, 2820, 32586, 48964, 62213, 40710, 47830, 57311, 65535)) From 001228558c9e395aa6d61bf4067b918038e7dcef Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Thu, 26 Dec 2019 21:50:57 +0100 Subject: [PATCH 20/59] Fix tick() in pew.py --- pewpew_m4/pew.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pewpew_m4/pew.py b/pewpew_m4/pew.py index 05d4263..2a456ed 100644 --- a/pewpew_m4/pew.py +++ b/pewpew_m4/pew.py @@ -135,8 +135,12 @@ def show(pix): def tick(delay): global _tick + now = time.monotonic() _tick += delay - time.sleep(max(0, _tick - time.monotonic())) + if _tick < now: + _tick = now + else: + time.sleep(_tick - now) class GameOver(Exception): From 8d5cc384058b1cb296aaeab86fb8405042d547ed Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Thu, 26 Dec 2019 21:51:14 +0100 Subject: [PATCH 21/59] Reset PewPew M4 when Z button is held for 5s --- pewpew_m4/ugame.py | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/pewpew_m4/ugame.py b/pewpew_m4/ugame.py index 7037913..762afb6 100644 --- a/pewpew_m4/ugame.py +++ b/pewpew_m4/ugame.py @@ -2,6 +2,7 @@ import digitalio import gamepad import stage +import time K_X = 0x01 @@ -15,14 +16,34 @@ K_SELECT = 0x00 +class _Buttons: + def __init__(self): + self.buttons = gamepad.GamePad( + digitalio.DigitalInOut(board.BUTTON_X), + digitalio.DigitalInOut(board.BUTTON_DOWN), + digitalio.DigitalInOut(board.BUTTON_LEFT), + digitalio.DigitalInOut(board.BUTTON_RIGHT), + digitalio.DigitalInOut(board.BUTTON_UP), + digitalio.DigitalInOut(board.BUTTON_O), + digitalio.DigitalInOut(board.BUTTON_Z), + ) + self.last_z_press = None + + def get_pressed(self): + buttons = self.buttons.get_pressed() + if buttons & K_Z: + now = time.monotonic() + if self.last_z_press: + if now - self.last_z_press > 5: + import microcontroller + microcontroller.reset() + else: + self.last_z_press = now + else: + self.last_z_press = None + return buttons + + display = board.DISPLAY -buttons = gamepad.GamePad( - digitalio.DigitalInOut(board.BUTTON_X), - digitalio.DigitalInOut(board.BUTTON_DOWN), - digitalio.DigitalInOut(board.BUTTON_LEFT), - digitalio.DigitalInOut(board.BUTTON_RIGHT), - digitalio.DigitalInOut(board.BUTTON_UP), - digitalio.DigitalInOut(board.BUTTON_O), - digitalio.DigitalInOut(board.BUTTON_Z), -) +buttons = _Buttons() audio = stage.Audio(board.P5) From b05e1951cb5089e6485bbfaba4d8fcfe0da23104 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Tue, 31 Dec 2019 00:54:58 +0100 Subject: [PATCH 22/59] Improve the error when gif file has too many colors --- stage.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stage.py b/stage.py index 673f51b..0492c76 100644 --- a/stage.py +++ b/stage.py @@ -317,8 +317,10 @@ def read_header(self): self.width, self.height, flags, self.background, self.aspect = ( struct.unpack(' 16: + if not flags & 0x80: raise NotImplementedError() + if self.palette_size > 16: + raise ValueError("Too many colors (%d/16)." % self.palette_size) def read_palette(self): palette = array.array('H', (0 for i in range(16))) From 19a66d79f0650a15e502464b42e16692365eab36 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Thu, 30 Jan 2020 14:48:11 +0100 Subject: [PATCH 23/59] PewPew M4: improve reload on holding Z button --- pewpew_m4/ugame.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pewpew_m4/ugame.py b/pewpew_m4/ugame.py index 762afb6..fbd4d02 100644 --- a/pewpew_m4/ugame.py +++ b/pewpew_m4/ugame.py @@ -2,6 +2,7 @@ import digitalio import gamepad import stage +import supervisor import time @@ -34,9 +35,8 @@ def get_pressed(self): if buttons & K_Z: now = time.monotonic() if self.last_z_press: - if now - self.last_z_press > 5: - import microcontroller - microcontroller.reset() + if now - self.last_z_press > 2: + supervisor.reload() else: self.last_z_press = now else: From e2a318085bad99d5eb83e3b04fb15474456b17e4 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Wed, 15 Apr 2020 23:14:03 +0200 Subject: [PATCH 24/59] Fix BGR/RGB colors for compatibility with DisplayIO --- feather_m4_minitft_featherwing/ugame.py | 2 +- itsybitsy_m4_express/ugame.py | 2 +- pewpew_m4/pew.py | 4 ++-- pybadge/ugame.py | 2 +- pygamer/ugame.py | 2 +- stage.py | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/feather_m4_minitft_featherwing/ugame.py b/feather_m4_minitft_featherwing/ugame.py index 45c0d4a..728e014 100644 --- a/feather_m4_minitft_featherwing/ugame.py +++ b/feather_m4_minitft_featherwing/ugame.py @@ -29,7 +29,7 @@ b"\xc4\x02\x8a\xee" b"\xc5\x01\x0e" # _VMCTR1 VCOMH = 4V, VOML = -1.1V b"\x20\x00" # _INVOFF - b"\x36\x01\x68" # _MADCTL bottom to top refresh + b"\x36\x01\x60" # _MADCTL bottom to top refresh # 1 clk cycle nonoverlap, 2 cycle gate rise, 3 sycle osc equalie, # fix on VTL b"\x3a\x01\x05" # COLMOD - 16bit color diff --git a/itsybitsy_m4_express/ugame.py b/itsybitsy_m4_express/ugame.py index 3193841..20d4d05 100644 --- a/itsybitsy_m4_express/ugame.py +++ b/itsybitsy_m4_express/ugame.py @@ -35,7 +35,7 @@ b"\xc4\x02\x8a\xee" b"\xc5\x01\x0e" # _VMCTR1 VCOMH = 4V, VOML = -1.1V b"\x20\x00" # _INVOFF - b"\x36\x01\x18" # _MADCTL bottom to top refresh + b"\x36\x01\x10" # _MADCTL bottom to top refresh # 1 clk cycle nonoverlap, 2 cycle gate rise, 3 sycle osc equalie, # fix on VTL b"\x3a\x01\x05" # COLMOD - 16bit color diff --git a/pewpew_m4/pew.py b/pewpew_m4/pew.py index 2a456ed..ba32a6b 100644 --- a/pewpew_m4/pew.py +++ b/pewpew_m4/pew.py @@ -104,8 +104,8 @@ b'\xddw\xe7~\xd0UU\x0e\xde\xdd~\xe7pUU\x0e~\xd7\xe7\xde\xd2UU\r\r\xd7\xdew' b'\x05UU\x02R\xdd}\xed%UUUU \x00\x02UUUUUUUUUUUUUUUUUUU') -_PALETTE = array.array('H', (0, 5632, 10081, 7936, 17410, 8184, 10243, 53226, - 2820, 32586, 48964, 62213, 40710, 47830, 57311, 65535)) +_PALETTE = array.array('H', [0, 176, 11321, 248, 16418, 8184, 8259, 65402, 92, + 27130, 43260, 57501, 33022, 47830, 56319, 8184]) K_X = 0x01 diff --git a/pybadge/ugame.py b/pybadge/ugame.py index 8306f45..fef524f 100644 --- a/pybadge/ugame.py +++ b/pybadge/ugame.py @@ -37,7 +37,7 @@ b"\xc4\x02\x8a\xee" b"\xc5\x01\x0e" # _VMCTR1 VCOMH = 4V, VOML = -1.1V b"\x20\x00" # _INVOFF - b"\x36\x01\xa8" # _MADCTL + b"\x36\x01\xa0" # _MADCTL # 1 clk cycle nonoverlap, 2 cycle gate rise, 3 sycle osc equalie, # fix on VTL b"\x3a\x01\x05" # COLMOD - 16bit color diff --git a/pygamer/ugame.py b/pygamer/ugame.py index 336a1eb..99ea2ff 100644 --- a/pygamer/ugame.py +++ b/pygamer/ugame.py @@ -38,7 +38,7 @@ b"\xc4\x02\x8a\xee" b"\xc5\x01\x0e" # _VMCTR1 VCOMH = 4V, VOML = -1.1V b"\x20\x00" # _INVOFF - b"\x36\x01\xa8" # _MADCTL + b"\x36\x01\xa0" # _MADCTL # 1 clk cycle nonoverlap, 2 cycle gate rise, 3 sycle osc equalie, # fix on VTL b"\x3a\x01\x05" # COLMOD - 16bit color diff --git a/stage.py b/stage.py index 0492c76..103bab9 100644 --- a/stage.py +++ b/stage.py @@ -216,7 +216,7 @@ def read_palette(self): f.seek(self.data - self.colors * 4) for color in range(self.colors): buffer = f.read(4) - c = color565(buffer[0], buffer[1], buffer[2]) + c = color565(buffer[2], buffer[1], buffer[0]) palette[color] = ((c << 8) | (c >> 8)) & 0xffff return palette @@ -328,7 +328,7 @@ def read_palette(self): f.seek(13) for color in range(self.palette_size): buffer = f.read(3) - c = color565(buffer[2], buffer[1], buffer[0]) + c = color565(buffer[0], buffer[1], buffer[2]) palette[color] = ((c << 8) | (c >> 8)) & 0xffff return palette From 0d2c083a2fb57a1562d4806775f45273abbfbfae Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Thu, 16 Apr 2020 13:50:08 +0200 Subject: [PATCH 25/59] Bump audio buffer to 128 bytes --- stage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stage.py b/stage.py index 103bab9..d8fe214 100644 --- a/stage.py +++ b/stage.py @@ -156,7 +156,7 @@ class Audio: def __init__(self, speaker_pin, mute_pin=None): self.muted = True - self.buffer = bytearray(64) + self.buffer = bytearray(128) if mute_pin: self.mute_pin = digitalio.DigitalInOut(mute_pin) self.mute_pin.switch_to_output(value=not self.muted) From 27e4cb7fe0f9e457c130a1f1a718b9ed28e055f2 Mon Sep 17 00:00:00 2001 From: FoamyGuy Date: Tue, 16 Jun 2020 21:16:30 -0500 Subject: [PATCH 26/59] fix pybadge button mapping for X and O --- pybadge/ugame.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pybadge/ugame.py b/pybadge/ugame.py index fef524f..9141752 100644 --- a/pybadge/ugame.py +++ b/pybadge/ugame.py @@ -12,12 +12,12 @@ import time -K_X = 0x02 +K_X = 0x01 +K_O = 0x02 K_DOWN = 0x20 K_LEFT = 0x80 K_RIGHT = 0x10 K_UP = 0x40 -K_O = 0x01 K_START = 0x04 K_SELECT = 0x08 From 9596a5904ed757e6fbffcf03e7aa77ae9ecf5223 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Sat, 18 Jul 2020 14:47:18 +0200 Subject: [PATCH 27/59] Don't include embedded graphics in pew.py pew.py contained embedded graphics for nice-looking pixels and the game selection menu. This used up a lot of space. Now the graphics for the pixels is generated on the fly, and the graphics for the menu has been moved into the font. --- font/font.bmp | Bin 4250 -> 4250 bytes pewpew_m4/pew.py | 97 ++----------------- stage.py | 236 +++++++++++++++++++++++------------------------ 3 files changed, 126 insertions(+), 207 deletions(-) diff --git a/font/font.bmp b/font/font.bmp index 205141999cba67f7383361e314ee7ff83c8d8f27..c576e8b5ef88f176af9331f417d62e7cbe826075 100644 GIT binary patch literal 4250 zcmb7`&5h(X5Jp8~4+eY@sR$%RpjHNZ@yVBs!#8KMa4t?@avHx^B-K>U*ubcvrpRIy zzxwJ&`u@kyd5Y^N|9^7->g?MzCI7sTFTd4WnfLeKfBdEBE7vbCo8PDRSb2MUo8I4@ zy-#nh{LT75RhDy}m-C#LeV>;{8=1R)nM--f`IyU6=6vLt>2>*>9y-9Cj~Ja_do)v; z+&$jTyLmU3rgwBX^Jbvq%VnL?RT;i-`qIpqgFNr@+3kBi{PkbP{}=s` zd$!z>AFuMu{}rDNYroqOcu=i}?G?P9mLdWP2l5Vn@+yyt@q8JmIn{G3oW}7+;T=D= z&lN84*)5EP`N-jo|85K0kw3(aQBWp$#HjHn&%}@1f}fE;p6~IA_zHH^7ZcdTwR6VE zKhO90J@K0apWCmtdk)2D%EDt>s3YsO(+9qCKFo>m=cY4E65Xnh|LGur!cSEw)&yp4 zPsYVFZa#Rh1XjH^^y9*J!I@&_#ZA z+>U0L>v}+@RXuYza!={d1N2k->w34}dw~8g2Ys=h{G#9RX~R2{KO_E{gXRop?&d#x z%(L#l^(QXYYK~ZAy$4V|p{I=g;V*;b0VdDoisP~M%%M>q|B+eqm+v3+D}Q0%8eiiS zS&IY6?shKgA+I<(zs4`#FOHRhprXOUO1F;>Cn3&^>~vlHfiLhm!dIR-$~p!vr7lRt1#GrR`4_2*eH)xU>&ths({O(OK9F*gq*7ni{U6s~ zN9^UO1t{#3H)X{yVqfxR zpSj}S+FQjGdV-4wnFr^8#H32E|DFSm*P;(j$N1iaPx;jsEdciqX6(^p|90N5h@Y|R zXD}w_uE*s=rauhwm|q*ZCB{#%rp#%3t6ON{9RuIsY50h4)O9*oP)_|>$-RGF`B~^+ z#H|$T@!4PW#rKx=tB;P(M;|-Fh&Asa_x1b;%!X^k-sfHMf5yl!d%|nnn$9RM_)x#| zUiNc#^jzLik&BI+Fri)SC(k%xCU|&$L#$8Ad>Smg@ZM^TZCu#0{G>U!I94yc(h6|R`20iP} z_xZ8x+rqc}u^#Tt^RaE~ah|JO4ocIqZDrZc!h5Hb<=k`HPXg(2aj`#dTql>$eZ7yG z!}N5X)b#QoF=+IGOmtto30&r5c!MLLg5n1Cb3Bkcm2Cs|i7F7F|LZ4LoekZ`n$t-X zmh-$492^NB3qCGjTZ|w0BHu-oxXE|(&c2Uo%_Zt8#6ax1nG3aQe~p)H_0K$udO}}a zdtac*03$xGVO!t!{0uRBKzBW*Fe zKF%m6ce-aBu7`LJ=FYbbI_oZA;7fUZ)qBG_ura^Na|2IV(gWL*78ov|2N$`F7P4>v4m`z#ySPOMzy$<(*FF*Hdyk~{{I})?&?gL^J`09pDh@lS=vk9-JDn8>o>v+^? zuVK(dehu8tW|;e0K&CZ5b2oBN>eK_YQ~B$9l|Oob_Lq&mI5fZUcYMllB=Tp*-*V8L zLFdE#=Ztwa?6>~J#a_!1Yi#rYsweoQ(Lemrr_&B5_40&zWSu!Q^GE)ebEoa!@HhX3 zd~19SEqW~!9Q7r}pf?=Dzs4`_7w6`Ja7BlQlp!A*o`jhAs=xb5#xO;{ZCkfIk zew#_IFa8U^Aa(S~klzq+j{4k}9QdF3L4CV5-0ed&uZ5c78enM8nSaf`T|V|b!ytc+ ztB-T*IaBe{=L)Z13?E!?3T}kI%J>b7MZ~4PIT}>)(H|KxEULW-Ug?#cRuh>QGbGqAS zPX7QUj?Xb}J1qjU@8_7jy^kDlzK~qb@x2gPbo$X3Edb9?V(ig#|8_kdzoQ~{LkF|# z@qHC9iT*IrU#^b>L(++5&cT|{_8_*#5Jz#x;BWABd_*_udcL1Hee|y*KQsM{_~mRp zp8Z8zytl01xSN<=&o&N(5o_)t&yD;DKaH9gHjedR{Es>E%bxHax6U)uGd8sEytniC zB6==&ROI3mJCdJW6VHSn&TrhXe4Byk+nJVh5p5H>`7+3PkaKi|x@UdA~1y=a`4_y`yd(%QUat5i9hxFE4X@nerDL)P3{?v#LvAD>^?4L%_#fxOl*^kF_oHusS zr-@JGQtuIAzuyD+FlGIR7vqv`{N`EI6KA6D(;Kh#-{Yrk#y9$J)NfNi;_LV0Ms9Fs zzJCHg_|lGU^OCu0-@xxo{-wM%I& Date: Thu, 30 Jul 2020 21:14:57 +0200 Subject: [PATCH 28/59] Make the generated pixels a little bit more nice-looking. - Same width for horizontal and vertical grid lines. - Rounded corners. - Colors adjusted to better match the brightness relations of an LED display. --- pewpew_m4/pew.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pewpew_m4/pew.py b/pewpew_m4/pew.py index e7a9c12..9897f4b 100644 --- a/pewpew_m4/pew.py +++ b/pewpew_m4/pew.py @@ -20,7 +20,7 @@ ) _SALT = const(132) -_PALETTE = array.array('H', (0x0, 0x4a29, 0xc032, 0x98, 0xa0fc, 0xf42, 0x825b, +_PALETTE = array.array('H', (0x0, 0x4a29, 0x6004, 0xf8, 0xfd, 0xf42, 0x825b, 0xf8, 0xfe, 0x125b, 0xcffb, 0xe0cf, 0xffff, 0x1ff8, 0xdbff, 0xffff)) @@ -191,6 +191,11 @@ def init(): for y in range(0, 15): for x in range(0, 7): _bank[c * 128 + y * 8 + x] = c | c << 4 + _bank[c * 128 + y * 8 + 7] = c << 4 + _bank[c * 128] = c + _bank[c * 128 + 7] = 0 + _bank[c * 128 + 14 * 8] = c + _bank[c * 128 + 14 * 8 + 7] = 0 tiles = stage.Bank(_bank, _PALETTE) _grid = stage.Grid(tiles, 10, 8) _grid.move(0, 0) From 40d8a03b4569d566faa62fcb0f798178118f2954 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Fri, 2 Oct 2020 11:43:11 +0200 Subject: [PATCH 29/59] Make the GameOver exception inherit from SystemExit --- pewpew_m4/pew.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pewpew_m4/pew.py b/pewpew_m4/pew.py index 9897f4b..c654544 100644 --- a/pewpew_m4/pew.py +++ b/pewpew_m4/pew.py @@ -59,7 +59,7 @@ def tick(delay): time.sleep(_tick - now) -class GameOver(Exception): +class GameOver(SystemExit): pass From 64aaa13caf2449111548b3c2a623c290da1533f1 Mon Sep 17 00:00:00 2001 From: Tsutomu IKEGAMI Date: Thu, 1 Apr 2021 17:21:21 +0900 Subject: [PATCH 30/59] Twice as much memory was allocated when initializing stage.Grid (and stage.WallGrid). --- stage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stage.py b/stage.py index 3b23ec0..c151a94 100644 --- a/stage.py +++ b/stage.py @@ -420,7 +420,7 @@ def __init__(self, bank, width=8, height=8, palette=None, buffer=None): self.height = height self.bank = bank self.palette = palette or bank.palette - self.buffer = buffer or bytearray(self.stride * height) + self.buffer = buffer or bytearray((self.stride * height)>>1) self.layer = _stage.Layer(self.stride, self.height, self.bank.buffer, self.palette, self.buffer) From b014aa045d9014b86a4ae583f452c6b8282a61b9 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Tue, 29 Jun 2021 00:44:43 +0200 Subject: [PATCH 31/59] Make PewPew M4 use the keypad module instead of gamepad --- pewpew_m4/ugame.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/pewpew_m4/ugame.py b/pewpew_m4/ugame.py index fbd4d02..9795677 100644 --- a/pewpew_m4/ugame.py +++ b/pewpew_m4/ugame.py @@ -1,9 +1,8 @@ import board -import digitalio -import gamepad import stage import supervisor import time +import keypad K_X = 0x01 @@ -14,24 +13,30 @@ K_O = 0x20 K_START = 0x40 K_Z = 0x40 -K_SELECT = 0x00 +K_SELECT = 0x80 class _Buttons: def __init__(self): - self.buttons = gamepad.GamePad( - digitalio.DigitalInOut(board.BUTTON_X), - digitalio.DigitalInOut(board.BUTTON_DOWN), - digitalio.DigitalInOut(board.BUTTON_LEFT), - digitalio.DigitalInOut(board.BUTTON_RIGHT), - digitalio.DigitalInOut(board.BUTTON_UP), - digitalio.DigitalInOut(board.BUTTON_O), - digitalio.DigitalInOut(board.BUTTON_Z), - ) + self.keys = keypad.Keys((board.BUTTON_X, board.BUTTON_DOWN, + board.BUTTON_LEFT, board.BUTTON_RIGHT, board.BUTTON_UP, + board.BUTTON_O, board.BUTTON_Z), value_when_pressed=False, + interval=0.05) + self.last_state = 0 + self.event = keypad.Event(0, False) self.last_z_press = None def get_pressed(self): - buttons = self.buttons.get_pressed() + buttons = self.last_state + events = self.keys.events + while events: + if events.get_into(self.event): + bit = 1 << self.event.key_number + if self.event.pressed: + buttons |= bit + self.last_state |= bit + else: + self.last_state &= ~bit if buttons & K_Z: now = time.monotonic() if self.last_z_press: From a2e540e7120ae10646e963704ebacf98392fce1f Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Sun, 29 Aug 2021 22:41:34 +0200 Subject: [PATCH 32/59] Update pybadge and pygamer to use keypad module instead of gamepadshift --- pewpew_m4/pew.py | 14 ++++----- pewpew_m4/ugame.py | 2 +- pybadge/ugame.py | 63 ++++++++++++++++++++++++-------------- pygamer/ugame.py | 76 ++++++++++++++++++++++++++-------------------- 4 files changed, 92 insertions(+), 63 deletions(-) diff --git a/pewpew_m4/pew.py b/pewpew_m4/pew.py index c654544..19e3937 100644 --- a/pewpew_m4/pew.py +++ b/pewpew_m4/pew.py @@ -24,12 +24,12 @@ 0xf8, 0xfe, 0x125b, 0xcffb, 0xe0cf, 0xffff, 0x1ff8, 0xdbff, 0xffff)) -K_X = 0x01 -K_DOWN = 0x02 -K_LEFT = 0x04 -K_RIGHT = 0x08 -K_UP = 0x10 -K_O = 0x20 +K_X = ugame.K_X +K_DOWN = ugame.K_DOWN +K_LEFT = ugame.K_LEFT +K_RIGHT = ugame.K_RIGHT +K_UP = ugame.K_UP +K_O = ugame.K_O _tick = None _display = None @@ -178,7 +178,7 @@ def __str__(self): def init(): - global _tick, _display, _gamepad, _bitmap, _grid, _game + global _tick, _display, _bitmap, _grid, _game if _tick is not None: return diff --git a/pewpew_m4/ugame.py b/pewpew_m4/ugame.py index 9795677..47e8b99 100644 --- a/pewpew_m4/ugame.py +++ b/pewpew_m4/ugame.py @@ -51,4 +51,4 @@ def get_pressed(self): display = board.DISPLAY buttons = _Buttons() -audio = stage.Audio(board.P5) +audio = stage.Audio(board.SPEAKER) diff --git a/pybadge/ugame.py b/pybadge/ugame.py index 9141752..aaef22e 100644 --- a/pybadge/ugame.py +++ b/pybadge/ugame.py @@ -1,15 +1,14 @@ """ -A helper module that initializes the display and buttons for the uGame -game console. See https://hackaday.io/project/27629-game +A helper module that initializes the display and buttons for the +Adafruit PyBadge game console. """ import board -import digitalio -import gamepadshift import stage import displayio import busio import time +import keypad K_X = 0x01 @@ -46,27 +45,47 @@ b"\x13\x80\x0a" # _NORON b"\x29\x80\x64" # _DISPON ) + + +class _Buttons: + def __init__(self): + self.keys = keypad.ShiftRegisterKeys(clock=board.BUTTON_CLOCK, + data=board.BUTTON_OUT, latch=board.BUTTON_LATCH, key_count=8, + interval=0.05) + self.last_state = 0 + self.event = keypad.Event(0, False) + self.last_z_press = None + + def get_pressed(self): + buttons = self.last_state + events = self.keys.events + while events: + if events.get_into(self.event): + bit = 1 << self.event.key_number + if self.event.pressed: + buttons |= bit + self.last_state |= bit + else: + self.last_state &= ~bit + if buttons & K_START: + now = time.monotonic() + if self.last_z_press: + if now - self.last_z_press > 2: + supervisor.reload() + else: + self.last_z_press = now + else: + self.last_z_press = None + return buttons + + displayio.release_displays() _tft_spi = busio.SPI(clock=board.TFT_SCK, MOSI=board.TFT_MOSI) -_tft_spi.try_lock() -_tft_spi.configure(baudrate=24000000) -_tft_spi.unlock() _fourwire = displayio.FourWire(_tft_spi, command=board.TFT_DC, - chip_select=board.TFT_CS) -_reset = digitalio.DigitalInOut(board.TFT_RST) -_reset.switch_to_output(value=0) -time.sleep(0.05) -_reset.value = 1 -time.sleep(0.05) + chip_select=board.TFT_CS, reset=board.TFT_RST) display = displayio.Display(_fourwire, _TFT_INIT, width=160, height=128, - rotation=0, backlight_pin=board.TFT_LITE) + rotation=0, backlight_pin=board.TFT_LITE, + auto_refresh=False, auto_brightness=True) del _TFT_INIT -display.auto_brightness = True - -buttons = gamepadshift.GamePadShift( - digitalio.DigitalInOut(board.BUTTON_CLOCK), - digitalio.DigitalInOut(board.BUTTON_OUT), - digitalio.DigitalInOut(board.BUTTON_LATCH), -) - +buttons = _Buttons() audio = stage.Audio(board.SPEAKER, board.SPEAKER_ENABLE) diff --git a/pygamer/ugame.py b/pygamer/ugame.py index 99ea2ff..37ab8c5 100644 --- a/pygamer/ugame.py +++ b/pygamer/ugame.py @@ -1,16 +1,15 @@ """ -A helper module that initializes the display and buttons for the uGame -game console. See https://hackaday.io/project/27629-game +A helper module that initializes the display and buttons for the +Adafruit PyGamer game console. """ import board -import digitalio import analogio -import gamepadshift import stage import displayio import busio import time +import keypad K_X = 0x01 @@ -47,49 +46,60 @@ b"\x13\x80\x0a" # _NORON b"\x29\x80\x64" # _DISPON ) -displayio.release_displays() -_tft_spi = busio.SPI(clock=board.TFT_SCK, MOSI=board.TFT_MOSI) -_tft_spi.try_lock() -_tft_spi.configure(baudrate=24000000) -_tft_spi.unlock() -_fourwire = displayio.FourWire(_tft_spi, command=board.TFT_DC, - chip_select=board.TFT_CS) -_reset = digitalio.DigitalInOut(board.TFT_RST) -_reset.switch_to_output(value=0) -time.sleep(0.05) -_reset.value = 1 -time.sleep(0.05) -display = displayio.Display(_fourwire, _TFT_INIT, width=160, height=128, - rotation=0, backlight_pin=board.TFT_LITE) -del _TFT_INIT -display.auto_brightness = True -class Buttons: +class _Buttons: def __init__(self): - self.buttons = gamepadshift.GamePadShift( - digitalio.DigitalInOut(board.BUTTON_CLOCK), - digitalio.DigitalInOut(board.BUTTON_OUT), - digitalio.DigitalInOut(board.BUTTON_LATCH), - ) + self.keys = keypad.ShiftRegisterKeys(clock=board.BUTTON_CLOCK, + data=board.BUTTON_OUT, latch=board.BUTTON_LATCH, key_count=4, + interval=0.05) + self.last_state = 0 + self.event = keypad.Event(0, False) + self.last_z_press = None self.joy_x = analogio.AnalogIn(board.JOYSTICK_X) self.joy_y = analogio.AnalogIn(board.JOYSTICK_Y) def get_pressed(self): - pressed = self.buttons.get_pressed() + buttons = self.last_state + events = self.keys.events + while events: + if events.get_into(self.event): + bit = 1 << self.event.key_number + if self.event.pressed: + buttons |= bit + self.last_state |= bit + else: + self.last_state &= ~bit + if buttons & K_START: + now = time.monotonic() + if self.last_z_press: + if now - self.last_z_press > 2: + supervisor.reload() + else: + self.last_z_press = now + else: + self.last_z_press = None dead = 15000 x = self.joy_x.value - 32767 if x < -dead: - pressed |= K_LEFT + buttons |= K_LEFT elif x > dead: - pressed |= K_RIGHT + buttons |= K_RIGHT y = self.joy_y.value - 32767 if y < -dead: - pressed |= K_UP + buttons |= K_UP elif y > dead: - pressed |= K_DOWN - return pressed + buttons |= K_DOWN + return buttons -buttons = Buttons() +displayio.release_displays() +_tft_spi = busio.SPI(clock=board.TFT_SCK, MOSI=board.TFT_MOSI) +_fourwire = displayio.FourWire(_tft_spi, command=board.TFT_DC, + chip_select=board.TFT_CS, reset=board.TFT_RST) +display = displayio.Display(_fourwire, _TFT_INIT, width=160, height=128, + rotation=0, backlight_pin=board.TFT_LITE, + auto_refresh=False, auto_brightness=True) +del _TFT_INIT +buttons = _Buttons() audio = stage.Audio(board.SPEAKER, board.SPEAKER_ENABLE) From a895b3a4c74cb4ce47edf0da54b05c7570888816 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Sun, 29 Aug 2021 22:41:34 +0200 Subject: [PATCH 33/59] Update pybadge and pygamer to use keypad module instead of gamepadshift --- pewpew_m4/pew.py | 14 ++++----- pewpew_m4/ugame.py | 2 +- pybadge/ugame.py | 64 +++++++++++++++++++++++--------------- pygamer/ugame.py | 77 ++++++++++++++++++++++++---------------------- 4 files changed, 88 insertions(+), 69 deletions(-) diff --git a/pewpew_m4/pew.py b/pewpew_m4/pew.py index c654544..19e3937 100644 --- a/pewpew_m4/pew.py +++ b/pewpew_m4/pew.py @@ -24,12 +24,12 @@ 0xf8, 0xfe, 0x125b, 0xcffb, 0xe0cf, 0xffff, 0x1ff8, 0xdbff, 0xffff)) -K_X = 0x01 -K_DOWN = 0x02 -K_LEFT = 0x04 -K_RIGHT = 0x08 -K_UP = 0x10 -K_O = 0x20 +K_X = ugame.K_X +K_DOWN = ugame.K_DOWN +K_LEFT = ugame.K_LEFT +K_RIGHT = ugame.K_RIGHT +K_UP = ugame.K_UP +K_O = ugame.K_O _tick = None _display = None @@ -178,7 +178,7 @@ def __str__(self): def init(): - global _tick, _display, _gamepad, _bitmap, _grid, _game + global _tick, _display, _bitmap, _grid, _game if _tick is not None: return diff --git a/pewpew_m4/ugame.py b/pewpew_m4/ugame.py index 9795677..47e8b99 100644 --- a/pewpew_m4/ugame.py +++ b/pewpew_m4/ugame.py @@ -51,4 +51,4 @@ def get_pressed(self): display = board.DISPLAY buttons = _Buttons() -audio = stage.Audio(board.P5) +audio = stage.Audio(board.SPEAKER) diff --git a/pybadge/ugame.py b/pybadge/ugame.py index 9141752..f3b30e8 100644 --- a/pybadge/ugame.py +++ b/pybadge/ugame.py @@ -1,15 +1,9 @@ -""" -A helper module that initializes the display and buttons for the uGame -game console. See https://hackaday.io/project/27629-game -""" - import board -import digitalio -import gamepadshift import stage import displayio import busio import time +import keypad K_X = 0x01 @@ -46,27 +40,47 @@ b"\x13\x80\x0a" # _NORON b"\x29\x80\x64" # _DISPON ) + + +class _Buttons: + def __init__(self): + self.keys = keypad.ShiftRegisterKeys(clock=board.BUTTON_CLOCK, + data=board.BUTTON_OUT, latch=board.BUTTON_LATCH, key_count=8, + interval=0.05) + self.last_state = 0 + self.event = keypad.Event(0, False) + self.last_z_press = None + + def get_pressed(self): + buttons = self.last_state + events = self.keys.events + while events: + if events.get_into(self.event): + bit = 1 << self.event.key_number + if self.event.pressed: + buttons |= bit + self.last_state |= bit + else: + self.last_state &= ~bit + if buttons & K_START: + now = time.monotonic() + if self.last_z_press: + if now - self.last_z_press > 2: + supervisor.reload() + else: + self.last_z_press = now + else: + self.last_z_press = None + return buttons + + displayio.release_displays() _tft_spi = busio.SPI(clock=board.TFT_SCK, MOSI=board.TFT_MOSI) -_tft_spi.try_lock() -_tft_spi.configure(baudrate=24000000) -_tft_spi.unlock() _fourwire = displayio.FourWire(_tft_spi, command=board.TFT_DC, - chip_select=board.TFT_CS) -_reset = digitalio.DigitalInOut(board.TFT_RST) -_reset.switch_to_output(value=0) -time.sleep(0.05) -_reset.value = 1 -time.sleep(0.05) + chip_select=board.TFT_CS, reset=board.TFT_RST) display = displayio.Display(_fourwire, _TFT_INIT, width=160, height=128, - rotation=0, backlight_pin=board.TFT_LITE) + rotation=0, backlight_pin=board.TFT_LITE, + auto_refresh=False, auto_brightness=True) del _TFT_INIT -display.auto_brightness = True - -buttons = gamepadshift.GamePadShift( - digitalio.DigitalInOut(board.BUTTON_CLOCK), - digitalio.DigitalInOut(board.BUTTON_OUT), - digitalio.DigitalInOut(board.BUTTON_LATCH), -) - +buttons = _Buttons() audio = stage.Audio(board.SPEAKER, board.SPEAKER_ENABLE) diff --git a/pygamer/ugame.py b/pygamer/ugame.py index 99ea2ff..88f6dea 100644 --- a/pygamer/ugame.py +++ b/pygamer/ugame.py @@ -1,16 +1,10 @@ -""" -A helper module that initializes the display and buttons for the uGame -game console. See https://hackaday.io/project/27629-game -""" - import board -import digitalio import analogio -import gamepadshift import stage import displayio import busio import time +import keypad K_X = 0x01 @@ -47,49 +41,60 @@ b"\x13\x80\x0a" # _NORON b"\x29\x80\x64" # _DISPON ) -displayio.release_displays() -_tft_spi = busio.SPI(clock=board.TFT_SCK, MOSI=board.TFT_MOSI) -_tft_spi.try_lock() -_tft_spi.configure(baudrate=24000000) -_tft_spi.unlock() -_fourwire = displayio.FourWire(_tft_spi, command=board.TFT_DC, - chip_select=board.TFT_CS) -_reset = digitalio.DigitalInOut(board.TFT_RST) -_reset.switch_to_output(value=0) -time.sleep(0.05) -_reset.value = 1 -time.sleep(0.05) -display = displayio.Display(_fourwire, _TFT_INIT, width=160, height=128, - rotation=0, backlight_pin=board.TFT_LITE) -del _TFT_INIT -display.auto_brightness = True -class Buttons: +class _Buttons: def __init__(self): - self.buttons = gamepadshift.GamePadShift( - digitalio.DigitalInOut(board.BUTTON_CLOCK), - digitalio.DigitalInOut(board.BUTTON_OUT), - digitalio.DigitalInOut(board.BUTTON_LATCH), - ) + self.keys = keypad.ShiftRegisterKeys(clock=board.BUTTON_CLOCK, + data=board.BUTTON_OUT, latch=board.BUTTON_LATCH, key_count=4, + interval=0.05) + self.last_state = 0 + self.event = keypad.Event(0, False) + self.last_z_press = None self.joy_x = analogio.AnalogIn(board.JOYSTICK_X) self.joy_y = analogio.AnalogIn(board.JOYSTICK_Y) def get_pressed(self): - pressed = self.buttons.get_pressed() + buttons = self.last_state + events = self.keys.events + while events: + if events.get_into(self.event): + bit = 1 << self.event.key_number + if self.event.pressed: + buttons |= bit + self.last_state |= bit + else: + self.last_state &= ~bit + if buttons & K_START: + now = time.monotonic() + if self.last_z_press: + if now - self.last_z_press > 2: + supervisor.reload() + else: + self.last_z_press = now + else: + self.last_z_press = None dead = 15000 x = self.joy_x.value - 32767 if x < -dead: - pressed |= K_LEFT + buttons |= K_LEFT elif x > dead: - pressed |= K_RIGHT + buttons |= K_RIGHT y = self.joy_y.value - 32767 if y < -dead: - pressed |= K_UP + buttons |= K_UP elif y > dead: - pressed |= K_DOWN - return pressed + buttons |= K_DOWN + return buttons -buttons = Buttons() +displayio.release_displays() +_tft_spi = busio.SPI(clock=board.TFT_SCK, MOSI=board.TFT_MOSI) +_fourwire = displayio.FourWire(_tft_spi, command=board.TFT_DC, + chip_select=board.TFT_CS, reset=board.TFT_RST) +display = displayio.Display(_fourwire, _TFT_INIT, width=160, height=128, + rotation=0, backlight_pin=board.TFT_LITE, + auto_refresh=False, auto_brightness=True) +del _TFT_INIT +buttons = _Buttons() audio = stage.Audio(board.SPEAKER, board.SPEAKER_ENABLE) From e211d39f5d5d235489da3984c939eaba99121acc Mon Sep 17 00:00:00 2001 From: Christian Walther Date: Fri, 25 Sep 2020 17:59:03 +0200 Subject: [PATCH 34/59] Z long-press should go to the menu, not restart the game. --- pewpew_m4/ugame.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pewpew_m4/ugame.py b/pewpew_m4/ugame.py index 47e8b99..0810ac1 100644 --- a/pewpew_m4/ugame.py +++ b/pewpew_m4/ugame.py @@ -41,6 +41,7 @@ def get_pressed(self): now = time.monotonic() if self.last_z_press: if now - self.last_z_press > 2: + supervisor.set_next_code_file(None) supervisor.reload() else: self.last_z_press = now From 275c03e340c3853ab1c53fc15b6df07ceb672bdc Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Sun, 29 Aug 2021 23:56:24 +0200 Subject: [PATCH 35/59] Add set_next_code_file to pygamer and pybadge --- pybadge/ugame.py | 1 + pygamer/ugame.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pybadge/ugame.py b/pybadge/ugame.py index 855e090..65918cb 100644 --- a/pybadge/ugame.py +++ b/pybadge/ugame.py @@ -71,6 +71,7 @@ def get_pressed(self): now = time.monotonic() if self.last_z_press: if now - self.last_z_press > 2: + supervisor.set_next_code_file(None) supervisor.reload() else: self.last_z_press = now diff --git a/pygamer/ugame.py b/pygamer/ugame.py index 37ab8c5..58d8a1d 100644 --- a/pygamer/ugame.py +++ b/pygamer/ugame.py @@ -74,6 +74,7 @@ def get_pressed(self): now = time.monotonic() if self.last_z_press: if now - self.last_z_press > 2: + supervisor.set_next_code_file(None) supervisor.reload() else: self.last_z_press = now From 3fa977caa0cca38c2b524c93f7f17b1c3d0bb1be Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Mon, 30 Aug 2021 18:57:40 +0200 Subject: [PATCH 36/59] Add support for Meowbit --- meowbit/audioio.py | 13 ++++++++++ meowbit/stage.py | 1 + meowbit/ugame.py | 65 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 meowbit/audioio.py create mode 120000 meowbit/stage.py create mode 100644 meowbit/ugame.py diff --git a/meowbit/audioio.py b/meowbit/audioio.py new file mode 100644 index 0000000..aae9e9f --- /dev/null +++ b/meowbit/audioio.py @@ -0,0 +1,13 @@ +class AudioOut: + def __init__(self, pin): + pass + + def stop(self): + pass + + def play(self, what): + pass + +class WaveFile: + def __init__(self, file, buffer=None): + pass diff --git a/meowbit/stage.py b/meowbit/stage.py new file mode 120000 index 0000000..2dedc93 --- /dev/null +++ b/meowbit/stage.py @@ -0,0 +1 @@ +../stage.py \ No newline at end of file diff --git a/meowbit/ugame.py b/meowbit/ugame.py new file mode 100644 index 0000000..cfc6b6e --- /dev/null +++ b/meowbit/ugame.py @@ -0,0 +1,65 @@ +import board +import stage +import busio +import time +import keypad + + +K_X = 0x01 +K_O = 0x02 +K_DOWN = 0x04 +K_LEFT = 0x08 +K_RIGHT = 0x10 +K_UP = 0x20 +K_Z = 0x40 + +display = board.DISPLAY +display.auto_brightness = True +display.auto_refresh = False + + +class _Buttons: + def __init__(self): + self.keys = keypad.Keys((board.BTNA, board.BTNB, board.DOWN, + board.LEFT, board.RIGHT, board.UP), + value_when_pressed=False, interval=0.05) + self.last_state = 0 + self.event = keypad.Event(0, False) + self.last_z_press = None + + def get_pressed(self): + buttons = self.last_state + events = self.keys.events + while events: + if events.get_into(self.event): + bit = 1 << self.event.key_number + if self.event.pressed: + buttons |= bit + self.last_state |= bit + else: + self.last_state &= ~bit + if buttons & K_Z: + now = time.monotonic() + if self.last_z_press: + if now - self.last_z_press > 2: + supervisor.set_next_code_file(None) + supervisor.reload() + else: + self.last_z_press = now + else: + self.last_z_press = None + return buttons + + +class DummyAudio: + def play(self, f, loop=False): + pass + + def stop(self): + pass + + def mute(self, mute): + pass + +audio = DummyAudio() +buttons = _Buttons() From d0f1c46d7f879cd60562ee69900d619499d4d206 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Mon, 30 Aug 2021 19:50:25 +0200 Subject: [PATCH 37/59] Allow use of audiopwmio in place of audioio --- meowbit/audioio.py | 13 ------------- meowbit/ugame.py | 27 ++++++++++++++++++++------- stage.py | 17 ++++++++++++----- 3 files changed, 32 insertions(+), 25 deletions(-) delete mode 100644 meowbit/audioio.py diff --git a/meowbit/audioio.py b/meowbit/audioio.py deleted file mode 100644 index aae9e9f..0000000 --- a/meowbit/audioio.py +++ /dev/null @@ -1,13 +0,0 @@ -class AudioOut: - def __init__(self, pin): - pass - - def stop(self): - pass - - def play(self, what): - pass - -class WaveFile: - def __init__(self, file, buffer=None): - pass diff --git a/meowbit/ugame.py b/meowbit/ugame.py index cfc6b6e..62dc378 100644 --- a/meowbit/ugame.py +++ b/meowbit/ugame.py @@ -3,6 +3,7 @@ import busio import time import keypad +import audiocore K_X = 0x01 @@ -51,15 +52,27 @@ def get_pressed(self): return buttons -class DummyAudio: - def play(self, f, loop=False): - pass +class _Audio: + last_audio = None + + def __init__(self): + self.muted = True + self.buffer = bytearray(128) + self.audio = board.BUZZ + + def play(self, audio_file, loop=False): + if self.muted: + return + self.stop() + wave = audiocore.WaveFile(audio_file, self.buffer) + self.audio.play(wave, loop=loop) def stop(self): - pass + self.audio.stop() + + def mute(self, value=True): + self.muted = value - def mute(self, mute): - pass -audio = DummyAudio() +audio = _Audio() buttons = _Buttons() diff --git a/stage.py b/stage.py index c151a94..fb6cb6e 100644 --- a/stage.py +++ b/stage.py @@ -1,12 +1,16 @@ import time import array import digitalio -import audioio -import struct try: - import audiocore + import audioio except ImportError: - audiocore = audioio + pass +else: + try: + import audiocore + except ImportError: + audiocore = audioio +import struct import _stage @@ -160,7 +164,10 @@ def __init__(self, speaker_pin, mute_pin=None): self.mute_pin.switch_to_output(value=not self.muted) else: self.mute_pin = None - self.audio = audioio.AudioOut(speaker_pin) + if audioio is None: + self.audio = audiopwmio.PWMAudioOut(speaker_pin) + else: + self.audio = audioio.AudioOut(speaker_pin) def play(self, audio_file, loop=False): """ From f6d4ea3915ccbf696375a4f877ad0649becf3e71 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Wed, 29 Sep 2021 20:15:57 +0200 Subject: [PATCH 38/59] Move the Audio class to ugame.py The Audio class is very different on different boards, so it should really live in the board-specific module. This also avoids having to check the existence of different audio libraries. --- pewpew_m4/ugame.py | 26 +++++++++++++++++++++++- pybadge/ugame.py | 38 +++++++++++++++++++++++++++++------ pygamer/ugame.py | 38 +++++++++++++++++++++++++++++------ stage.py | 49 ---------------------------------------------- 4 files changed, 89 insertions(+), 62 deletions(-) diff --git a/pewpew_m4/ugame.py b/pewpew_m4/ugame.py index 0810ac1..f92a6e5 100644 --- a/pewpew_m4/ugame.py +++ b/pewpew_m4/ugame.py @@ -3,6 +3,8 @@ import supervisor import time import keypad +import audioio +import audiocore K_X = 0x01 @@ -50,6 +52,28 @@ def get_pressed(self): return buttons +class _Audio: + last_audio = None + + def __init__(self, speaker_pin): + self.muted = True + self.buffer = bytearray(128) + self.audio = audioio.AudioOut(speaker_pin) + + def play(self, audio_file, loop=False): + if self.muted: + return + self.stop() + wave = audiocore.WaveFile(audio_file, self.buffer) + self.audio.play(wave, loop=loop) + + def stop(self): + self.audio.stop() + + def mute(self, value=True): + self.muted = value + + display = board.DISPLAY buttons = _Buttons() -audio = stage.Audio(board.SPEAKER) +audio = _Audio(board.SPEAKER) diff --git a/pybadge/ugame.py b/pybadge/ugame.py index 65918cb..8b64efb 100644 --- a/pybadge/ugame.py +++ b/pybadge/ugame.py @@ -1,14 +1,11 @@ import board -""" -A helper module that initializes the display and buttons for the -Adafruit PyBadge game console. -""" - import stage import displayio import busio import time import keypad +import audioio +import audiocore K_X = 0x01 @@ -80,6 +77,35 @@ def get_pressed(self): return buttons +class _Audio: + last_audio = None + + def __init__(self, speaker_pin, mute_pin=None): + self.muted = True + self.buffer = bytearray(128) + if mute_pin: + self.mute_pin = digitalio.DigitalInOut(mute_pin) + self.mute_pin.switch_to_output(value=not self.muted) + else: + self.mute_pin = None + self.audio = audioio.AudioOut(speaker_pin) + + def play(self, audio_file, loop=False): + if self.muted: + return + self.stop() + wave = audiocore.WaveFile(audio_file, self.buffer) + self.audio.play(wave, loop=loop) + + def stop(self): + self.audio.stop() + + def mute(self, value=True): + self.muted = value + if self.mute_pin: + self.mute_pin.value = not value + + displayio.release_displays() _tft_spi = busio.SPI(clock=board.TFT_SCK, MOSI=board.TFT_MOSI) _fourwire = displayio.FourWire(_tft_spi, command=board.TFT_DC, @@ -89,4 +115,4 @@ def get_pressed(self): auto_refresh=False, auto_brightness=True) del _TFT_INIT buttons = _Buttons() -audio = stage.Audio(board.SPEAKER, board.SPEAKER_ENABLE) +audio = _Audio(board.SPEAKER, board.SPEAKER_ENABLE) diff --git a/pygamer/ugame.py b/pygamer/ugame.py index 58d8a1d..666710f 100644 --- a/pygamer/ugame.py +++ b/pygamer/ugame.py @@ -1,8 +1,3 @@ -""" -A helper module that initializes the display and buttons for the -Adafruit PyGamer game console. -""" - import board import analogio import stage @@ -10,6 +5,8 @@ import busio import time import keypad +import audioio +import audiocore K_X = 0x01 @@ -94,6 +91,35 @@ def get_pressed(self): return buttons +class _Audio: + last_audio = None + + def __init__(self, speaker_pin, mute_pin=None): + self.muted = True + self.buffer = bytearray(128) + if mute_pin: + self.mute_pin = digitalio.DigitalInOut(mute_pin) + self.mute_pin.switch_to_output(value=not self.muted) + else: + self.mute_pin = None + self.audio = audioio.AudioOut(speaker_pin) + + def play(self, audio_file, loop=False): + if self.muted: + return + self.stop() + wave = audiocore.WaveFile(audio_file, self.buffer) + self.audio.play(wave, loop=loop) + + def stop(self): + self.audio.stop() + + def mute(self, value=True): + self.muted = value + if self.mute_pin: + self.mute_pin.value = not value + + displayio.release_displays() _tft_spi = busio.SPI(clock=board.TFT_SCK, MOSI=board.TFT_MOSI) _fourwire = displayio.FourWire(_tft_spi, command=board.TFT_DC, @@ -103,4 +129,4 @@ def get_pressed(self): auto_refresh=False, auto_brightness=True) del _TFT_INIT buttons = _Buttons() -audio = stage.Audio(board.SPEAKER, board.SPEAKER_ENABLE) +audio = _Audio(board.SPEAKER, board.SPEAKER_ENABLE) diff --git a/stage.py b/stage.py index fb6cb6e..6a3ae42 100644 --- a/stage.py +++ b/stage.py @@ -1,15 +1,6 @@ import time import array import digitalio -try: - import audioio -except ImportError: - pass -else: - try: - import audiocore - except ImportError: - audiocore = audioio import struct import _stage @@ -152,46 +143,6 @@ def collide(ax0, ay0, ax1, ay1, bx0, by0, bx1=None, by1=None): return not (ax1 < bx0 or ay1 < by0 or ax0 > bx1 or ay0 > by1) -class Audio: - """Play sounds.""" - last_audio = None - - def __init__(self, speaker_pin, mute_pin=None): - self.muted = True - self.buffer = bytearray(128) - if mute_pin: - self.mute_pin = digitalio.DigitalInOut(mute_pin) - self.mute_pin.switch_to_output(value=not self.muted) - else: - self.mute_pin = None - if audioio is None: - self.audio = audiopwmio.PWMAudioOut(speaker_pin) - else: - self.audio = audioio.AudioOut(speaker_pin) - - def play(self, audio_file, loop=False): - """ - Start playing an open file ``audio_file``. If ``loop`` is ``True``, - repeat until stopped. This function doesn't block, the sound is - played in the background. - """ - if self.muted: - return - self.stop() - wave = audiocore.WaveFile(audio_file, self.buffer) - self.audio.play(wave, loop=loop) - - def stop(self): - """Stop playing whatever sound is playing.""" - self.audio.stop() - - def mute(self, value=True): - """Enable or disable all sounds.""" - self.muted = value - if self.mute_pin: - self.mute_pin.value = not value - - class BMP16: """Read 16-color BMP files.""" From 2ae4bb158f56747ed30037de1d03aac5c6c25526 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Mon, 4 Oct 2021 13:16:04 +0200 Subject: [PATCH 39/59] Return size of rendered text from Text.text() Return the actual area taken up by the rendered text, so that it can be more easily centered. Also allow characters <= 32 in text, except for newline. --- stage.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/stage.py b/stage.py index 6a3ae42..8051c17 100644 --- a/stage.py +++ b/stage.py @@ -529,16 +529,26 @@ def cursor(self, x=None, y=None): self.column = min(max(0, x), self.height - 1) def text(self, text, hightlight=False): - """Display text starting at the current cursor location.""" + """ + Display text starting at the current cursor location. + Return the dimensions of the rendered text. + """ + longest = 0 + tallest = 0 for c in text: - if ord(c) >= 32: + if c != '\n': self.char(self.column, self.row, c, hightlight) self.column += 1 if self.column >= self.width or c == '\n': + longest = max(longest, self.column) self.column = 0 self.row += 1 if self.row >= self.height: + tallest = max(tallest, self.row) self.row = 0 + longest = max(longest, self.column) + tallest = max(tallest, self.row) + (1 if self.column > 0 else 0) + return longest * 8, tallest * 8 def clear(self): """Clear all text from the layer.""" From ab28481acaac4a38b3066bd90249ae6fcf4aeda5 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Thu, 7 Oct 2021 12:55:22 +0200 Subject: [PATCH 40/59] Add support for scaled displays Allows to use 2x2 pixels on larger displays. Also fix some bounds checking code. --- stage.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/stage.py b/stage.py index 8051c17..b391e6c 100644 --- a/stage.py +++ b/stage.py @@ -567,11 +567,15 @@ class Stage: """ buffer = bytearray(512) - def __init__(self, display, fps=6): + def __init__(self, display, fps=6, scale=None): + if scale is None: + self.scale = max(1, display.width // 128) + else: + self.scale = scale self.layers = [] self.display = display - self.width = display.width - self.height = display.height + self.width = display.width // self.scale + self.height = display.height // self.scale self.last_tick = time.monotonic() self.tick_delay = 1 / fps @@ -588,10 +592,17 @@ def render_block(self, x0=0, y0=0, x1=None, y1=None): """Update a rectangle of the screen.""" if x1 is None: x1 = self.width + else: + x1 = min(max(1, x1), self.width) if y1 is None: y1 = self.height + else: + y1 = min(max(1, y1), self.height) + if x0 >= x1 or y0 >= y1: + return layers = [l.layer for l in self.layers] - _stage.render(x0, y0, x1, y1, layers, self.buffer, self.display) + _stage.render(x0, y0, x1, y1, layers, self.buffer, + self.display, self.scale) def render_sprites(self, sprites): """Update the spots taken by all the sprites in the list.""" @@ -601,8 +612,8 @@ def render_sprites(self, sprites): y0 = max(0, min(self.height - 1, min(sprite.py, int(sprite.y)))) x1 = max(1, min(self.width, max(sprite.px, int(sprite.x)) + 16)) y1 = max(1, min(self.height, max(sprite.py, int(sprite.y)) + 16)) - if x0 == x1 or y0 == y1: + if x0 >= x1 or y0 >= y1: continue _stage.render(x0, y0, x1, y1, layers, self.buffer, - self.display) + self.display, self.scale) sprite._updated() From 2b9b9f0a80810b0c4ff0feebbbe07312251de144 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Thu, 7 Oct 2021 13:03:18 +0200 Subject: [PATCH 41/59] Add support for Pimoroni PicoSystem --- picosystem/stage.py | 1 + picosystem/ugame.py | 86 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 120000 picosystem/stage.py create mode 100644 picosystem/ugame.py diff --git a/picosystem/stage.py b/picosystem/stage.py new file mode 120000 index 0000000..2dedc93 --- /dev/null +++ b/picosystem/stage.py @@ -0,0 +1 @@ +../stage.py \ No newline at end of file diff --git a/picosystem/ugame.py b/picosystem/ugame.py new file mode 100644 index 0000000..b0b2fd1 --- /dev/null +++ b/picosystem/ugame.py @@ -0,0 +1,86 @@ +import board +import analogio +import stage +import keypad +import audiocore +import audiopwmio +import time +import supervisor + + +K_O = 0x01 # A +K_X = 0x02 # B +K_SELECT = 0x04 # X +K_START = 0x08 # Y +K_Z = 0x08 # Y +K_DOWN = 0x10 +K_LEFT = 0x20 +K_RIGHT = 0x40 +K_UP = 0x80 + + +class _Buttons: + def __init__(self): + self.keys = keypad.Keys(( + board.SW_A, + board.SW_B, + board.SW_X, + board.SW_Y, + board.SW_DOWN, + board.SW_LEFT, + board.SW_RIGHT, + board.SW_UP + ), value_when_pressed=False, pull=True, interval=0.05) + self.last_state = 0 + self.event = keypad.Event(0, False) + self.last_z_press = None + + def get_pressed(self): + buttons = self.last_state + events = self.keys.events + while events: + if events.get_into(self.event): + bit = 1 << self.event.key_number + if self.event.pressed: + buttons |= bit + self.last_state |= bit + else: + self.last_state &= ~bit + if buttons & K_Z: + now = time.monotonic() + if self.last_z_press: + if now - self.last_z_press > 2: + supervisor.set_next_code_file(None) + supervisor.reload() + else: + self.last_z_press = now + else: + self.last_z_press = None + return buttons + +class _Audio: + last_audio = None + + def __init__(self): + self.muted = True + self.buffer = bytearray(128) + self.audio = audiopwmio.PWMAudioOut(board.AUDIO) + + def play(self, audio_file, loop=False): + if self.muted: + return + self.stop() + wave = audiocore.WaveFile(audio_file, self.buffer) + self.audio.play(wave, loop=loop) + + def stop(self): + self.audio.stop() + + def mute(self, value=True): + self.muted = value + + +audio = _Audio() +display = board.DISPLAY +buttons = _Buttons() +battery = analogio.AnalogIn(board.BAT_SENSE) From e531c94f5a45bcc703ba4a40e174089a6c7c3af6 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Fri, 8 Oct 2021 20:36:17 +0200 Subject: [PATCH 42/59] Document scale and GIF16 --- stage.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/stage.py b/stage.py index b391e6c..2dbae7f 100644 --- a/stage.py +++ b/stage.py @@ -262,6 +262,8 @@ def lzw_decode(data, code_size): class GIF16: + """Read 16-color GIF files.""" + def __init__(self, filename): self.filename = filename @@ -564,6 +566,10 @@ class Stage: display connected to the device. The ``fps`` specifies the maximum frame rate to be enforced. + + The ``scale`` specifies an optional scaling up of the display, to use + 2x2 or 3x3, etc. pixels. If not specified, it is inferred from the display + size (displays wider than 256 pixels will have scale=2, for example). """ buffer = bytearray(512) From c8d2c3f2de2e6bcdde46b3b356082f3bf93cabf5 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Sat, 9 Oct 2021 00:02:00 +0200 Subject: [PATCH 43/59] Add support for viewport scrolling --- stage.py | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/stage.py b/stage.py index 2dbae7f..44ab69a 100644 --- a/stage.py +++ b/stage.py @@ -482,10 +482,6 @@ def set_frame(self, frame=None, rotation=None): def update(self): pass - def _updated(self): - self.px = int(self.x) - self.py = int(self.y) - class Text: """Text layer. For displaying text.""" @@ -584,6 +580,8 @@ def __init__(self, display, fps=6, scale=None): self.height = display.height // self.scale self.last_tick = time.monotonic() self.tick_delay = 1 / fps + self.vx = 0 + self.vy = 0 def tick(self): """Wait for the start of the next frame.""" @@ -594,32 +592,39 @@ def tick(self): else: self.last_tick = time.monotonic() - def render_block(self, x0=0, y0=0, x1=None, y1=None): + def render_block(self, x0=None, y0=None, x1=None, y1=None): """Update a rectangle of the screen.""" + if x0 is None: + x0 = self.vx + if y0 is None: + y0 = self.vy if x1 is None: - x1 = self.width - else: - x1 = min(max(1, x1), self.width) + x1 = self.width + self.vx if y1 is None: - y1 = self.height - else: - y1 = min(max(1, y1), self.height) + y1 = self.height + self.vy + x0 = min(max(0, x0 - self.vx), self.width - 1) + y0 = min(max(0, y0 - self.vy), self.height - 1) + x1 = min(max(1, x1 - self.vx), self.width) + y1 = min(max(1, y1 - self.vy), self.height) if x0 >= x1 or y0 >= y1: return layers = [l.layer for l in self.layers] _stage.render(x0, y0, x1, y1, layers, self.buffer, - self.display, self.scale) + self.display, self.scale, self.vx, self.vy) def render_sprites(self, sprites): """Update the spots taken by all the sprites in the list.""" layers = [l.layer for l in self.layers] for sprite in sprites: - x0 = max(0, min(self.width - 1, min(sprite.px, int(sprite.x)))) - y0 = max(0, min(self.height - 1, min(sprite.py, int(sprite.y)))) - x1 = max(1, min(self.width, max(sprite.px, int(sprite.x)) + 16)) - y1 = max(1, min(self.height, max(sprite.py, int(sprite.y)) + 16)) + x = int(sprite.x) - self.vx + y = int(sprite.y) - self.vy + x0 = max(0, min(self.width - 1, min(sprite.px, x))) + y0 = max(0, min(self.height - 1, min(sprite.py, y))) + x1 = max(1, min(self.width, max(sprite.px, x) + 16)) + y1 = max(1, min(self.height, max(sprite.py, y) + 16)) + sprite.px = x + sprite.py = y if x0 >= x1 or y0 >= y1: continue _stage.render(x0, y0, x1, y1, layers, self.buffer, - self.display, self.scale) - sprite._updated() + self.display, self.scale, self.vx, self.vy) From 2c89a329063848bcd7dba6af6fb6c9d6036dd4f2 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Sat, 9 Oct 2021 00:04:17 +0200 Subject: [PATCH 44/59] Add the font generation and examples --- examples/ball/main.py | 38 +++++++++++++++++ examples/rpg/ground.bmp | Bin 0 -> 2234 bytes examples/rpg/main.py | 68 ++++++++++++++++++++++++++++++ examples/rpg/tiles.bmp | Bin 0 -> 2218 bytes font/font2.bmp | Bin 0 -> 4250 bytes font/genfont2.py | 89 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 195 insertions(+) create mode 100644 examples/ball/main.py create mode 100644 examples/rpg/ground.bmp create mode 100644 examples/rpg/main.py create mode 100644 examples/rpg/tiles.bmp create mode 100644 font/font2.bmp create mode 100644 font/genfont2.py diff --git a/examples/ball/main.py b/examples/ball/main.py new file mode 100644 index 0000000..66cc52c --- /dev/null +++ b/examples/ball/main.py @@ -0,0 +1,38 @@ +import ugame +import stage + + +class Ball(stage.Sprite): + def __init__(self, x, y): + super().__init__(bank, 1, x, y) + self.dx = 2 + self.dy = 1 + + def update(self): + super().update() + self.set_frame(self.frame % 4 + 1) + self.move(self.x + self.dx, self.y + self.dy) + if not 0 < self.x < 112: + self.dx = -self.dx + if not 0 < self.y < 112: + self.dy = -self.dy + + +bank = stage.Bank.from_bmp16("ball.bmp") +background = stage.Grid(bank) +text = stage.Text(12, 1) +text.move(16, 60) +text.text("Hello world!") +ball1 = Ball(64, 0) +ball2 = Ball(0, 76) +ball3 = Ball(111, 64) +game = stage.Stage(ugame.display, 12) +sprites = [ball1, ball2, ball3] +game.layers = [text, ball1, ball2, ball3, background] +game.render_block(0, 0, 128, 128) + +while True: + for sprite in sprites: + sprite.update() + game.render_sprites(sprites) + game.tick() diff --git a/examples/rpg/ground.bmp b/examples/rpg/ground.bmp new file mode 100644 index 0000000000000000000000000000000000000000..d7e5f16e59ea3333fc9c8f9fcadda587bfe90424 GIT binary patch literal 2234 zcmeH^F^C&S6o#MfBF;jKPQr-bZc$;UbdVU%T5!O-tj{9{P62_Pc9!9|NyuRr!p0`W zEsNO=_&Q@>slkYGBiJy$Xq)STo3w6KhY(yjaS@XDb}s}BDFW$ykDdMZy*KZDGjFeb zzB~t|E%E&ot~Pl!dOo5r0L-6nR*Q19u3i7)E~Wm9t-ZJT;wP6d@)G`lKjG6GU&EDK z8LWNwJM_Q#5?+;6)emHof{nNd&j*2?BflSMj(m%JZqga^cn}-&LJj8g0XM^C z$`3?87IR~?Vtr*&!$T>K4H{>R#LUzXOO5fPZEZ9U*plzCK3Yk2?0)8-GP?qTB)rRQ!;_t6JuHQhhyx#LsqHWmOBha=}#RJYyQY?1DC zHXYPnZPI}@X?HRyb=)o0?M}v&$4Vgi+uRC|2PoFc7&Iz%R{Q9?a8zo2=V*ic+87T& z@f!R;{EcaOAH=U;#=ba*^8PM7d`ys{ZjHbTdOy;f73a6Q}O{Se#Lb=-7)$2U&`O`9{(?Sl^xc>M+u8w;nI zsng69`DpR?oZT1`VqxX%efP}S^Y=Mt&fL4-lq+Ye@P5MYrnnpW^V&Iw{NoSz-$$_e z-M#&4)8_s%!eGxHv$W99`}U!UJ5>|*(nGs*PJj(OIlWJPUC45KeC_| za#GSekEy{RKw(P;mLmQXK`0D@QYgi+T2ZPcVd)hhk%UUXW(S*6OJK-(%`5)$GA<`M zS_q0|R%}efj(9#64h%J{+z?#FR%k7u=cR<~o2skaL98r!B^40wCzYV4WmE~$3B2|w z*n5@>39qgCFo3YuI3*mH8t&nqSaAaiLBlp!R^cpYy@g+tNbO#7wjKk8_kKvq%Q@4p z^DO(`a#p5vOSO_mI$i*wuBhmwsqj#6ps6kio_GOP+pyhv3L!aq&E#}P_HqowwA-C9 z+vO^r>{UR?m9X^H(X&4*(Y{?7u(HMNo&rIsM6aS!2r(ERBqz|e^F0LPTL=^q!4#%} z`e(*`UDL7*ZpfJ?q&cM*1gAB1f$HGeK~OrJB&A2b-~cbsm9Iuf`SM*n2V|FnW3LAR z4Gx7zT_;fW;qi}&%Z&AHz*HfT`CHu*XZL@RgP0el=ZS#<2-5JoME1yyV;IRe`ec$~r8ykfxy5+DF}tVrc0Ljo_S%Q-E{BhS51bv@;g zrB=7T{`$xM^6R&Git8Kye{%oi?8j+J{&^u^ey+DN?=N3}`%BSht{+}DzfEtk^8New z>Fv!K^S^(erayj}rhlp|=R7axIWPM@FON1dcl|P#@|5#2m!-`4$TQRH@;Nis$K0o!PnKK7@-sQ8~_k8&4 zzl{Gc`XTphxg$Sb<(K~}J{#74w=TN6AGrlTBY!;K;}h`}?5Hm$ zu!(EujFEqy@9}%$HwiwrUv2jsiqVvX$FxvK)@!E^eC2$Y6XDNIXP6|qRU!Y=K>&rH zs!*&6%-Wuei)Y+?@L&n7dTr>(hp*`b(^0p?=)7?tF7c%Ue3;4Dzu^_O<_p{%)N!xT zR>Po+{OY(J%`n&XfK01;=5FMk(xC_Fr}o$NZol^c{a+6HVn6vszv0t{cP4*E{51#7 z8P43zfA*MX-GA#(T&&d`vBr82pn5`28U4dw2Fn9Xp34=-W9ykiqdxv4v*s_~-|1KW z!oD@W#woHE2aw(ET-HNgaddu-U%X!&D+NJCgNK!FA0JLaoEh2ay7&WM^e5guoy5U{ z@&s?A$oY-G;03F_Plo-9fPK{GV=i(GfT%(9jrAvg;G$-D4RGtvvtFuy5A|4g4>$R1 zTth#&w`TQI#{7@i%TWtZ*e7qw zieJROF#lN+;iYfF27Y{NI&i{x>m0tfn2OO_OADoWyy$PT4t1nsr?jOw9qsRX3 zyk8MNW7p4MOw3)6%ZE&V800a(HgrpjA7D+H)Am-k(84UI z|GM(C(7%XVDc0k&zvzqaE$dew9i5Lpc7zdY-b3!|`4N~6*NDB(yW;yVy^jal%aS@chON|5x(!fD;<5V`kp{@A)0Ii;d`C zxBMqt>8AzTiFY_3$u)gj{L!D&w#{kx=8TQIkRa5}G;P~>-)B$K-1%q}`>Huupj8YP zETs*4)}8P3W7)TbZ~0?A+?(fP+t%YeSGgRNre)j8vYmzZPASW|=d_;$(&OS{f8MxG zE}#2)A2o;R={%|Fmg^K`!D@q;p4x50sI*3P5=M^ literal 0 HcmV?d00001 diff --git a/font/genfont2.py b/font/genfont2.py new file mode 100644 index 0000000..158d5e9 --- /dev/null +++ b/font/genfont2.py @@ -0,0 +1,89 @@ +import array +import pprint + + +def color565(r, g, b): + return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3 + + +class BMP16: + """Read 16-color BMP files.""" + + def __init__(self, filename): + self.filename = filename + self.colors = 0 + + def read_header(self): + """Read the file's header information.""" + + if self.colors: + return + with open(self.filename, 'rb') as f: + f.seek(10) + self.data = int.from_bytes(f.read(4), 'little') + f.seek(18) + self.width = int.from_bytes(f.read(4), 'little') + self.height = int.from_bytes(f.read(4), 'little') + f.seek(46) + self.colors = int.from_bytes(f.read(4), 'little') + + def read_palette(self): + """Read the color palette information.""" + + palette = array.array('H', (0 for i in range(16))) + with open(self.filename, 'rb') as f: + f.seek(self.data - self.colors * 4) + for color in range(self.colors): + buffer = f.read(4) + c = color565(buffer[2], buffer[1], buffer[0]) + palette[color] = ((c & 0xff) << 8) | (c >> 8) + return palette + + def read_data(self, offset=0, size=-1): + """Read the image data.""" + + with open(self.filename, 'rb') as f: + f.seek(self.data + offset) + return f.read(size) + + +class Font: + def __init__(self, buffer): + self.buffer = buffer + + @classmethod + def from_bmp16(cls, filename): + bmp = BMP16(filename) + bmp.read_header() + if bmp.width != 8 or bmp.height != 1024: + raise ValueError("A 8x1024 16-color BMP expected!") + data = bmp.read_data() + self = cls(bytearray(2048)) + c = 0 + x = 0 + y = 7 + for b in data: + self.pixel(c, x, y, b >> 4) + x += 1 + self.pixel(c, x, y, b & 0x0f) + x += 1 + if x >= 8: + x = 0 + y -= 1 + if y < 0: + y = 7 + c += 1 + del data + self.palette = bmp.read_palette() + return self + + def pixel(self, c, x, y, color): + index = (127 - c) * 16 + 2 * y + x // 4 + bit = (x % 4) * 2 + color = color & 0x03 + self.buffer[index] |= color << bit + + +font = Font.from_bmp16("font2.bmp") +pprint.pprint(font.buffer) +pprint.pprint(font.palette.tobytes()) From f508d4d0fdb560afbb95ffda36dba12e10f29406 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Fri, 3 Dec 2021 21:18:03 +0100 Subject: [PATCH 45/59] Fix stage for pygamer and pybadge on CP 7.0 Backlight handling is broken in displayio, so we handle it ourselves. The keypad now requires additional parameters. Swapped O and X keys on pybadge. --- pybadge/ugame.py | 14 +++++++++----- pygamer/ugame.py | 10 +++++++--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/pybadge/ugame.py b/pybadge/ugame.py index 8b64efb..d8e6e04 100644 --- a/pybadge/ugame.py +++ b/pybadge/ugame.py @@ -6,10 +6,12 @@ import keypad import audioio import audiocore +import digitalio +import supervisor -K_X = 0x01 -K_O = 0x02 +K_O = 0x01 +K_X = 0x02 K_DOWN = 0x20 K_LEFT = 0x80 K_RIGHT = 0x10 @@ -48,7 +50,7 @@ class _Buttons: def __init__(self): self.keys = keypad.ShiftRegisterKeys(clock=board.BUTTON_CLOCK, data=board.BUTTON_OUT, latch=board.BUTTON_LATCH, key_count=8, - interval=0.05) + interval=0.05, value_when_pressed=True) self.last_state = 0 self.event = keypad.Event(0, False) self.last_z_press = None @@ -111,8 +113,10 @@ def mute(self, value=True): _fourwire = displayio.FourWire(_tft_spi, command=board.TFT_DC, chip_select=board.TFT_CS, reset=board.TFT_RST) display = displayio.Display(_fourwire, _TFT_INIT, width=160, height=128, - rotation=0, backlight_pin=board.TFT_LITE, - auto_refresh=False, auto_brightness=True) + rotation=0, auto_refresh=False) +# Work around broken backlight in CP 7.0 +_backlight = digitalio.DigitalInOut(board.TFT_LITE) +_backlight.switch_to_output(value=1) del _TFT_INIT buttons = _Buttons() audio = _Audio(board.SPEAKER, board.SPEAKER_ENABLE) diff --git a/pygamer/ugame.py b/pygamer/ugame.py index 666710f..931f7f2 100644 --- a/pygamer/ugame.py +++ b/pygamer/ugame.py @@ -7,6 +7,8 @@ import keypad import audioio import audiocore +import supervisor +import digitalio K_X = 0x01 @@ -49,7 +51,7 @@ class _Buttons: def __init__(self): self.keys = keypad.ShiftRegisterKeys(clock=board.BUTTON_CLOCK, data=board.BUTTON_OUT, latch=board.BUTTON_LATCH, key_count=4, - interval=0.05) + interval=0.05, value_when_pressed=True) self.last_state = 0 self.event = keypad.Event(0, False) self.last_z_press = None @@ -125,8 +127,10 @@ def mute(self, value=True): _fourwire = displayio.FourWire(_tft_spi, command=board.TFT_DC, chip_select=board.TFT_CS, reset=board.TFT_RST) display = displayio.Display(_fourwire, _TFT_INIT, width=160, height=128, - rotation=0, backlight_pin=board.TFT_LITE, - auto_refresh=False, auto_brightness=True) + rotation=0, auto_refresh=False) +# Work around broken backlight in CP 7.0 +_backlight = digitalio.DigitalInOut(board.TFT_LITE) +_backlight.switch_to_output(value=1) del _TFT_INIT buttons = _Buttons() audio = _Audio(board.SPEAKER, board.SPEAKER_ENABLE) From 44dfe8dfaae900dc80cf955b1dcabf2e855b7e52 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Tue, 26 Jul 2022 21:16:15 +0200 Subject: [PATCH 46/59] Add makefile for docs --- docs/Makefile | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 docs/Makefile diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..28ee17e --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = Stage +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file From 088d68f5a9772ec3acca5eaccf03a1636d7ff304 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Tue, 26 Jul 2022 21:18:30 +0200 Subject: [PATCH 47/59] Add support for PNG images --- stage.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/stage.py b/stage.py index 44ab69a..828b138 100644 --- a/stage.py +++ b/stage.py @@ -2,6 +2,10 @@ import array import digitalio import struct +try: + import zlib +except ImportError: + pass import _stage @@ -329,6 +333,62 @@ def read_data(self, buffer=None): return buffer +class PNG16: + """Read 16-color PNG files.""" + + def __init__(self, filename): + self.filename = filename + + def read_header(self): + with open(self.filename, 'rb') as f: + magic = f.read(8) + assert magic == b'\x89PNG\r\n\x1a\n' + ( + size, chunk, self.width, self.height, self.depth, self.mode, + self.compression, self.filters, self.interlaced, crc + ) = struct.unpack(">I4sIIBBBBBI", f.read(25)) + assert size == 13 # header length + assert chunk == b'IHDR' + if self.depth != 4 or self.mode != 3 or self.interlaced != 0: + raise ValueError("16-color non-interaced PNG expected") + + def read_palette(self, palette=None): + if palette is None: + palette = array.array('H', (0 for i in range(16))) + with open(self.filename, 'rb') as f: + f.seek(8 + 25) + size, chunk = struct.unpack(">I4s", f.read(8)) + assert chunk == b'PLTE' + for color in range(size // 3): + c = color565(*struct.unpack("BBB", f.read(3))) + palette[color] = ((c << 8) | (c >> 8)) & 0xffff + return palette + + def read_data(self, buffer=None): + data = bytearray() + with open(self.filename, 'rb') as f: + f.seek(8 + 25) + while True: + size, chunk = struct.unpack(">I4s", f.read(8)) + if chunk == b'IEND': + break + elif chunk != b'IDAT': + f.seek(size + 4, 1) + continue + data.extend(f.read(size)) + f.seek(4, 1) # skip CRC + data = zlib.decompress(data) + line_size = self.width >> 1 + if buffer is None: + buffer = bytearray(line_size * self.height) + for line in range(self.height): + a = line * line_size + b = line * (line_size + 1) + assert data[b] == 0 # no filter + buffer[a:a + line_size] = data[b + 1:b + 1 + line_size] + return buffer + + class Bank: """ Store graphics for the tiles and sprites. @@ -355,6 +415,8 @@ def from_image(cls, filename): image = GIF16(filename) elif filename.lower().endswith(".bmp"): image = BMP16(filename) + elif filename.lower().endswith(".png"): + image = PNG16(filename) else: raise ValueError("Unsupported format") image.read_header() From d12ac5ba4feea2bf8bac8835bf120760c6970ee4 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Tue, 26 Jul 2022 22:02:34 +0200 Subject: [PATCH 48/59] Avoid smallint overflow in PNG crc --- stage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stage.py b/stage.py index 828b138..c251836 100644 --- a/stage.py +++ b/stage.py @@ -346,7 +346,7 @@ def read_header(self): ( size, chunk, self.width, self.height, self.depth, self.mode, self.compression, self.filters, self.interlaced, crc - ) = struct.unpack(">I4sIIBBBBBI", f.read(25)) + ) = struct.unpack(">I4sIIBBBBB4s", f.read(25)) assert size == 13 # header length assert chunk == b'IHDR' if self.depth != 4 or self.mode != 3 or self.interlaced != 0: From 3d5f7deaf372d771b4f322083a8f56f1c5ace1eb Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Wed, 27 Jul 2022 09:38:16 +0200 Subject: [PATCH 49/59] Remove GIF support --- stage.py | 156 ++++--------------------------------------------------- 1 file changed, 11 insertions(+), 145 deletions(-) diff --git a/stage.py b/stage.py index c251836..876aaba 100644 --- a/stage.py +++ b/stage.py @@ -1,6 +1,5 @@ import time import array -import digitalio import struct try: import zlib @@ -155,8 +154,6 @@ def __init__(self, filename): self.colors = 0 def read_header(self): - """Read the file's header information.""" - if self.colors: return with open(self.filename, 'rb') as f: @@ -168,10 +165,9 @@ def read_header(self): f.seek(46) self.colors = int.from_bytes(f.read(4), 'little') - def read_palette(self): - """Read the color palette information.""" - - palette = array.array('H', (0 for i in range(16))) + def read_palette(self, palette=None): + if palette is None: + palette = array.array('H', (0 for i in range(16))) with open(self.filename, 'rb') as f: f.seek(self.data - self.colors * 4) for color in range(self.colors): @@ -181,7 +177,6 @@ def read_palette(self): return palette def read_data(self, buffer=None): - """Read the image data.""" line_size = self.width >> 1 if buffer is None: buffer = bytearray(line_size * self.height) @@ -196,143 +191,6 @@ def read_data(self, buffer=None): return buffer -def read_blockstream(f): - while True: - size = f.read(1)[0] - if size == 0: - break - for i in range(size): - yield f.read(1)[0] - - -class EndOfData(Exception): - pass - - -class LZWDict: - def __init__(self, code_size): - self.code_size = code_size - self.clear_code = 1 << code_size - self.end_code = self.clear_code + 1 - self.codes = [] - self.clear() - - def clear(self): - self.last = b'' - self.code_len = self.code_size + 1 - self.codes[:] = [] - - def decode(self, code): - if code == self.clear_code: - self.clear() - return b'' - elif code == self.end_code: - raise EndOfData() - elif code < self.clear_code: - value = bytes([code]) - elif code <= len(self.codes) + self.end_code: - value = self.codes[code - self.end_code - 1] - else: - value = self.last + self.last[0:1] - if self.last: - self.codes.append(self.last + value[0:1]) - if (len(self.codes) + self.end_code + 1 >= 1 << self.code_len and - self.code_len < 12): - self.code_len += 1 - self.last = value - return value - - -def lzw_decode(data, code_size): - dictionary = LZWDict(code_size) - bit = 0 - try: - byte = next(data) - try: - while True: - code = 0 - for i in range(dictionary.code_len): - code |= ((byte >> bit) & 0x01) << i - bit += 1 - if bit >= 8: - bit = 0 - byte = next(data) - yield dictionary.decode(code) - except EndOfData: - while True: - next(data) - except StopIteration: - return - - -class GIF16: - """Read 16-color GIF files.""" - - def __init__(self, filename): - self.filename = filename - - def read_header(self): - with open(self.filename, 'rb') as f: - header = f.read(6) - if header not in {b'GIF87a', b'GIF89a'}: - raise ValueError("Not GIF file") - self.width, self.height, flags, self.background, self.aspect = ( - struct.unpack(' 16: - raise ValueError("Too many colors (%d/16)." % self.palette_size) - - def read_palette(self): - palette = array.array('H', (0 for i in range(16))) - with open(self.filename, 'rb') as f: - f.seek(13) - for color in range(self.palette_size): - buffer = f.read(3) - c = color565(buffer[0], buffer[1], buffer[2]) - palette[color] = ((c << 8) | (c >> 8)) & 0xffff - return palette - - def read_data(self, buffer=None): - line_size = (self.width + 1) >> 1 - if buffer is None: - buffer = bytearray(line_size * self.height) - with open(self.filename, 'rb') as f: - f.seek(13 + self.palette_size * 3) - while True: # skip to first frame - block_type = f.read(1)[0] - if block_type == 0x2c: - break - elif block_type == 0x21: # skip extension - extension_type = f.read(1)[0] - while True: - size = f.read(1)[0] - if size == 0: - break - f.seek(1, size) - elif block_type == 0x3b: - raise NotImplementedError() - x, y, w, h, flags = struct.unpack('> 1) + y * line_size] |= pixel - else: - buffer[(x >> 1) + y * line_size] = pixel << 4 - x += 1 - if (x >= self.width): - x = 0 - y += 1 - return buffer - - class PNG16: """Read 16-color PNG files.""" @@ -564,6 +422,7 @@ def __init__(self, width, height, font=None, palette=None, buffer=None): def char(self, x, y, c=None, hightlight=False): """Get or set the character at the given location.""" + if not 0 <= x < self.width or not 0 <= y < self.height: return if c is None: @@ -575,6 +434,7 @@ def char(self, x, y, c=None, hightlight=False): def move(self, x, y, z=None): """Shift the whole layer respective to the screen.""" + self.x = x self.y = y if z is not None: @@ -583,6 +443,7 @@ def move(self, x, y, z=None): def cursor(self, x=None, y=None): """Move the text cursor to the specified row and column.""" + if y is not None: self.row = min(max(0, y), self.width - 1) if x is not None: @@ -593,6 +454,7 @@ def text(self, text, hightlight=False): Display text starting at the current cursor location. Return the dimensions of the rendered text. """ + longest = 0 tallest = 0 for c in text: @@ -612,6 +474,7 @@ def text(self, text, hightlight=False): def clear(self): """Clear all text from the layer.""" + for i in range(self.width * self.height): self.buffer[i] = 0 @@ -647,6 +510,7 @@ def __init__(self, display, fps=6, scale=None): def tick(self): """Wait for the start of the next frame.""" + self.last_tick += self.tick_delay wait = max(0, self.last_tick - time.monotonic()) if wait: @@ -656,6 +520,7 @@ def tick(self): def render_block(self, x0=None, y0=None, x1=None, y1=None): """Update a rectangle of the screen.""" + if x0 is None: x0 = self.vx if y0 is None: @@ -676,6 +541,7 @@ def render_block(self, x0=None, y0=None, x1=None, y1=None): def render_sprites(self, sprites): """Update the spots taken by all the sprites in the list.""" + layers = [l.layer for l in self.layers] for sprite in sprites: x = int(sprite.x) - self.vx From 0455f1dad60db7e3ae65aacea4ee5b64b58e145f Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Wed, 10 Aug 2022 13:26:23 -0400 Subject: [PATCH 50/59] Remove auto_brightness references --- itsybitsy_m4_express/ugame.py | 1 - meowbit/ugame.py | 1 - 2 files changed, 2 deletions(-) diff --git a/itsybitsy_m4_express/ugame.py b/itsybitsy_m4_express/ugame.py index 20d4d05..0ba32b4 100644 --- a/itsybitsy_m4_express/ugame.py +++ b/itsybitsy_m4_express/ugame.py @@ -66,7 +66,6 @@ def mute(self, mute): chip_select=board.A2, reset=board.A4) display = displayio.Display(_fourwire, _INIT_SEQUENCE, width=160, height=128, rotation=0, backlight_pin=board.A5) -display.auto_brightness = True buttons = gamepad.GamePad( digitalio.DigitalInOut(board.SCL), digitalio.DigitalInOut(board.D12), diff --git a/meowbit/ugame.py b/meowbit/ugame.py index 62dc378..3e08bd4 100644 --- a/meowbit/ugame.py +++ b/meowbit/ugame.py @@ -15,7 +15,6 @@ K_Z = 0x40 display = board.DISPLAY -display.auto_brightness = True display.auto_refresh = False From 52aaa5a2833271a89460b8a5264dc6d16ce4ef91 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Wed, 27 Jul 2022 10:33:54 +0200 Subject: [PATCH 51/59] Completely remove gif support --- stage.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/stage.py b/stage.py index 876aaba..10629cc 100644 --- a/stage.py +++ b/stage.py @@ -269,9 +269,7 @@ def from_bmp16(cls, filename): @classmethod def from_image(cls, filename): """Read the bank from an image file.""" - if filename.lower().endswith(".gif"): - image = GIF16(filename) - elif filename.lower().endswith(".bmp"): + if filename.lower().endswith(".bmp"): image = BMP16(filename) elif filename.lower().endswith(".png"): image = PNG16(filename) From 6196bf9961d6cab751b2ef52715717fd313981c8 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Wed, 27 Jul 2022 15:49:13 +0200 Subject: [PATCH 52/59] Support also 8-bit PNG images in Stage As long as only 16 colors are used --- stage.py | 44 +++++++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/stage.py b/stage.py index 10629cc..9537aef 100644 --- a/stage.py +++ b/stage.py @@ -177,7 +177,7 @@ def read_palette(self, palette=None): return palette def read_data(self, buffer=None): - line_size = self.width >> 1 + line_size = (self.width + 1 ) >> 1 if buffer is None: buffer = bytearray(line_size * self.height) @@ -207,7 +207,7 @@ def read_header(self): ) = struct.unpack(">I4sIIBBBBB4s", f.read(25)) assert size == 13 # header length assert chunk == b'IHDR' - if self.depth != 4 or self.mode != 3 or self.interlaced != 0: + if self.depth not in {4, 8} or self.mode != 3 or self.interlaced != 0: raise ValueError("16-color non-interaced PNG expected") def read_palette(self, palette=None): @@ -215,9 +215,15 @@ def read_palette(self, palette=None): palette = array.array('H', (0 for i in range(16))) with open(self.filename, 'rb') as f: f.seek(8 + 25) - size, chunk = struct.unpack(">I4s", f.read(8)) - assert chunk == b'PLTE' - for color in range(size // 3): + while True: + size, chunk = struct.unpack(">I4s", f.read(8)) + if chunk == b'PLTE': + break + f.seek(size + 4, 1) + colors = size // 3 + if colors > 16: + raise ValueError("16-color PNG expected") + for color in range(colors): c = color565(*struct.unpack("BBB", f.read(3))) palette[color] = ((c << 8) | (c >> 8)) & 0xffff return palette @@ -236,14 +242,30 @@ def read_data(self, buffer=None): data.extend(f.read(size)) f.seek(4, 1) # skip CRC data = zlib.decompress(data) - line_size = self.width >> 1 + line_size = (self.width + 1) >> 1 if buffer is None: buffer = bytearray(line_size * self.height) - for line in range(self.height): - a = line * line_size - b = line * (line_size + 1) - assert data[b] == 0 # no filter - buffer[a:a + line_size] = data[b + 1:b + 1 + line_size] + if self.depth == 4: + for line in range(self.height): + a = line * line_size + b = line * (line_size + 1) + assert data[b] == 0 # no filter + buffer[a:a + line_size] = data[b + 1:b + 1 + line_size] + elif self.depth == 8: + for line in range(self.height): + a = line * line_size + b = line * (self.width + 1) + assert data[b] == 0 # no filter + b += 1 + for col in range(line_size): + buffer[a] = (data[b] & 0x0f) << 4 + b += 1 + try: + buffer[a] |= data[b] & 0x0f + except IndexError: + pass + b += 1 + a += 1 return buffer From 9a8338b3bdaeac9eeb5b74d147107c67db33fdac Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Wed, 10 Aug 2022 19:40:04 +0200 Subject: [PATCH 53/59] Add a PNG conversion script --- png16.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 png16.py diff --git a/png16.py b/png16.py new file mode 100644 index 0000000..d196110 --- /dev/null +++ b/png16.py @@ -0,0 +1,14 @@ +""" +Converts images to the 4-bit PNG format required by Stage. +""" + +import sys +from PIL import Image + + +filename = sys.argv[1] +image = Image.open(filename) +image = image.convert(mode='P', dither=Image.Dither.NONE, + palette=Image.Palette.ADAPTIVE, colors=16) +filename = filename.rsplit('.', 1)[0] + '.png' +image.save(filename, 'png', bits=4) From 3020d92409c95b9cd261e089944946e615ae8556 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Thu, 25 Aug 2022 17:50:40 +0200 Subject: [PATCH 54/59] Add support for PewPew Pi --- pew.py | 203 ++++++++++++++++++++++++++++++++++++++++++++ pewpew_m4/pew.py | 204 +-------------------------------------------- pewpew_pi/pew.py | 1 + pewpew_pi/stage.py | 1 + pewpew_pi/ugame.py | 80 ++++++++++++++++++ 5 files changed, 286 insertions(+), 203 deletions(-) create mode 100644 pew.py mode change 100644 => 120000 pewpew_m4/pew.py create mode 120000 pewpew_pi/pew.py create mode 120000 pewpew_pi/stage.py create mode 100644 pewpew_pi/ugame.py diff --git a/pew.py b/pew.py new file mode 100644 index 0000000..19e3937 --- /dev/null +++ b/pew.py @@ -0,0 +1,203 @@ +from micropython import const +import board +import busio +import digitalio +import time +import ugame +import stage +import array + + +_FONT = ( + b'{{{{{{wws{w{HY{{{{YDYDY{sUtGUsH[wyH{uHgHE{ws{{{{vyxyv{g[K[g{{]f]{{{wDw{{' + b'{{{wy{{{D{{{{{{{w{K_w}x{VHLHe{wuwww{`KfyD{UKgKU{w}XDK{DxTKT{VxUHU{D[wyx{' + b'UHfHU{UHEKe{{w{w{{{w{wy{KwxwK{{D{D{{xwKwx{eKg{w{VIHyB{fYH@H{dHdHd{FyxyF{' + b'`XHX`{DxtxD{Dxtxx{FyxIF{HHDHH{wwwww{KKKHU{HXpXH{xxxxD{Y@DLH{IL@LX{fYHYf{' + b'`HH`x{fYHIF{`HH`H{UxUKU{Dwwww{HHHIR{HHH]w{HHLD@{HYsYH{HYbww{D[wyD{txxxt{' + b'x}w_K{GKKKG{wLY{{{{{{{{Dxs{{{{{BIIB{x`XX`{{ByyB{KBIIB{{WIpF{OwUwww{`YB[`' + b'x`XHH{w{vwc{K{OKHUxHpXH{vwws_{{dD@H{{`XHH{{fYYf{{`XX`x{bYIBK{Ipxx{{F}_d{' + b'wUws_{{HHIV{{HH]s{{HLD@{{HbbH{{HHV[a{D_}D{Cw|wC{wwwwwwpwOwp{WKfxu{@YYY@{' +) +_SALT = const(132) + +_PALETTE = array.array('H', (0x0, 0x4a29, 0x6004, 0xf8, 0xfd, 0xf42, 0x825b, + 0xf8, 0xfe, 0x125b, 0xcffb, 0xe0cf, 0xffff, + 0x1ff8, 0xdbff, 0xffff)) + +K_X = ugame.K_X +K_DOWN = ugame.K_DOWN +K_LEFT = ugame.K_LEFT +K_RIGHT = ugame.K_RIGHT +K_UP = ugame.K_UP +K_O = ugame.K_O + +_tick = None +_display = None + + +def brightness(level): + pass + + +def show(pix): + for y in range(8): + for x in range(8): + _grid.tile(x + 1, y, 1 + (pix.pixel(x, y) & 0x03)) + _game.render_block(16, 0, 144, 128) + +keys = ugame.buttons.get_pressed + + +def tick(delay): + global _tick + + now = time.monotonic() + _tick += delay + if _tick < now: + _tick = now + else: + time.sleep(_tick - now) + + +class GameOver(SystemExit): + pass + + +class Pix: + __slots__ = ('buffer', 'width', 'height') + + def __init__(self, width=8, height=8, buffer=None): + if buffer is None: + buffer = bytearray(width * height) + self.buffer = buffer + self.width = width + self.height = height + + @classmethod + def from_text(cls, string, color=None, bgcolor=0, colors=None): + pix = cls(4 * len(string), 6) + font = memoryview(_FONT) + if colors is None: + if color is None: + colors = (3, 2, bgcolor, bgcolor) + else: + colors = (color, color, bgcolor, bgcolor) + x = 0 + for c in string: + index = ord(c) - 0x20 + if not 0 <= index <= 95: + continue + row = 0 + for byte in font[index * 6:index * 6 + 6]: + unsalted = byte ^ _SALT + for col in range(4): + pix.pixel(x + col, row, colors[unsalted & 0x03]) + unsalted >>= 2 + row += 1 + x += 4 + return pix + + @classmethod + def from_iter(cls, lines): + pix = cls(len(lines[0]), len(lines)) + y = 0 + for line in lines: + x = 0 + for pixel in line: + pix.pixel(x, y, pixel) + x += 1 + y += 1 + return pix + + def pixel(self, x, y, color=None): + if not 0 <= x < self.width or not 0 <= y < self.height: + return 0 + if color is None: + return self.buffer[x + y * self.width] + self.buffer[x + y * self.width] = color + + def box(self, color, x=0, y=0, width=None, height=None): + x = min(max(x, 0), self.width - 1) + y = min(max(y, 0), self.height - 1) + width = max(0, min(width or self.width, self.width - x)) + height = max(0, min(height or self.height, self.height - y)) + for y in range(y, y + height): + xx = y * self.width + x + for i in range(width): + self.buffer[xx] = color + xx += 1 + + def blit(self, source, dx=0, dy=0, x=0, y=0, + width=None, height=None, key=None): + if dx < 0: + x -= dx + dx = 0 + if x < 0: + dx -= x + x = 0 + if dy < 0: + y -= dy + dy = 0 + if y < 0: + dy -= y + y = 0 + width = min(min(width or source.width, source.width - x), + self.width - dx) + height = min(min(height or source.height, source.height - y), + self.height - dy) + source_buffer = memoryview(source.buffer) + self_buffer = self.buffer + if key is None: + for row in range(height): + xx = y * source.width + x + dxx = dy * self.width + dx + self_buffer[dxx:dxx + width] = source_buffer[xx:xx + width] + y += 1 + dy += 1 + else: + for row in range(height): + xx = y * source.width + x + dxx = dy * self.width + dx + for col in range(width): + color = source_buffer[xx] + if color != key: + self_buffer[dxx] = color + dxx += 1 + xx += 1 + y += 1 + dy += 1 + + def __str__(self): + return "\n".join( + "".join( + ('.', '+', '*', '@')[self.pixel(x, y)] + for x in range(self.width) + ) + for y in range(self.height) + ) + + +def init(): + global _tick, _display, _bitmap, _grid, _game + + if _tick is not None: + return + + _tick = time.monotonic() + + _game = stage.Stage(ugame.display, 12) + _bank = bytearray(2048) + for c in range(16): + for y in range(0, 15): + for x in range(0, 7): + _bank[c * 128 + y * 8 + x] = c | c << 4 + _bank[c * 128 + y * 8 + 7] = c << 4 + _bank[c * 128] = c + _bank[c * 128 + 7] = 0 + _bank[c * 128 + 14 * 8] = c + _bank[c * 128 + 14 * 8 + 7] = 0 + tiles = stage.Bank(_bank, _PALETTE) + _grid = stage.Grid(tiles, 10, 8) + _grid.move(0, 0) + _game.layers = [_grid] + _game.render_block() diff --git a/pewpew_m4/pew.py b/pewpew_m4/pew.py deleted file mode 100644 index 19e3937..0000000 --- a/pewpew_m4/pew.py +++ /dev/null @@ -1,203 +0,0 @@ -from micropython import const -import board -import busio -import digitalio -import time -import ugame -import stage -import array - - -_FONT = ( - b'{{{{{{wws{w{HY{{{{YDYDY{sUtGUsH[wyH{uHgHE{ws{{{{vyxyv{g[K[g{{]f]{{{wDw{{' - b'{{{wy{{{D{{{{{{{w{K_w}x{VHLHe{wuwww{`KfyD{UKgKU{w}XDK{DxTKT{VxUHU{D[wyx{' - b'UHfHU{UHEKe{{w{w{{{w{wy{KwxwK{{D{D{{xwKwx{eKg{w{VIHyB{fYH@H{dHdHd{FyxyF{' - b'`XHX`{DxtxD{Dxtxx{FyxIF{HHDHH{wwwww{KKKHU{HXpXH{xxxxD{Y@DLH{IL@LX{fYHYf{' - b'`HH`x{fYHIF{`HH`H{UxUKU{Dwwww{HHHIR{HHH]w{HHLD@{HYsYH{HYbww{D[wyD{txxxt{' - b'x}w_K{GKKKG{wLY{{{{{{{{Dxs{{{{{BIIB{x`XX`{{ByyB{KBIIB{{WIpF{OwUwww{`YB[`' - b'x`XHH{w{vwc{K{OKHUxHpXH{vwws_{{dD@H{{`XHH{{fYYf{{`XX`x{bYIBK{Ipxx{{F}_d{' - b'wUws_{{HHIV{{HH]s{{HLD@{{HbbH{{HHV[a{D_}D{Cw|wC{wwwwwwpwOwp{WKfxu{@YYY@{' -) -_SALT = const(132) - -_PALETTE = array.array('H', (0x0, 0x4a29, 0x6004, 0xf8, 0xfd, 0xf42, 0x825b, - 0xf8, 0xfe, 0x125b, 0xcffb, 0xe0cf, 0xffff, - 0x1ff8, 0xdbff, 0xffff)) - -K_X = ugame.K_X -K_DOWN = ugame.K_DOWN -K_LEFT = ugame.K_LEFT -K_RIGHT = ugame.K_RIGHT -K_UP = ugame.K_UP -K_O = ugame.K_O - -_tick = None -_display = None - - -def brightness(level): - pass - - -def show(pix): - for y in range(8): - for x in range(8): - _grid.tile(x + 1, y, 1 + (pix.pixel(x, y) & 0x03)) - _game.render_block(16, 0, 144, 128) - -keys = ugame.buttons.get_pressed - - -def tick(delay): - global _tick - - now = time.monotonic() - _tick += delay - if _tick < now: - _tick = now - else: - time.sleep(_tick - now) - - -class GameOver(SystemExit): - pass - - -class Pix: - __slots__ = ('buffer', 'width', 'height') - - def __init__(self, width=8, height=8, buffer=None): - if buffer is None: - buffer = bytearray(width * height) - self.buffer = buffer - self.width = width - self.height = height - - @classmethod - def from_text(cls, string, color=None, bgcolor=0, colors=None): - pix = cls(4 * len(string), 6) - font = memoryview(_FONT) - if colors is None: - if color is None: - colors = (3, 2, bgcolor, bgcolor) - else: - colors = (color, color, bgcolor, bgcolor) - x = 0 - for c in string: - index = ord(c) - 0x20 - if not 0 <= index <= 95: - continue - row = 0 - for byte in font[index * 6:index * 6 + 6]: - unsalted = byte ^ _SALT - for col in range(4): - pix.pixel(x + col, row, colors[unsalted & 0x03]) - unsalted >>= 2 - row += 1 - x += 4 - return pix - - @classmethod - def from_iter(cls, lines): - pix = cls(len(lines[0]), len(lines)) - y = 0 - for line in lines: - x = 0 - for pixel in line: - pix.pixel(x, y, pixel) - x += 1 - y += 1 - return pix - - def pixel(self, x, y, color=None): - if not 0 <= x < self.width or not 0 <= y < self.height: - return 0 - if color is None: - return self.buffer[x + y * self.width] - self.buffer[x + y * self.width] = color - - def box(self, color, x=0, y=0, width=None, height=None): - x = min(max(x, 0), self.width - 1) - y = min(max(y, 0), self.height - 1) - width = max(0, min(width or self.width, self.width - x)) - height = max(0, min(height or self.height, self.height - y)) - for y in range(y, y + height): - xx = y * self.width + x - for i in range(width): - self.buffer[xx] = color - xx += 1 - - def blit(self, source, dx=0, dy=0, x=0, y=0, - width=None, height=None, key=None): - if dx < 0: - x -= dx - dx = 0 - if x < 0: - dx -= x - x = 0 - if dy < 0: - y -= dy - dy = 0 - if y < 0: - dy -= y - y = 0 - width = min(min(width or source.width, source.width - x), - self.width - dx) - height = min(min(height or source.height, source.height - y), - self.height - dy) - source_buffer = memoryview(source.buffer) - self_buffer = self.buffer - if key is None: - for row in range(height): - xx = y * source.width + x - dxx = dy * self.width + dx - self_buffer[dxx:dxx + width] = source_buffer[xx:xx + width] - y += 1 - dy += 1 - else: - for row in range(height): - xx = y * source.width + x - dxx = dy * self.width + dx - for col in range(width): - color = source_buffer[xx] - if color != key: - self_buffer[dxx] = color - dxx += 1 - xx += 1 - y += 1 - dy += 1 - - def __str__(self): - return "\n".join( - "".join( - ('.', '+', '*', '@')[self.pixel(x, y)] - for x in range(self.width) - ) - for y in range(self.height) - ) - - -def init(): - global _tick, _display, _bitmap, _grid, _game - - if _tick is not None: - return - - _tick = time.monotonic() - - _game = stage.Stage(ugame.display, 12) - _bank = bytearray(2048) - for c in range(16): - for y in range(0, 15): - for x in range(0, 7): - _bank[c * 128 + y * 8 + x] = c | c << 4 - _bank[c * 128 + y * 8 + 7] = c << 4 - _bank[c * 128] = c - _bank[c * 128 + 7] = 0 - _bank[c * 128 + 14 * 8] = c - _bank[c * 128 + 14 * 8 + 7] = 0 - tiles = stage.Bank(_bank, _PALETTE) - _grid = stage.Grid(tiles, 10, 8) - _grid.move(0, 0) - _game.layers = [_grid] - _game.render_block() diff --git a/pewpew_m4/pew.py b/pewpew_m4/pew.py new file mode 120000 index 0000000..def1b96 --- /dev/null +++ b/pewpew_m4/pew.py @@ -0,0 +1 @@ +../pew.py \ No newline at end of file diff --git a/pewpew_pi/pew.py b/pewpew_pi/pew.py new file mode 120000 index 0000000..def1b96 --- /dev/null +++ b/pewpew_pi/pew.py @@ -0,0 +1 @@ +../pew.py \ No newline at end of file diff --git a/pewpew_pi/stage.py b/pewpew_pi/stage.py new file mode 120000 index 0000000..2dedc93 --- /dev/null +++ b/pewpew_pi/stage.py @@ -0,0 +1 @@ +../stage.py \ No newline at end of file diff --git a/pewpew_pi/ugame.py b/pewpew_pi/ugame.py new file mode 100644 index 0000000..ebda30d --- /dev/null +++ b/pewpew_pi/ugame.py @@ -0,0 +1,80 @@ +import board +import supervisor +import time +import keypad +import audiocore +import audiopwmio +import displayio +import busio + + +K_X = 0x01 +K_DOWN = 0x02 +K_LEFT = 0x04 +K_RIGHT = 0x08 +K_UP = 0x10 +K_O = 0x20 +K_START = 0x40 +K_Z = 0x40 +K_SELECT = 0x80 + + +class _Buttons: + def __init__(self): + self.keys = keypad.Keys((board.BUTTON_X, board.BUTTON_DOWN, + board.BUTTON_LEFT, board.BUTTON_RIGHT, board.BUTTON_UP, + board.BUTTON_O, board.BUTTON_Z), value_when_pressed=False, + interval=0.05) + self.last_state = 0 + self.event = keypad.Event(0, False) + self.last_z_press = None + + def get_pressed(self): + buttons = self.last_state + events = self.keys.events + while events: + if events.get_into(self.event): + bit = 1 << self.event.key_number + if self.event.pressed: + buttons |= bit + self.last_state |= bit + else: + self.last_state &= ~bit + if buttons & K_Z: + now = time.monotonic() + if self.last_z_press: + if now - self.last_z_press > 2: + supervisor.set_next_code_file(None) + supervisor.reload() + else: + self.last_z_press = now + else: + self.last_z_press = None + return buttons + + +class _Audio: + last_audio = None + + def __init__(self): + self.muted = True + self.buffer = bytearray(128) + self.audio = audiopwmio.PWMAudioOut(board.BUZZER) + + def play(self, audio_file, loop=False): + if self.muted: + return + self.stop() + wave = audiocore.WaveFile(audio_file, self.buffer) + self.audio.play(wave, loop=loop) + + def stop(self): + self.audio.stop() + + def mute(self, value=True): + self.muted = value + + +display = board.DISPLAY +buttons = _Buttons() +audio = _Audio() From 4124dfbdaadce1966f457d7d6c6984e9832999bf Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Fri, 26 Aug 2022 17:03:41 +0200 Subject: [PATCH 55/59] Fix ugame.py for the ugame10 console --- ugame10/ugame.py | 69 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/ugame10/ugame.py b/ugame10/ugame.py index d803dea..307192b 100644 --- a/ugame10/ugame.py +++ b/ugame10/ugame.py @@ -1,13 +1,10 @@ -""" -A helper module that initializes the display and buttons for the uGame -game console. See https://hackaday.io/project/27629-game -""" - import board import digitalio import analogio -import gamepad +import keypad import stage +import audioio +import audiocore K_X = 0x01 @@ -20,14 +17,56 @@ K_SELECT = 0x00 +class _Buttons: + def __init__(self): + self.keys = keypad.Keys((board.X, board.DOWN, + board.LEFT, board.RIGHT, board.UP, + board.O), value_when_pressed=False, + interval=0.05) + self.last_state = 0 + self.event = keypad.Event(0, False) + self.last_z_press = None + + def get_pressed(self): + buttons = self.last_state + events = self.keys.events + while events: + if events.get_into(self.event): + bit = 1 << self.event.key_number + if self.event.pressed: + buttons |= bit + self.last_state |= bit + else: + self.last_state &= ~bit + return buttons + + +class _Audio: + last_audio = None + + def __init__(self, speaker_pin, mute_pin): + self.muted = True + self.buffer = bytearray(256) + self.audio = audioio.AudioOut(speaker_pin) + self.mute_pin = digitalio.DigitalInOut(mute_pin) + self.mute_pin.switch_to_output(value=False) + + def play(self, audio_file, loop=False): + if self.muted: + return + self.stop() + wave = audiocore.WaveFile(audio_file, self.buffer) + self.audio.play(wave, loop=loop) + + def stop(self): + self.audio.stop() + + def mute(self, value=True): + self.muted = value + self.mute_pin.value = not value + + display = board.DISPLAY -buttons = gamepad.GamePad( - digitalio.DigitalInOut(board.X), - digitalio.DigitalInOut(board.DOWN), - digitalio.DigitalInOut(board.LEFT), - digitalio.DigitalInOut(board.RIGHT), - digitalio.DigitalInOut(board.UP), - digitalio.DigitalInOut(board.O), -) -audio = stage.Audio(board.SPEAKER, board.MUTE) +audio = _Audio(board.SPEAKER, board.MUTE) +buttons = _Buttons() battery = analogio.AnalogIn(board.BATTERY) From d3c1b935697d6fce76c99469a729e2bd61c866b3 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Sun, 20 Aug 2023 13:38:33 +0200 Subject: [PATCH 56/59] Disable displayio when initializing display --- stage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/stage.py b/stage.py index 9537aef..4bac67a 100644 --- a/stage.py +++ b/stage.py @@ -521,6 +521,7 @@ def __init__(self, display, fps=6, scale=None): self.scale = scale self.layers = [] self.display = display + display.root_group = None self.width = display.width // self.scale self.height = display.height // self.scale self.last_tick = time.monotonic() From 25e35a8620bfab08ff4ec30bf89667dca0b05fcf Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Sun, 20 Aug 2023 14:06:28 +0200 Subject: [PATCH 57/59] Update and rename ugame22 to version 2.2 --- {pewpew_pi => ugame22}/pew.py | 0 {pewpew_pi => ugame22}/stage.py | 0 {pewpew_pi => ugame22}/ugame.py | 22 ++++++++++++++++------ 3 files changed, 16 insertions(+), 6 deletions(-) rename {pewpew_pi => ugame22}/pew.py (100%) rename {pewpew_pi => ugame22}/stage.py (100%) rename {pewpew_pi => ugame22}/ugame.py (82%) diff --git a/pewpew_pi/pew.py b/ugame22/pew.py similarity index 100% rename from pewpew_pi/pew.py rename to ugame22/pew.py diff --git a/pewpew_pi/stage.py b/ugame22/stage.py similarity index 100% rename from pewpew_pi/stage.py rename to ugame22/stage.py diff --git a/pewpew_pi/ugame.py b/ugame22/ugame.py similarity index 82% rename from pewpew_pi/ugame.py rename to ugame22/ugame.py index ebda30d..7e3f49a 100644 --- a/pewpew_pi/ugame.py +++ b/ugame22/ugame.py @@ -1,11 +1,13 @@ +import audiobusio +import audiocore import board +import busio +import digitalio +import displayio +import keypad +import os import supervisor import time -import keypad -import audiocore -import audiopwmio -import displayio -import busio K_X = 0x01 @@ -44,6 +46,7 @@ def get_pressed(self): now = time.monotonic() if self.last_z_press: if now - self.last_z_press > 2: + os.chdir('/') supervisor.set_next_code_file(None) supervisor.reload() else: @@ -59,7 +62,14 @@ class _Audio: def __init__(self): self.muted = True self.buffer = bytearray(128) - self.audio = audiopwmio.PWMAudioOut(board.BUZZER) + self.audio = audiobusio.I2SOut( + board.I2S_BCLK, + board.I2S_LRCLK, + board.I2S_DIN, + ) + self.gain_pin= digitalio.DigitalInOut(board.GAIN) + self.gain_pin.pull=digitalio.Pull.UP # 2dB gain + #self.gain_pin.switch_to_output(value=True) # 6dB gain def play(self, audio_file, loop=False): if self.muted: From 710f95108769bc6aac122f780f2c537fab5279e2 Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Mon, 22 Jul 2024 09:28:30 +0200 Subject: [PATCH 58/59] Add .readthedocs.yaml file --- .readthedocs.yaml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..dd2aa46 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,35 @@ +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.12" + # You can also specify other tool versions: + # nodejs: "20" + # rust: "1.70" + # golang: "1.20" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs + # builder: "dirhtml" + # Fail on all warnings to avoid broken references + # fail_on_warning: true + +# Optionally build your docs in additional formats such as PDF and ePub +# formats: +# - pdf +# - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +# python: +# install: +# - requirements: docs/requirements.txt From 9e5c046201ef5da8b1bacfaf2ce828a8037eaccb Mon Sep 17 00:00:00 2001 From: Radomir Dopieralski Date: Mon, 22 Jul 2024 09:47:08 +0200 Subject: [PATCH 59/59] Ensure index.html in docs --- docs/conf.py | 4 ++-- docs/index.rst | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 120000 docs/index.rst diff --git a/docs/conf.py b/docs/conf.py index 585fe6b..53c4331 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,7 @@ source_suffix = '.rst' # The master toctree document. -master_doc = 'README' +master_doc = 'index' # General information about the project. project = u'Stage' @@ -48,7 +48,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. diff --git a/docs/index.rst b/docs/index.rst new file mode 120000 index 0000000..89a0106 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1 @@ +../README.rst \ No newline at end of file