Source code for stage

import time
import array
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@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')


[docs]def color565(r, g, b): """Convert 24-bit RGB color to 16-bit.""" return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3
[docs]def collide(ax0, ay0, ax1, ay1, bx0, by0, bx1=None, by1=None): """Return True if the two rectangles intersect.""" if bx1 is None: bx1 = bx0 if by1 is None: by1 = by0 return not (ax1 < bx0 or ay1 < by0 or ax0 > bx1 or ay0 > by1)
[docs]class BMP16: """Read 16-color BMP files.""" def __init__(self, filename): self.filename = filename self.colors = 0 def read_header(self): 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, 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[2], buffer[1], buffer[0]) palette[color] = ((c << 8) | (c >> 8)) & 0xffff return palette def read_data(self, buffer=None): line_size = (self.width + 1 ) >> 1 if buffer is None: buffer = bytearray(line_size * self.height) with open(self.filename, 'rb') as f: f.seek(self.data) index = (self.height - 1) * line_size for line in range(self.height): chunk = f.read(line_size) buffer[index:index + line_size] = chunk index -= line_size return buffer
[docs]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
[docs]class Bank: """ Store graphics for the tiles and sprites. A single bank stores exactly 16 tiles, each 16x16 pixels in 16 possible colors, and a 16-color palette. We just like the number 16. """ def __init__(self, buffer=None, palette=None): self.buffer = buffer self.palette = palette
[docs] @classmethod def from_bmp16(cls, filename): """Read the bank from a BMP file.""" return cls.from_image(filename)
[docs] @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)
[docs]class Grid: """ A grid is a layer of tiles that can be displayed on the screen. Each square can contain any of the 16 tiles from the associated bank. """ def __init__(self, bank, width=8, height=8, palette=None, buffer=None): self.x = 0 self.y = 0 self.z = 0 self.stride = (width + 1) & 0xfe self.width = width self.height = height self.bank = bank self.palette = palette or bank.palette self.buffer = buffer or bytearray((self.stride * height)>>1) self.layer = _stage.Layer(self.stride, self.height, self.bank.buffer, self.palette, self.buffer)
[docs] def tile(self, x, y, tile=None): """Get or set what tile is displayed in the given place.""" if not 0 <= x < self.width or not 0 <= y < self.height: return 0 index = (y * self.stride + x) >> 1 b = self.buffer[index] if tile is None: return b & 0x0f if x & 0x01 else b >> 4 if x & 0x01: b = b & 0xf0 | tile else: b = b & 0x0f | (tile << 4) self.buffer[index] = b
[docs] 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: self.z = z self.layer.move(int(x), int(y))
[docs]class WallGrid(Grid): """ A special grid, shifted from its parents by half a tile, useful for making nice-looking corners of walls and similar structures. """ def __init__(self, grid, walls, bank, palette=None): super().__init__(bank, grid.width + 1, grid.height + 1, palette) self.grid = grid self.walls = walls self.update() self.move(self.x - 8, self.y - 8) def update(self): for y in range(self.height): for x in range(self.width): t = 0 bit = 1 for dy in (-1, 0): for dx in (-1, 0): if self.grid.tile(x + dx, y + dy) in self.walls: t |= bit bit <<= 1 self.tile(x, y, t)
[docs]class Sprite: """ A sprite is a layer containing just a single tile from the associated bank, that can be positioned anywhere on the screen. """ def __init__(self, bank, frame, x, y, z=0, rotation=0, palette=None): self.bank = bank self.palette = palette or bank.palette self.frame = frame self.rotation = rotation self.x = x self.y = y self.z = z self.layer = _stage.Layer(1, 1, self.bank.buffer, self.palette) self.layer.move(x, y) self.layer.frame(frame, rotation) self.px = x self.py = y
[docs] def move(self, x, y, z=None): """Move the sprite to the given place.""" self.x = x self.y = y if z is not None: self.z = z self.layer.move(int(x), int(y))
[docs] def set_frame(self, frame=None, rotation=None): """ Set the current graphic and rotation of the sprite. The possible values for rotation are: 0 - none, 1 - 90 degrees clockwise, 2 - 180 degrees, 3 - 90 degrees counter-clockwise, 4 - mirrored, 5 - 90 degrees clockwise and mirrored, 6 - 180 degrees and mirrored, 7 - 90 degrees counter-clockwise and mirrored. """ if frame is not None: self.frame = frame if rotation is not None: self.rotation = rotation self.layer.frame(self.frame, self.rotation)
def update(self): pass
[docs]class Text: """Text layer. For displaying text.""" def __init__(self, width, height, font=None, palette=None, buffer=None): self.width = width self.height = height self.font = font or FONT self.palette = palette or PALETTE self.buffer = buffer or bytearray(width * height) self.layer = _stage.Text(width, height, self.font, self.palette, self.buffer) self.column = 0 self.row = 0 self.x = 0 self.y = 0 self.z = 0
[docs] 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: return chr(self.buffer[y * self.width + x]) c = ord(c) if hightlight: c |= 0x80 self.buffer[y * self.width + x] = c
[docs] 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: self.z = z self.layer.move(int(x), int(y))
[docs] 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)
[docs] def text(self, text, hightlight=False): """ Display text starting at the current cursor location. Return the dimensions of the rendered text. """ longest = 0 tallest = 0 for c in text: 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
[docs] def clear(self): """Clear all text from the layer.""" for i in range(self.width * self.height): self.buffer[i] = 0
[docs]class Stage: """ Represents what is being displayed on the screen. The ``display`` parameter is displayio.Display representing an initialized 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, 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.scale self.height = display.height // self.scale self.last_tick = time.monotonic() self.tick_delay = 1 / fps self.vx = 0 self.vy = 0
[docs] 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: time.sleep(wait) else: self.last_tick = time.monotonic()
[docs] 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 + self.vx if y1 is None: y1 = self.height + self.vy x0 = min(max(0, x0 - self.vx), self.width - 1) y0 = min(max(0, y0 - self.vy), self.height - 1) x1 = min(max(1, x1 - self.vx), self.width) y1 = min(max(1, y1 - self.vy), self.height) if x0 >= x1 or y0 >= y1: return layers = [l.layer for l in self.layers] _stage.render(x0, y0, x1, y1, layers, self.buffer, self.display, self.scale, self.vx, self.vy)
[docs] def render_sprites(self, sprites): """Update the spots taken by all the sprites in the list.""" layers = [l.layer for l in self.layers] for sprite in sprites: x = int(sprite.x) - self.vx y = int(sprite.y) - self.vy x0 = max(0, min(self.width - 1, min(sprite.px, x))) y0 = max(0, min(self.height - 1, min(sprite.py, y))) x1 = max(1, min(self.width, max(sprite.px, x) + 16)) y1 = max(1, min(self.height, max(sprite.py, y) + 16)) sprite.px = x sprite.py = y if x0 >= x1 or y0 >= y1: continue _stage.render(x0, y0, x1, y1, layers, self.buffer, self.display, self.scale, self.vx, self.vy)