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/ugame.py b/pygamer/ugame.py index 336a1eb..931f7f2 100644 --- a/pygamer/ugame.py +++ b/pygamer/ugame.py @@ -1,16 +1,14 @@ -""" -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 +import audioio +import audiocore +import supervisor +import digitalio K_X = 0x01 @@ -38,7 +36,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 @@ -47,49 +45,92 @@ 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, 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): - 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.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: - 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 + + +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) -buttons = Buttons() -audio = stage.Audio(board.SPEAKER, board.SPEAKER_ENABLE) + 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 442d45d..4bac67a 100644 --- a/stage.py +++ b/stage.py @@ -1,135 +1,132 @@ import time import array -import digitalio -import audioio +import struct try: - import audiocore + import zlib except ImportError: - audiocore = audioio + 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') @@ -149,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 = audiocore.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.""" @@ -189,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: @@ -202,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) @@ -230,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. @@ -245,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) @@ -270,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) @@ -313,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): @@ -372,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.""" @@ -396,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: @@ -407,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: @@ -415,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 @@ -446,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: @@ -466,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() pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy