Layers and drawables

Everything drawn belongs to a layer, and every layer belongs to a scene:

A scene holds layers; a layer holds sprites, sprite pools, tilemaps and labels

How a scene is put together.

Layers create their own drawables — there is no free-standing Sprite(...) — and the projection belongs to the layer, not to its drawables.

All drawables share x, y, visible, show() and hide(). Coordinates are signed and fractional. X is an angle that wraps at vs2.display.width (256), and Y is measured inward from the rim. What Y means depends on the layer’s projection; see the circular display.

Layer

class vs2.Layer(scene, name, projection, visible)[source]

Bases: object

An ordered group of drawables sharing one projection.

Created by Scene.layer(), never directly. A layer owns its drawables: they are created through sprite(), sprite_pool(), tilemap() and label(), which allocate and attach in one step, so a drawable that nothing draws cannot exist.

Within a layer, drawables paint in creation order — each one over those created before it — and layers themselves paint in the order the scene created them. Sprites, tilemaps and labels interleave freely:

ground = world.tilemap("ground.png", columns=8, rows=17)
player = world.sprite("ship.png")
clouds = world.tilemap("clouds.png", columns=8, rows=4)

That paints ground, then the player, then clouds over both.

property projection

How this layer maps Y to LEDs – TUNNEL, HUD or FULLSCREEN. Writable at runtime, e.g. a radar layer that flips between tunnel and HUD.

property visible

Whether the whole layer draws. Toggling it touches no drawable.

property sprites

The layer’s Sprite drawables, in creation order.

property tilemaps

The layer’s Tilemap drawables, in creation order.

sprite(image, x=0, y=0, frame=0, visible=True, flip_x=False, flip_y=False)[source]

Create one sprite on this layer. Only callable from build().

Parameters:
  • image – An image name or an Image handle.

  • x – Angle. 0 is the bottom of the disc, 64 left, 128 top, 192 right; wraps at vs2.display.width.

  • y – Distance inward from the rim, where 0 is the outermost LED. Its range follows the layer’s projection: 0..53 on HUD, 0..255 (depth) on TUNNEL and FULLSCREEN.

  • frame – Initial frame, validated against the image’s frame count.

  • visible – Whether it draws. Independent of frame.

  • flip_x – Mirror horizontally.

  • flip_y – Mirror vertically.

Returns:

Sprite – The new sprite.

Raises:
sprite_pool(image, count, frame=0, on_empty=None)[source]

Create a fixed group of interchangeable sprites.

All count sprites are allocated here, hidden, so the budget is spent visibly and up front in one reviewable number. Nothing is allocated when they later spawn and despawn.

Parameters:
  • image – An image name or an Image handle.

  • count – How many sprites to reserve. Must be at least 1.

  • frame – Initial frame for every sprite in the pool.

  • on_empty – What SpritePool.spawn() does when the pool is exhausted. None returns None — “no free bullet this frame” is a game rule, not an error. vs2.RECYCLE reuses the oldest live sprite instead, which is what explosions and particles usually want.

Returns:

SpritePool – The new pool, with every sprite hidden.

Raises:
tilemap(image, columns, rows, cells=None, x=0, y=0, view_width=None, view_height=None, view_x=0, view_y=0, visible=True, flip_x=False, flip_y=False)[source]

Create a grid of tiles drawn from one tileset image.

Tile size comes from the image, so it can never disagree with it.

Parameters:
  • image – The tileset. One frame per distinct tile.

  • columns – Grid width in tiles.

  • rows – Grid height in tiles.

  • cells – An existing bytearray of columns * rows tile indices to draw from. Omit it and one is allocated, filled with vs2.EMPTY_TILE. Pass your own to keep object identity with a buffer the game already owns.

  • x – Angle of the map’s origin; 0 is the bottom of the disc.

  • y – Distance of the origin inward from the rim, 0 being the outermost LED.

  • view_width – Width of the on-screen window onto the map. Defaults to the whole map.

  • view_height – Height of that window. Defaults to the whole map.

  • view_x – Horizontal scroll offset into the map.

  • view_y – Vertical scroll offset into the map.

  • visible – Whether it draws.

  • flip_x – Mirror the complete visible viewport horizontally.

  • flip_y – Mirror the complete visible viewport vertically.

Returns:

Tilemap – The new tilemap.

Raises:
label(image, columns, rows=1, x=0, y=0, text=None, glyphs=None, visible=True, flip_x=False, flip_y=False)[source]

Create a text label: a tilemap you write strings into.

A label costs one tilemap record and columns * rows bytes no matter how often the text changes, where the same text as individual sprites would cost one sprite per character.

Characters map to frames through a glyph table, resolved once here in this order: an explicit glyphs argument, then a glyphs: entry declared beside the strip in __images__.yaml, then CP437 (frame = ord(ch)). Unmappable characters and spaces become vs2.EMPTY_TILE, which the renderer skips.

