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 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 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 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 0000000..d7e5f16 Binary files /dev/null and b/examples/rpg/ground.bmp differ diff --git a/examples/rpg/main.py b/examples/rpg/main.py new file mode 100644 index 0000000..37dfd87 --- /dev/null +++ b/examples/rpg/main.py @@ -0,0 +1,68 @@ +import random +import ugame +import stage + + +g = stage.Bank.from_bmp16("ground.bmp") +b = stage.Bank.from_bmp16("tiles.bmp") +l1 = stage.Grid(g) +l0 = stage.Grid(b, 10, 9) +l0.tile(0, 0, 13) +l0.move(-8, -8) +for y in range(8): + for x in range(8): + l1.tile(x, y, random.randint(0, 4)) +for y in range(9): + for x in range(9): + t = 0 + bit = 1 + for dx in (0, -1): + for dy in (-1, 0): + if l1.tile(x + dx, y + dy) == 4: + t |= bit + bit <<= 1 + l0.tile(x, y, 15 - t) +p = stage.Sprite(g, 15, 10, 10) +t = stage.Text(14, 14) +t.move(8, 8) +t.text("Hello world!") + +game = stage.Stage(ugame.display, 12) +sprites = [ + stage.Sprite(g, 15, 60, 50), + stage.Sprite(g, 15, 70, 60), + stage.Sprite(g, 15, 80, 70), + stage.Sprite(g, 15, 90, 80), + stage.Sprite(g, 15, 100, 90), + p, +] +game.layers = [t, l0] + sprites + [l1] +game.render(0, 0, 128, 128) + +frame = 0 +while True: + frame = (frame + 1) % 8 + keys = ugame.buttons.get_pressed() + if keys & ugame.K_RIGHT: + p.move(p.x, p.y + 2) + p.set_frame(12 + frame // 4, 0) + elif keys & ugame.K_LEFT: + p.move(p.x, p.y - 2) + p.set_frame(12 + frame // 4, 4) + elif keys & ugame.K_UP: + p.move(p.x + 2, p.y) + p.set_frame(14, (frame // 4) * 4) + elif keys & ugame.K_DOWN: + p.move(p.x - 2, p.y) + p.set_frame(15, (frame // 4) * 4) + else: + p.set_frame(15, (frame // 4) * 4) + for sprite in sprites: + if sprite != p: + sprite.set_frame(15, (frame // 4) * 4) + x0 = min(sprite.px, sprite.x) + y0 = min(sprite.py, sprite.y) + x1 = max(sprite.px, sprite.x) + 16 + y1 = max(sprite.py, sprite.y) + 16 + game.render(x0, y0, x1, y1) + game.tick() diff --git a/examples/rpg/tiles.bmp b/examples/rpg/tiles.bmp new file mode 100644 index 0000000..0e50ae3 Binary files /dev/null and b/examples/rpg/tiles.bmp differ 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/font/font.bmp b/font/font.bmp index 2051419..c576e8b 100644 Binary files a/font/font.bmp and b/font/font.bmp differ diff --git a/font/font2.bmp b/font/font2.bmp new file mode 100644 index 0000000..7d41ca6 Binary files /dev/null and b/font/font2.bmp differ 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()) diff --git a/itsybitsy_m4_express/ugame.py b/itsybitsy_m4_express/ugame.py index 3193841..0ba32b4 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 @@ -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/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..3e08bd4 --- /dev/null +++ b/meowbit/ugame.py @@ -0,0 +1,77 @@ +import board +import stage +import busio +import time +import keypad +import audiocore + + +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_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 _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): + self.audio.stop() + + def mute(self, value=True): + self.muted = value + + +audio = _Audio() +buttons = _Buttons() 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 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_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..f92a6e5 --- /dev/null +++ b/pewpew_m4/ugame.py @@ -0,0 +1,79 @@ +import board +import stage +import supervisor +import time +import keypad +import audioio +import audiocore + + +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, 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 = _Audio(board.SPEAKER) 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) 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) diff --git a/pybadge/ugame.py b/pybadge/ugame.py index 8306f45..d8e6e04 100644 --- a/pybadge/ugame.py +++ b/pybadge/ugame.py @@ -1,23 +1,21 @@ -""" -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 +import audioio +import audiocore +import digitalio +import supervisor +K_O = 0x01 K_X = 0x02 K_DOWN = 0x20 K_LEFT = 0x80 K_RIGHT = 0x10 K_UP = 0x40 -K_O = 0x01 K_START = 0x04 K_SELECT = 0x08 @@ -37,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\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 @@ -46,27 +44,79 @@ 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, value_when_pressed=True) + 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.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, 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) -_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, 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 -display.auto_brightness = True - -buttons = gamepadshift.GamePadShift( - digitalio.DigitalInOut(board.BUTTON_CLOCK), - digitalio.DigitalInOut(board.BUTTON_OUT), - digitalio.DigitalInOut(board.BUTTON_LATCH), -) - -audio = stage.Audio(board.SPEAKER, board.SPEAKER_ENABLE) +buttons = _Buttons() +audio = _Audio(board.SPEAKER, board.SPEAKER_ENABLE) 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..931f7f2 --- /dev/null +++ b/pygamer/ugame.py @@ -0,0 +1,136 @@ +import board +import analogio +import stage +import displayio +import busio +import time +import keypad +import audioio +import audiocore +import supervisor +import digitalio + + +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\xa0" # _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 +) + + +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, value_when_pressed=True) + 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): + 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.set_next_code_file(None) + 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: + buttons |= K_LEFT + elif x > dead: + buttons |= K_RIGHT + y = self.joy_y.value - 32767 + if y < -dead: + buttons |= K_UP + elif y > dead: + buttons |= K_DOWN + 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, + chip_select=board.TFT_CS, reset=board.TFT_RST) +display = displayio.Display(_fourwire, _TFT_INIT, width=160, height=128, + 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/stage.py b/stage.py index e1d0ff2..4bac67a 100644 --- a/stage.py +++ b/stage.py @@ -1,130 +1,132 @@ import time import array -import digitalio -import audioio +import struct +try: + import zlib +except ImportError: + pass + import _stage FONT = (b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'P\x01\xd4\x05\xf5\x17\xed\x1e\xd5\x15\xd0\x01P\x01\x00\x00' - b'P\x01\xd0\x01\xd5\x15\xed\x1e\xf5\x17\xd4\x05P\x01\x00\x00' - b'P\x01\xd0\x05\x95\x17\xfd\x1f\x95\x17\xd0\x05P\x01\x00\x00' - b'P\x01\xd4\x01\xb5\x15\xfd\x1f\xb5\x15\xd4\x01P\x01\x00\x00' - b'T\x05\xf9\x1b\xdd\x1d}\x1f\xd9\x19\xa9\x1aT\x05\x00\x00' - b'T\x05\xf9\x1b]\x1d\xdd\x1dY\x19\xa9\x1aT\x05\x00\x00P\x01\xd0\x01' - b'\xe5\x16\xfd\x1f\xe4\x06t\x07\x14\x05\x00\x00P\x01\xd5\x15' - b']\x1d\x95\x15\xf4\x07\xe4\x06T\x05\x00\x00\x14\x05y\x1b' - b'\xfd\x1f\xf9\x1b\xe4\x06\xd0\x01@\x00\x00\x00P\x01\xf4\x06' - b'\xad\x1b\xed\x1b\xf9\x1a\xa4\x06P\x01\x00\x00@\x00\xd0\x01' - b'\xf4\x06\xfd\x1a\xa4\x06\x90\x01@\x00\x00\x00@\x15\xd0\x1a' - b'\xb4\x1b\xed\x1b\xfd\x06\xad\x01U\x00\x00\x00T\x05\xf5\x17' - b'\xbd\x1a]\x19m\x1bm\x1aU\x15\x00\x00\x00\x15D\x1f\xd9\x1f\xe4\x07' - b'\x94\x01Y\x06\x05\x01\x00\x00T\x05\xbd\x1a\xfd\x1a\xfd\x1a' - b'\xf4\x06\x90\x01@\x00\x00\x00\x15\x15m\x1e\xfd\x1f\xf5\x16' - b'\xb4\x06\xf4\x06T\x05\x00\x00P\x01\x04\x04\x04\x04P\x01' - b'\xf4\x06\xb4\x06P\x01\x00\x00P\x05t\x1b]\x1a\x1d\x15]\x1d\xf4\x07' - b'P\x01\x00\x00T\x00\x10\x01\x10\x05T\x1bm\x1ai\x05\x14\x00\x00\x00' - b'T\x05\xf4\x06\x90\x01\xf4\x06\xb9\x1a\xa9\x1aT\x05\x00\x00' - b'T\x05\xf5\x17\xdd\x1d\xdd\x1d\xf5\x17\xe4\x06T\x05\x00\x00' - b'U\x15\xad\x1e\xfd\x1f\xad\x1e\xd5\x15\xa9\x1aU\x15\x00\x00' - b'P\x01\xe4\x06t\x07\xe4\x06\xd0\x01\xd0\x05\xd0\x06P\x05' - b'P\x05\xd4\x17\xa5\x1d\xf9\x16\xb9\x06\xa5\x05T\x01\x00\x00' - b'U\x15\xfd\x1f\xbd\x1f\xad\x1e\xbd\x1f\xfd\x1fU\x15\x00\x00' - b'U\x15\xf9\x1a\xb4\x06\xf9\x1a\xf9\x1a\xa5\x16T\x05\x00\x00' - b'\x14\x05e\x16y\x1b\xd4\x05y\x1be\x16\x14\x05\x00\x00T\x15\xf5\x1f' - b'\x9d\x19\xf5\x1d\xd4\x1d\xd0\x1dP\x15\x00\x00\x00\x00P\x01' - b'\xe4\x06\xf4\x07\xe4\x06P\x01\x00\x00\x00\x00U\x15\xdd\x1d' - b'\xdd\x1d\x99\x19U\x15\xdd\x1dU\x15\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00U\x15\xdd\x1dU\x15\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00P\x01\xd0\x01' - b'\xd0\x01\x90\x01P\x01\xd0\x01P\x01\x00\x00T\x05t\x07d\x06T\x05' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x14\x05u\x17\xed\x1et\x07' - b'\xed\x1eu\x17\x14\x05\x00\x00T\x15\xf5\x1b\x99\x05\xf5\x17' - b'\x94\x19\xf9\x17U\x05\x00\x00\x15\x14\x1d\x1dU\x07\xd0\x01' - b't\x15\x1d\x1d\x05\x15\x00\x00T\x01\xe4\x05u\x07\xdd\x01' - b']\x17\xe5\x1dT\x14\x00\x00P\x01\xd0\x01\x90\x01P\x01' - b'\x00\x00\x00\x00\x00\x00\x00\x00@\x05P\x06\x90\x01\xd0\x01' - b'\x90\x01P\x06@\x05\x00\x00T\x00d\x01\x90\x01\xd0\x01\x90\x01d\x01' - b'T\x00\x00\x00\x00\x00\x14\x05t\x07\xd0\x01t\x07\x14\x05' - b'\x00\x00\x00\x00P\x01\x90\x01\xd5\x15\xf9\x1b\xd5\x15\x90\x01' - b'P\x01\x00\x00\x00\x00\x00\x00\x00\x00P\x01\xd0\x01\x90\x01' - b'P\x01\x00\x00\x00\x00\x00\x00U\x15\xf9\x1bU\x15\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00P\x01\xd0\x01' - b'P\x01\x00\x00\x00\x04\x00\x1d@\x07\xd0\x01t\x00\x1d\x00' - b'\x04\x00\x00\x00T\x05\xe5\x16Y\x1a\xdd\x1di\x19\xe5\x16' - b'T\x05\x00\x00@\x01\xd0\x01\xe4\x01\xd0\x01\xd0\x01\xe4\x06' - b'T\x05\x00\x00T\x05\xf9\x17U\x1d\xf4\x17Y\x05\xfd\x1fU\x15\x00\x00' - b'T\x05\xf5\x17]\x1d\x94\x07]\x1d\xf5\x17T\x05\x00\x00P\x00t\x00' - b']\x05]\x17\xfd\x1fU\x17@\x05\x00\x00U\x15\xfd\x1b]\x05\xfd\x1b' - b'U\x1d\xf9\x1bU\x05\x00\x00T\x15\xf5\x1b]\x05\xfd\x1b]\x1d\xf9\x1b' - b'T\x05\x00\x00U\x15\xfd\x1fU\x19\xd0\x06d\x01t\x00T\x00\x00\x00' - b'T\x05\xf5\x17]\x1d\xf5\x17]\x1d\xf5\x17T\x05\x00\x00T\x05\xf9\x1b' - b']\x1d\xf9\x1fT\x1d\xf9\x17U\x05\x00\x00\x00\x00P\x01\xd0\x01P\x01' - b'\xd0\x01P\x01\x00\x00\x00\x00\x00\x00P\x01\xd0\x01P\x01' - b'\xd0\x01\x90\x01P\x01\x00\x00\x00\x05@\x07\xd0\x01t\x00' - b'\xd0\x01@\x07\x00\x05\x00\x00\x00\x00U\x15\xf9\x1bT\x05' - b'\xf9\x1bU\x15\x00\x00\x00\x00\x14\x00t\x00\xd0\x01@\x07' - b'\xd0\x01t\x00\x14\x00\x00\x00T\x05\xe5\x17]\x1d\xd5\x16' - b'P\x05\xd0\x01P\x01\x00\x00T\x05\xb5\x17\xdd\x1d\x9d\x1b' - b'Y\x15\xf5\x06T\x05\x00\x00P\x00\xe4\x01Y\x07]\x1d\xed\x1e]\x1d' - b'\x15\x15\x00\x00U\x01\xfd\x05]\x07\xed\x16]\x1d\xfd\x17' - b'U\x05\x00\x00T\x05\xf5\x06]\x01\x1d\x14]\x1d\xf5\x17T\x05\x00\x00' - b'U\x01\xbd\x05]\x17\x1d\x1d]\x1d\xfd\x16U\x05\x00\x00U\x05\xfd\x06' - b']\x01\xfd\x01]\x15\xfd\x1bU\x15\x00\x00U\x15\xfd\x1b]\x15]\x00' - b'\xbd\x01]\x01\x15\x00\x00\x00T\x15\xf5\x1b]\x05\xdd\x1f' - b'Y\x1d\xf5\x1bT\x15\x00\x00\x15\x15\x1d\x1d]\x1d\xfd\x1f' - b']\x1d\x1d\x1d\x15\x15\x00\x00T\x05\xe4\x06\xd0\x01\xd0\x01' - b'\xd0\x01\xe4\x06T\x05\x00\x00\x00\x15\x00\x1d\x00\x1d\x05\x1d' - b']\x19\xf5\x17T\x05\x00\x00\x15\x14\x1d\x1d]\x07\xfd\x01' - b']\x07\x1d\x1d\x15\x14\x00\x00\x15\x00\x1d\x00\x1d\x00\x1d\x00' - b']\x15\xfd\x1fU\x15\x00\x00\x05\x14\x1d\x1dm\x1e\xdd\x1d' - b']\x1d\x1d\x1d\x15\x15\x00\x00\x05\x15\x1d\x1dm\x1d\xdd\x1d' - b']\x1e\x1d\x1d\x15\x14\x00\x00T\x01\xb5\x05]\x17\x1d\x1d' - b']\x1d\xe5\x17T\x05\x00\x00U\x05\xfd\x16]\x19]\x1d\xfd\x17]\x05' - b'\x15\x00\x00\x00T\x01\xb5\x05]\x17\x1d\x1d]\x1e\xe5\x07' - b'T\x1d\x00\x15U\x05\xfd\x16]\x19]\x1d\xfd\x07]\x1d\x15\x15\x00\x00' - b'T\x05\xf5\x07]\x01\xe5\x06T\x1d\xf9\x17U\x05\x00\x00U\x15\xf9\x1b' - b'\xd5\x15\xd0\x01\xd0\x01\xd0\x01P\x01\x00\x00\x15\x15\x1d\x1d' - b'\x1d\x1d\x19\x1du\x19\xd4\x17P\x05\x00\x00\x05\x14\x1d\x1d' - b'\x19\x19u\x17d\x06\xd0\x01@\x00\x00\x00\x15\x15\x1d\x1d' - b'\x1d\x1d]\x1d\xd9\x19u\x17\x14\x05\x00\x00\x05\x14\x1d\x1d' - b't\x07\xd0\x01t\x07\x1d\x1d\x05\x14\x00\x00\x15\x15\x1d\x1d' - b'\x19\x19u\x17\x94\x05\xd0\x01P\x01\x00\x00U\x15\xf9\x1b' - b'U\x07\xd0\x01t\x15\xf9\x1bU\x15\x00\x00T\x05\xf4\x06t\x01t\x00' - b't\x01\xf4\x06T\x05\x00\x00\x05\x00\x1d\x00t\x00\xd0\x01' - b'@\x07\x00\x1d\x00\x14\x00\x00T\x05\xe4\x07P\x07@\x07P\x07\xe4\x07' - b'T\x05\x00\x00@\x00\xd0\x01t\x07\x19\x19\x04\x04\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00U\x15\xf9\x1b' - b'U\x15\x00\x00P\x00\xb4\x01\xd4\x06P\x07@\x01\x00\x00' - b'\x00\x00\x00\x00\x00\x00T\x15\xe5\x1f]\x1d]\x1d\xf5\x1f' - b'T\x15\x00\x00\x15\x00]\x05\xfd\x16]\x1d]\x1d\xfd\x17U\x05\x00\x00' - b'\x00\x00T\x05\xe5\x07]\x05]\x1d\xf5\x16T\x05\x00\x00\x00\x15T\x1d' - b'\xe5\x1f]\x1d]\x1d\xf5\x1fT\x15\x00\x00\x00\x00T\x05' - b'\xf5\x17\xad\x1e]\x15\xf5\x07T\x05\x00\x00@\x15P\x1e' - b'\xd4\x15\xf4\x07\xd4\x05\xd0\x01\xd0\x01P\x01\x00\x00T\x15' - b'\xe5\x1f]\x1d\xf5\x1fT\x1d\xf9\x16U\x05\x15\x00]\x05\xfd\x16]\x1d' - b'\x1d\x1d\x1d\x1d\x15\x15\x00\x00P\x01\xd0\x01P\x01\xd0\x01' - b'\xd0\x01\xd0\x01P\x01\x00\x00@\x05@\x07@\x05@\x07E\x07]\x07' - b'\xe5\x05T\x01\x15\x00\x1d\x14]\x1d\xfd\x06]\x19\x1d\x1d' - b'\x15\x14\x00\x00T\x00t\x00t\x00t\x00d\x05\xd4\x07P\x05\x00\x00' - b'\x00\x00U\x05\xfd\x17\xdd\x19\xdd\x1d]\x1d\x15\x15\x00\x00' - b'\x00\x00U\x05\xfd\x17]\x19\x1d\x1d\x1d\x1d\x15\x15\x00\x00' - b'\x00\x00T\x05\xe5\x17]\x1d]\x1d\xf5\x17T\x05\x00\x00\x00\x00U\x05' - b'\xfd\x17]\x1d]\x1d\xfd\x17]\x05\x15\x00\x00\x00T\x15\xf5\x1f]\x1d' - b']\x1d\xf5\x1fT\x1d\x00\x15\x00\x00U\x05\xdd\x16}\x1d]\x04\x1d\x00' - b'\x15\x00\x00\x00\x00\x00T\x15\xe5\x1f\xad\x05\x94\x1e\xfd\x16' - b'U\x05\x00\x00T\x00u\x05\xfd\x07t\x01t\x01\xd4\x07P\x05\x00\x00' - b'\x00\x00\x15\x15\x1d\x1d\x1d\x1d]\x1d\xe5\x1fT\x15\x00\x00' - b'\x00\x00\x05\x14\x1d\x1d\x19\x19u\x17\xd4\x05P\x01\x00\x00' - b'\x00\x00\x15\x15]\x1d\xdd\x1d\xd9\x19u\x17T\x05\x00\x00' - b'\x00\x00\x15\x15m\x1e\xd4\x05\xd4\x05m\x1e\x15\x15\x00\x00' - b'\x00\x00\x15\x15\x1d\x1d]\x1d\xe5\x1fT\x1d\xfd\x17U\x05' - b'\x00\x00U\x15\xfd\x1f\xa4\x15\x95\x06\xfd\x1fU\x15\x00\x00' - b'@\x05\x90\x07\xd0\x01t\x01\xd0\x01\x90\x07@\x05\x00\x00' - b'P\x01\x90\x01\xd0\x01\xd0\x01\xd0\x01\x90\x01P\x01\x00\x00' - b'T\x00\xb4\x01\xd0\x01P\x07\xd0\x01\xb4\x01T\x00\x00\x00' - b'\x00\x00T\x00u\x15\xd9\x19U\x17@\x05\x00\x00\x00\x00U\x15\xfd\x1f' - b'\xed\x1e\xbd\x1f\xed\x1e\xfd\x1fU\x15\x00\x00') + b'P\x01\xd4\x05\xf5\x17\xed\x1e\xd5\x15\xd0\x01P\x01\x00\x00' + b'P\x01\xd0\x01\xd5\x15\xed\x1e\xf5\x17\xd4\x05P\x01\x00\x00' + b'P\x01\xd0\x05\x95\x17\xfd\x1f\x95\x17\xd0\x05P\x01\x00\x00' + b'P\x01\xd4\x01\xb5\x15\xfd\x1f\xb5\x15\xd4\x01P\x01\x00\x00' + b'T\x05\xf9\x1b\xdd\x1d}\x1f\xd9\x19\xa9\x1aT\x05\x00\x00' + b'T\x05\xf9\x1b]\x1d\xdd\x1dY\x19\xa9\x1aT\x05\x00\x00P\x01\xd0\x01' + b'\xe5\x16\xfd\x1f\xe4\x06t\x07\x14\x05\x00\x00P\x01\xd5\x15' + b']\x1d\x95\x15\xf4\x07\xe4\x06T\x05\x00\x00\x14\x05y\x1b' + b'\xfd\x1f\xf9\x1b\xe4\x06\xd0\x01@\x00\x00\x00P\x01\xf4\x06' + b'\xad\x1b\xed\x1b\xf9\x1a\xa4\x06P\x01\x00\x00@U\xd0\xff' + b'\xf4\xaa\xbdV\xad\x01m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00' + b'm\x00m\x00m\x00m\x00m\x00\xbd\x01\xf9V\xe4\xff\x90\xaa@UUU\xff\xff' + b'\xaa\xaaUU\x00\x00\x00\x00\x00\x00\x00\x00U\x01\xff\x06' + b'\xea\x1b\x95o@n\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m' + b'\x00m\x00m\x00m\x00m\x00m@o\xd5k\xff\x1a\xaa\x06U\x01' + b'\x00\x00\x00\x00\x00\x00\x00\x00UU\xff\xff\xaa\xaaUU' + b'\x00\x00\x00\x00\x00UE\xfe\xd9\xef\xdd\x9f\xad\x9f\xad\x9a' + b'\x00\x00\x00\x00\x00\x00U\x15\xf7o\xa7jW\x15v\x00\xadu\xed\xda' + b'\xddv\x99\xe6E\x9a\x00U\x00\x00\x00\x00m\x00W\x00n\x00\x15\x00' + b'\x1b\x00\x05\x00\x00\x00\x00\x00\xaa\x00\xaa\x00\xaa\x00\xaa\x00' + b'\x00\xaa\x00\xaa\x00\xaa\x00\xaaP\x05\x94\x16\xa4\x1b\xe4\x1b' + b'\xe4\x1a\xa4\x1aT\x15\x00\x00P\x00\xd0\x01\xd0\x07\xd4\x19' + b'\xf9\x1d\xbd\x05T\x00\x00\x00T\x05\xf5\x17\xdd\x1d\xdd\x1d' + b'\xf5\x17\xe4\x06T\x05\x00\x00\x14\x05e\x16y\x1b\xd4\x05y\x1be\x16' + b'\x14\x05\x00\x00T\x15\xf5\x1f\x9d\x19\xf5\x1d\xd4\x1d\xd0\x1d' + b'P\x15\x00\x00\x00\x00P\x01\xe4\x06\xf4\x07\xe4\x06P\x01' + b'\x00\x00\x00\x00U\x15\xdd\x1d\xdd\x1d\x99\x19U\x15\xdd\x1d' + b'U\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00U\x15\xdd\x1d' + b'U\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00P\x01\xd0\x01\xd0\x01\x90\x01P\x01\xd0\x01' + b'P\x01\x00\x00T\x05t\x07d\x06T\x05\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x14\x05u\x17\xed\x1et\x07\xed\x1eu\x17\x14\x05\x00\x00' + b'T\x15\xf5\x1b\x99\x05\xf5\x17\x94\x19\xf9\x17U\x05\x00\x00' + b'\x15\x14\x1d\x1dU\x07\xd0\x01t\x15\x1d\x1d\x05\x15\x00\x00' + b'T\x01\xe4\x05u\x07\xdd\x01]\x17\xe5\x1dT\x14\x00\x00P\x01\xd0\x01' + b'\x90\x01P\x01\x00\x00\x00\x00\x00\x00\x00\x00@\x05P\x06' + b'\x90\x01\xd0\x01\x90\x01P\x06@\x05\x00\x00T\x00d\x01' + b'\x90\x01\xd0\x01\x90\x01d\x01T\x00\x00\x00\x00\x00\x14\x05' + b't\x07\xd0\x01t\x07\x14\x05\x00\x00\x00\x00P\x01\x90\x01' + b'\xd5\x15\xf9\x1b\xd5\x15\x90\x01P\x01\x00\x00\x00\x00\x00\x00' + b'\x00\x00P\x01\xd0\x01\x90\x01P\x01\x00\x00\x00\x00\x00\x00' + b'U\x15\xf9\x1bU\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00P\x01\xd0\x01P\x01\x00\x00\x00\x04\x00\x1d' + b'@\x07\xd0\x01t\x00\x1d\x00\x04\x00\x00\x00T\x05\xe5\x16' + b'Y\x1a\xdd\x1di\x19\xe5\x16T\x05\x00\x00@\x01\xd0\x01' + b'\xe4\x01\xd0\x01\xd0\x01\xe4\x06T\x05\x00\x00T\x05\xf9\x17' + b'U\x1d\xf4\x17Y\x05\xfd\x1fU\x15\x00\x00T\x05\xf5\x17]\x1d\x94\x07' + b']\x1d\xf5\x17T\x05\x00\x00P\x00t\x00]\x05]\x17\xfd\x1fU\x17' + b'@\x05\x00\x00U\x15\xfd\x1b]\x05\xfd\x1bU\x1d\xf9\x1bU\x05\x00\x00' + b'T\x15\xf5\x1b]\x05\xfd\x1b]\x1d\xf9\x1bT\x05\x00\x00U\x15\xfd\x1f' + b'U\x19\xd0\x06d\x01t\x00T\x00\x00\x00T\x05\xf5\x17]\x1d\xf5\x17' + b']\x1d\xf5\x17T\x05\x00\x00T\x05\xf9\x1b]\x1d\xf9\x1fT\x1d\xf9\x17' + b'U\x05\x00\x00\x00\x00P\x01\xd0\x01P\x01\xd0\x01P\x01' + b'\x00\x00\x00\x00\x00\x00P\x01\xd0\x01P\x01\xd0\x01\x90\x01' + b'P\x01\x00\x00\x00\x05@\x07\xd0\x01t\x00\xd0\x01@\x07' + b'\x00\x05\x00\x00\x00\x00U\x15\xf9\x1bT\x05\xf9\x1bU\x15' + b'\x00\x00\x00\x00\x14\x00t\x00\xd0\x01@\x07\xd0\x01t\x00' + b'\x14\x00\x00\x00T\x05\xe5\x17]\x1d\xd5\x16P\x05\xd0\x01' + b'P\x01\x00\x00T\x05\xb5\x17\xdd\x1d\x9d\x1bY\x15\xf5\x06' + b'T\x05\x00\x00P\x00\xe4\x01Y\x07]\x1d\xed\x1e]\x1d\x15\x15\x00\x00' + b'U\x01\xfd\x05]\x07\xed\x16]\x1d\xfd\x17U\x05\x00\x00T\x05\xf5\x06' + b']\x01\x1d\x14]\x1d\xf5\x17T\x05\x00\x00U\x01\xbd\x05]\x17\x1d\x1d' + b']\x1d\xfd\x16U\x05\x00\x00U\x05\xfd\x06]\x01\xfd\x01]\x15\xfd\x1b' + b'U\x15\x00\x00U\x15\xfd\x1b]\x15]\x00\xbd\x01]\x01\x15\x00\x00\x00' + b'T\x15\xf5\x1b]\x05\xdd\x1fY\x1d\xf5\x1bT\x15\x00\x00' + b'\x15\x15\x1d\x1d]\x1d\xfd\x1f]\x1d\x1d\x1d\x15\x15\x00\x00' + b'T\x05\xe4\x06\xd0\x01\xd0\x01\xd0\x01\xe4\x06T\x05\x00\x00' + b'\x00\x15\x00\x1d\x00\x1d\x05\x1d]\x19\xf5\x17T\x05\x00\x00' + b'\x15\x14\x1d\x1d]\x07\xfd\x01]\x07\x1d\x1d\x15\x14\x00\x00' + b'\x15\x00\x1d\x00\x1d\x00\x1d\x00]\x15\xfd\x1fU\x15\x00\x00' + b'\x05\x14\x1d\x1dm\x1e\xdd\x1d]\x1d\x1d\x1d\x15\x15\x00\x00' + b'\x05\x15\x1d\x1dm\x1d\xdd\x1d]\x1e\x1d\x1d\x15\x14\x00\x00' + b'T\x01\xb5\x05]\x17\x1d\x1d]\x1d\xe5\x17T\x05\x00\x00U\x05\xfd\x16' + b']\x19]\x1d\xfd\x17]\x05\x15\x00\x00\x00T\x01\xb5\x05]\x17\x1d\x1d' + b']\x1e\xe5\x07T\x1d\x00\x15U\x05\xfd\x16]\x19]\x1d\xfd\x07]\x1d' + b'\x15\x15\x00\x00T\x05\xf5\x07]\x01\xe5\x06T\x1d\xf9\x17' + b'U\x05\x00\x00U\x15\xf9\x1b\xd5\x15\xd0\x01\xd0\x01\xd0\x01' + b'P\x01\x00\x00\x15\x15\x1d\x1d\x1d\x1d\x19\x1du\x19\xd4\x17' + b'P\x05\x00\x00\x05\x14\x1d\x1d\x19\x19u\x17d\x06\xd0\x01' + b'@\x00\x00\x00\x15\x15\x1d\x1d\x1d\x1d]\x1d\xd9\x19u\x17' + b'\x14\x05\x00\x00\x05\x14\x1d\x1dt\x07\xd0\x01t\x07\x1d\x1d' + b'\x05\x14\x00\x00\x15\x15\x1d\x1d\x19\x19u\x17\x94\x05\xd0\x01' + b'P\x01\x00\x00U\x15\xf9\x1bU\x07\xd0\x01t\x15\xf9\x1bU\x15\x00\x00' + b'T\x05\xf4\x06t\x01t\x00t\x01\xf4\x06T\x05\x00\x00\x05\x00\x1d\x00' + b't\x00\xd0\x01@\x07\x00\x1d\x00\x14\x00\x00T\x05\xe4\x07P\x07@\x07' + b'P\x07\xe4\x07T\x05\x00\x00@\x00\xd0\x01t\x07\x19\x19' + b'\x04\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'U\x15\xf9\x1bU\x15\x00\x00P\x00\xb4\x01\xd4\x06P\x07@\x01\x00\x00' + b'\x00\x00\x00\x00\x00\x00T\x15\xe5\x1f]\x1d]\x1d\xf5\x1f' + b'T\x15\x00\x00\x15\x00]\x05\xfd\x16]\x1d]\x1d\xfd\x17U\x05\x00\x00' + b'\x00\x00T\x05\xe5\x07]\x05]\x1d\xf5\x16T\x05\x00\x00\x00\x15T\x1d' + b'\xe5\x1f]\x1d]\x1d\xf5\x1fT\x15\x00\x00\x00\x00T\x05' + b'\xf5\x17\xad\x1e]\x15\xf5\x07T\x05\x00\x00@\x15P\x1e' + b'\xd4\x15\xf4\x07\xd4\x05\xd0\x01\xd0\x01P\x01\x00\x00T\x15' + b'\xe5\x1f]\x1d\xf5\x1fT\x1d\xf9\x16U\x05\x15\x00]\x05\xfd\x16]\x1d' + b'\x1d\x1d\x1d\x1d\x15\x15\x00\x00P\x01\xd0\x01P\x01\xd0\x01' + b'\xd0\x01\xd0\x01P\x01\x00\x00@\x05@\x07@\x05@\x07E\x07]\x07' + b'\xe5\x05T\x01\x15\x00\x1d\x14]\x1d\xfd\x06]\x19\x1d\x1d' + b'\x15\x14\x00\x00T\x00t\x00t\x00t\x00d\x05\xd4\x07P\x05\x00\x00' + b'\x00\x00U\x05\xfd\x17\xdd\x19\xdd\x1d]\x1d\x15\x15\x00\x00' + b'\x00\x00U\x05\xfd\x17]\x19\x1d\x1d\x1d\x1d\x15\x15\x00\x00' + b'\x00\x00T\x05\xe5\x17]\x1d]\x1d\xf5\x17T\x05\x00\x00\x00\x00U\x05' + b'\xfd\x17]\x1d]\x1d\xfd\x17]\x05\x15\x00\x00\x00T\x15\xf5\x1f]\x1d' + b']\x1d\xf5\x1fT\x1d\x00\x15\x00\x00U\x05\xdd\x16}\x1d]\x04\x1d\x00' + b'\x15\x00\x00\x00\x00\x00T\x15\xe5\x1f\xad\x05\x94\x1e\xfd\x16' + b'U\x05\x00\x00T\x00u\x05\xfd\x07t\x01t\x01\xd4\x07P\x05\x00\x00' + b'\x00\x00\x15\x15\x1d\x1d\x1d\x1d]\x1d\xe5\x1fT\x15\x00\x00' + b'\x00\x00\x05\x14\x1d\x1d\x19\x19u\x17\xd4\x05P\x01\x00\x00' + b'\x00\x00\x15\x15]\x1d\xdd\x1d\xd9\x19u\x17T\x05\x00\x00' + b'\x00\x00\x15\x15m\x1e\xd4\x05\xd4\x05m\x1e\x15\x15\x00\x00' + b'\x00\x00\x15\x15\x1d\x1d]\x1d\xe5\x1fT\x1d\xfd\x17U\x05' + b'\x00\x00U\x15\xfd\x1f\xa4\x15\x95\x06\xfd\x1fU\x15\x00\x00' + b'@\x05\x90\x07\xd0\x01t\x01\xd0\x01\x90\x07@\x05\x00\x00' + b'P\x01\x90\x01\xd0\x01\xd0\x01\xd0\x01\x90\x01P\x01\x00\x00' + b'T\x00\xb4\x01\xd0\x01P\x07\xd0\x01\xb4\x01T\x00\x00\x00' + b'\x00\x00T\x00u\x15\xd9\x19U\x17@\x05\x00\x00\x00\x00U\x15\xfd\x1f' + b'\xed\x1e\xbd\x1f\xed\x1e\xfd\x1fU\x15\x00\x00') PALETTE = (b'\xf8\x1f\x00\x00\xcey\xff\xff\xf8\x1f\x00\x19\xfc\xe0\xfd\xe0' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') @@ -144,38 +146,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): - if mute_pin: - self.mute_pin = digitalio.DigitalInOut(mute_pin) - self.mute_pin.switch_to_output(value=0) - else: - self.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 - - class BMP16: """Read 16-color BMP files.""" @@ -184,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: @@ -197,26 +165,24 @@ 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): 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 - def read_data(self, offset=0, buffer=None): - """Read the image data.""" - line_size = self.width >> 1 + 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(self.data + offset) + f.seek(self.data) index = (self.height - 1) * line_size for line in range(self.height): chunk = f.read(line_size) @@ -225,6 +191,84 @@ def read_data(self, offset=0, 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(">I4sIIBBBBB4s", f.read(25)) + assert size == 13 # header length + assert chunk == b'IHDR' + 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): + 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) + 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 + + 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) >> 1 + if buffer is None: + buffer = bytearray(line_size * self.height) + 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 + + class Bank: """ Store graphics for the tiles and sprites. @@ -240,13 +284,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(".bmp"): + image = BMP16(filename) + elif filename.lower().endswith(".png"): + image = PNG16(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) @@ -265,7 +320,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) @@ -308,8 +363,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): @@ -367,10 +422,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.""" @@ -391,6 +442,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: @@ -402,6 +454,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: @@ -410,25 +463,38 @@ 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: 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.""" + for i in range(self.width * self.height): self.buffer[i] = 0 @@ -441,19 +507,31 @@ 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) - 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 + display.root_group = None + self.width = display.width // self.scale + 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.""" + self.last_tick += self.tick_delay wait = max(0, self.last_tick - time.monotonic()) if wait: @@ -461,25 +539,41 @@ 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 + x1 = self.width + self.vx if y1 is None: - 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) + _stage.render(x0, y0, x1, y1, layers, self.buffer, + 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)) - if x0 == x1 or y0 == y1: + 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) - sprite._updated() + self.display, self.scale, self.vx, self.vy) 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) diff --git a/ugame22/pew.py b/ugame22/pew.py new file mode 120000 index 0000000..def1b96 --- /dev/null +++ b/ugame22/pew.py @@ -0,0 +1 @@ +../pew.py \ No newline at end of file diff --git a/ugame22/stage.py b/ugame22/stage.py new file mode 120000 index 0000000..2dedc93 --- /dev/null +++ b/ugame22/stage.py @@ -0,0 +1 @@ +../stage.py \ No newline at end of file diff --git a/ugame22/ugame.py b/ugame22/ugame.py new file mode 100644 index 0000000..7e3f49a --- /dev/null +++ b/ugame22/ugame.py @@ -0,0 +1,90 @@ +import audiobusio +import audiocore +import board +import busio +import digitalio +import displayio +import keypad +import os +import supervisor +import time + + +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: + os.chdir('/') + 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 = 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: + 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()