Parameters:
  • image – The font strip. One frame per glyph.

  • columns – Width in characters.

  • rows – Height in lines. One-line labels get the Label.text property.

  • x – Angle; 0 is the bottom of the disc.

  • y – Distance inward from the rim, 0 being the outermost LED. Text is most legible at low Y, near the rim.

  • text – Initial text for a one-line label.

  • glyphs – A charmap such as "0123456789/-", where frame = glyphs.index(ch). Include " " to get a real space glyph on fonts with opaque backgrounds.

  • visible – Whether it draws.

  • flip_x – Mirror the complete label horizontally.

  • flip_y – Mirror the complete label vertically. Set both flips to rotate a label 180 degrees without changing its font strip.

Returns:

Label – The new label.

Raises:

Sprite

class vs2.Sprite(layer, image, x, y, frame, visible, flip_x, flip_y)[source]

Bases: object

One movable image on a layer.

Created by Layer.sprite() or handed out by a SpritePool, never directly. Writing to x, y, frame, visible, flip_x, flip_y or image goes straight into the renderer’s record and allocates nothing, so moving sprites every tick is free.

frame and visible are independent, which is what makes priming a pooled sprite two plain statements:

shot.frame = BULLET_FRAME    # still hidden
shot.show()                  # now visible, same frame
property layer

The Layer that draws this sprite. Read-only for its whole life, so draw order is never ambiguous.

property image

The sprite’s Image. Assigning a name or handle swaps the artwork, keeping frame if it is still in range and resetting it to 0 otherwise.

property x

Angle around the disc – 0 at the bottom, 64 left, 128 top, 192 right – wrapping at vs2.display.width. Fractional values are kept: the renderer stores signed 8.8 fixed point.

property y

Distance inward from the rim, where 0 is the outermost LED.

What it means follows the layer’s projection. On a HUD layer it is a direct LED index, 0..53. On a TUNNEL layer it is depth, 0..255: 0 is the outer rim and 255 is the centre. FULLSCREEN uses that same curve as radial extent: 0 fills the disc and increasing values contract toward the centre. Out-of-range values clip. Fractional values are kept.

property frame

Which animation frame to draw. Never changes visible.

Raises:

FrameError – If the frame is out of range for the image.

property visible

Whether the sprite draws. Never changes frame.

property flip_x

Mirror the sprite horizontally.

property flip_y

Mirror the sprite vertically.

property width

Frame width from the image metadata. Read-only, no renderer call.

property height

Frame height from the image metadata. Read-only, no renderer call.

show()[source]

Make the sprite visible without touching its frame.

hide()[source]

Hide the sprite without touching its frame.

overlaps(other)[source]

Whether this sprite’s box overlaps other’s.

An axis-aligned test that allocates nothing. X wraps around the display, so a sprite straddling column 0 still collides correctly; Y does not wrap, because the disc has an inside and an outside.

first_overlap(sprites)[source]

Return the first sprite in sprites that overlaps this one.

Accepts any iterable, including a SpritePool, which is the common case:

baddie = shot.first_overlap(self.baddies)
if baddie:
    self.baddies.despawn(baddie)
Returns:

Sprite or None – The first overlapping sprite, or None.

SpritePool

class vs2.SpritePool(sprites, on_empty)[source]

Bases: object

A fixed group of interchangeable sprites, cycled without allocating.

Created by Layer.sprite_pool(). Every operation is O(1) index bookkeeping — nothing allocates a Python object or a renderer record.

Iterating yields only the live sprites, and despawning the current sprite mid-loop is supported, which is the pattern most games need. Y is depth on a TUNNEL layer, so a shot fired away from the player counts up toward the centre and is retired at a depth the game picks:

for shot in self.shots:
    shot.y += SHOT_SPEED
    if shot.y > SHOT_RANGE:
        self.shots.despawn(shot)
        continue
    baddie = shot.first_overlap(self.baddies)
    if baddie:
        self.baddies.despawn(baddie)
        self.booms.spawn(x=baddie.x, y=baddie.y)
        self.shots.despawn(shot)

len(pool) is the live count and free is the remainder.

property free

How many sprites are still available to spawn().

__len__()[source]

The number of live sprites.

__iter__()[source]

Iterate the live sprites, tolerating despawns during the loop.

spawn(x, y, frame=0, flip_x=False, flip_y=False)[source]

Take a free sprite, position it, show it, and return it.

Parameters:
  • x – Angular position.

  • y – Radial position.

  • frame – Frame to show.

  • flip_x – Mirror horizontally.

  • flip_y – Mirror vertically.

Returns:

Sprite or None – The spawned sprite. None when the pool is exhausted and it was not created with on_empty=vs2.RECYCLE.

despawn(sprite)[source]

Hide sprite and return it to the pool.

Raises:

ValueError – If the sprite belongs to another pool, or is already despawned. Both mean a bookkeeping bug in the game.

despawn_all()[source]

Despawn every live sprite. The usual way to reset a level.

Tilemap

class vs2.Tilemap(layer, image, columns, rows, cells, x, y, view_width, view_height, view_x, view_y, visible, flip_x=False, flip_y=False)[source]

Bases: object

A grid of tiles drawn from one tileset image.

Created by Layer.tilemap(). The grid itself is a flat bytearray of tile indices in cells, one byte per cell, addressed in (column, row) order — the same axis order as (x, y) everywhere else:

self.ground[col, row] = ROCK
tile = self.ground[col, row]
self.ground.fill(GRASS)
self.ground.cells[row * self.ground.columns + col] = ROCK  # fastest

vs2.EMPTY_TILE leaves a cell blank and the renderer skips it.

Scrolling moves the view over the grid rather than rewriting it, so it costs two scalar writes and allocates nothing:

self.ground.view_y = self.depth % self.ground.tile_height
property layer

The Layer that draws this tilemap.

property image

The tileset Image this map draws from.

property columns

Grid width in tiles. Read-only.

property rows

Grid height in tiles. Read-only.

property tile_width

Width of one tile, taken from the image. Read-only.

property tile_height

Height of one tile, taken from the image. Read-only.

property cells

The flat bytearray of tile indices, row-major.

Writable in place; the buffer itself cannot be replaced or resized while the map is active, because the renderer reads these bytes directly.

property x

Angle of the map’s origin, 0 at the bottom of the disc, wrapping at vs2.display.width.

property y

Distance of the map’s origin inward from the rim, 0 being the outermost LED. Interpreted per the layer’s projection, like vs2.Sprite.y.

property view_x

Horizontal scroll offset into the grid. Writing it pans the view without touching cells.

property view_y

Vertical scroll offset into the grid. Writing it pans the view without touching cells.

property view_width

Width of the on-screen window onto the map. Read-only.

property view_height

Height of the on-screen window onto the map. Read-only.

property visible

Whether the tilemap draws.

property flip_x

Whether the complete visible viewport is mirrored horizontally.

property flip_y

Whether the complete visible viewport is mirrored vertically.

__getitem__(position)[source]

Read one cell: tile = tilemap[column, row].

Raises:

IndexError – If the cell is outside the grid.

__setitem__(position, value)[source]

Write one cell: tilemap[column, row] = tile.

Raises:

IndexError – If the cell is outside the grid.

fill(value)[source]

Set every cell to value, in place.

show()[source]

Make the tilemap visible.

hide()[source]

Hide the tilemap.

Label

class vs2.Label(layer, image, columns, rows, x, y, glyphs, visible, flip_x=False, flip_y=False)[source]

Bases: Tilemap

A tilemap you write text into.

Created by Layer.label(). Because it is a tilemap, a label counts against the tilemap budget rather than the sprite budget, and costs the same whether it shows two characters or twenty.

The display stores tiles counter-clockwise, but a label handles that inversion internally: you write ordinary left-to-right strings and never see the reversed indices.

Only fixed-cell ASCII and CP437 are supported. There is no variable-width text, wrapping, or Unicode shaping.

write(column, row, text, frame_offset=0, pad=True)[source]

Write text starting at (column, row).

Touches only the bytes it changes and allocates nothing, so it is safe to call every tick. Text longer than the remaining columns is clipped.

frame_offset shifts every glyph by a constant, which is how font strips that pack a second colour at a fixed offset are reached:

self.status.write(3, 1, "ABXY", frame_offset=0x80)
Parameters:
  • column – Starting column.

  • row – Row to write into.

  • text – The string. Non-strings are converted.

  • frame_offset – Added to each glyph’s frame index.

  • pad – When true, clear the rest of the row after the text. Set it false to overwrite in place without disturbing neighbours.

Raises:

IndexError – If column or row is outside the grid.

property text

The label’s contents, for one-line labels only.

Reading maps the cells back through the glyph table and strips trailing blanks; writing truncates at columns and pads the rest with vs2.EMPTY_TILE:

title = hud.label("rainbow437.png", columns=18, text="VS2 SPRITES")
title.text = "GAME OVER"
Raises:

AttributeError – On a label with more than one row. Use write() there.

set_number(value, width=1, pad='0')[source]

Write a right-aligned integer without building a string.

A score updated every tick this way costs no allocation at all, where "%05d" % value would allocate a string per frame:

self.score.set_number(value, width=5, pad="0")

Negative values clamp to zero, and values too large for width fill with nines rather than overflowing the label.

Parameters:
  • value – The number to display.

  • width – How many digit cells to use, starting at column 0.

  • pad – Character for leading blanks. "0" zero-pads; " " leaves them blank.

Raises:

ValueError – If width does not fit the label.

Image

class vs2.Image(name, strip, metadata)[source]

Bases: object

A resolved image from the scene’s asset pack.

Returned by Scene.image() and carried by every drawable as its image attribute. Resolving a name once and reusing the handle avoids repeating the lookup, and lets one image back several drawables:

def build(self):
    enemy = self.image("enemy.png")
    self.small = self.world.sprite_pool(enemy, count=12)
    self.boss = self.world.sprite(enemy, frame=4)

Attributes are read-only and come from the ROM’s metadata, so reading width or frames costs nothing at runtime.

name

The image’s name in the asset pack, e.g. "ship.png".

width

Width of a single frame, in display columns.

height

Height of a single frame, in LEDs.

frames

Number of animation frames. Valid frame indices are 0..frames-1.