Skip to content

Snippets — patterns ready to adapt

These are paste-ready fragments — the code for a specific task. For the why and the higher-level structure behind them, see Game patterns. The skeleton below is a whole game in miniature; the fragments after it fill in the parts. Copy the relevant piece, adapt the names, and adjust the limits to your game. They use the same conventions as the bundled examples.

Start here. All mutable game state lives in one State object (st), and the per-frame loop lives in a main() function — inside a function its names resolve as fast locals instead of globals-dict lookups, a measured win on device. Every fragment further down slots into this shape. Copy it, rename the State fields, and fill in the play branch.

# Ready-to-use picogame skeleton: a TITLE -> PLAY -> GAME-OVER state machine, with ALL game state
# in one State object and the per-frame loop in a function (main). Copy it, rename the State fields,
# and fill in the PLAY branch. Runs as-is here in the browser and on a device.
import board
import terminalio
import picogame as pg
import picogame_game
import picogame_input
import picogame_clock
import picogame_shapes as shapes
import picogame_ui as ui
W, H = board.DISPLAY.width, board.DISPLAY.height # size-independent: never hardcode 320x240
BAR = 16 # reserved top HUD strip
scene, bufA, _ = picogame_game.setup(background=pg.rgb565(16, 20, 34), top=BAR)
btn = picogame_input.Buttons()
clock = picogame_clock.Clock(30)
hud = ui.HudBar(pg, board.DISPLAY, bufA, 0, 0, W, BAR, pg.rgb565(10, 12, 22))
label = hud.label(terminalio.FONT, 4, 3, pg.rgb565(235, 240, 255), "")
# The player: a generated shape now; swap in a Bitmap (png2picogame) later.
player = pg.Sprite(shapes.circle(14, pg.rgb565(240, 90, 90)), W // 2, H // 2)
player.anchor = (0.5, 0.5)
scene.add(player)
class State:
"""ALL mutable game state in ONE object, created once and never reassigned. reset() re-inits it
in place, so main() can hold `st` and every helper sees the same state. Add your fields here;
keep engine objects (sprites, pools, clock) as module globals, not in State."""
def __init__(self):
self.reset()
def reset(self):
self.mode = "title" # "title" -> "play" -> "over"
self.score = 0
st = State()
def show(text): # set the HUD text and repaint the bar
label.set(text)
hud.draw()
def new_game():
st.reset()
st.mode = "play"
player.move(W // 2, H // 2)
show("SCORE 0")
def main():
# The per-frame loop lives in a FUNCTION, not at module scope: here its names resolve as fast
# array-indexed locals instead of globals-dict lookups -- a measured win on device.
show("PRESS A TO START")
while True:
btn.poll()
if st.mode == "play":
# --- your game goes here: move, collide, score ---
player.x += (btn.is_pressed(btn.RIGHT) - btn.is_pressed(btn.LEFT)) * 3
if btn.just_pressed(btn.A):
st.score += 1
show("SCORE %d" % st.score) # set text where it CHANGES, not every frame
if btn.just_pressed(btn.B):
st.mode = "over"
show("GAME OVER - press A")
elif btn.just_pressed(btn.A): # title or game-over -> (re)start, no reload
new_game()
scene.refresh()
clock.tick()
main()
▶ Try it in the browser

The Game patterns page explains the shape; the fragments below are the individual moves.

Read the screen size instead of hardcoding it

Section titled “Read the screen size instead of hardcoding it”

Derive positions from W and H when the layout should adapt to different supported screen sizes (for example 320×240 and 240×240).

W, H = board.DISPLAY.width, board.DISPLAY.height
player = pg.Sprite(bm, W // 2, H - 40)

label.set() skips the redraw on unchanged text, but the "%"-format itself allocates a new string on every call. Doing that each frame creates avoidable garbage. Compare the integers first and format only when the displayed value changes:

hud = picogame_font.Label(...) # or a picogame_ui.HudBar; built ONCE at load
if score != shown_score: # shadow int of what the label shows
shown_score = score
hud.set("SCORE %d" % score)

Full HUD/label options: Text & UI.

When possible, set the label where its value changes (in the hit handler or at the level transition) and keep the frame loop free of HUD code. Quantize continuous values to the displayed unit first:

secs = int(time.monotonic() - t0) # whole seconds = what the label shows
if secs != shown_secs:
shown_secs = secs
timer_lbl.set("TIME %d" % secs)

For frequently created objects such as bullets, avoid constructing a new pg.Sprite(...) each time. A Pool pre-allocates them once; spawn() and free() toggle visibility:

bullets = Pool(scene, bullet_bm, 8, anchor=(0.5, 0.5)) # ONE allocation, at load
b = bullets.spawn() # None when all 8 fly -> fire rate self-limits
if b:
b.move(ship.x + 14, ship.y)
for b in bullets.items: # fixed list -> the loop allocates nothing
if not b.visible:
continue
b.x += 6
if b.x > W + 8:
bullets.free(b) # back to the pool for reuse

Fixed-count HUD icons (lives, ammo pips) don’t need a Pool — a plain list of sprites with visible toggles is right there.

Instant restart: one reset(), one never-rebound st

Section titled “Instant restart: one reset(), one never-rebound st”

Keep all run-scoped values in one State class and re-init them in place with reset(). st is created once and never reassigned — a never-rebound singleton — so main() (and any helper) can hold it as a fast local with no risk of pointing at a stale copy after a restart. Engine objects and cross-run values (best score) stay module globals.

class State:
def __init__(self): self.reset() # __init__ just calls reset -> one place for defaults
def reset(self):
self.score = 0
self.lives = 3
self.px = 60.0
st = State() # created ONCE, at load
def new_game():
st.reset() # every field back to its documented default, IN PLACE
bullets.free_all()
player.move(60, GROUND)

pg.render paints behind the scene’s back — without invalidate() the next refresh() leaves stale overlay fragments. overlay() does both; draw once, freeze, resume clean:

if btn.just_pressed(btn.START):
paused = not paused
if paused:
picogame_game.overlay(scene, board.DISPLAY, [pause_sd], bufA, 0, 0, W, H)
if paused:
clock.tick() # no updates, no refresh -> overlay stays put
continue
scene.refresh() # first refresh after unpause repaints fully

Shuffle: there is no random.shuffle on the device

Section titled “Shuffle: there is no random.shuffle on the device”

CircuitPython’s random lacks shuffle()/sample() (the desktop simulator has them — it runs CPython). Fisher-Yates over randint is the same algorithm:

for i in range(len(deck) - 1, 0, -1):
j = random.randint(0, i)
deck[i], deck[j] = deck[j], deck[i]

The cheapest visual punch. Set flash on the impact, count it down in the loop:

enemy.flash = WHITE # at the hit
st.flash_t = 3
if st.flash_t > 0: # in the frame loop
st.flash_t -= 1
if st.flash_t == 0:
enemy.flash = None

(On the device the blit effects flash/tint/dither/shadow share one slot — last set wins; the sim treats them as independent, so it won’t reproduce this. Re-assert a permanent dither after a flash ends. Fuller toolkit: Effects.)

Pair the flash with a sound. Build the tone once at load, then fire it on the event. Guard everything behind if audio: so an audio-less board (or a failed init) just runs silent:

audio = None
try:
import picogame_audio
audio = picogame_audio.Audio() # PWM out + mixer; may fail on audio-less boards
boom = picogame_audio.tone(140, 200) # low thud, built ONCE (a RawSample in RAM)
except Exception: # tight heap, no audio pin, ...
audio = None # -> run silent
if audio: # at the hit / pickup:
audio.sfx(boom) # fire-and-forget on a free voice

tone() beeps like this play in the browser playground (WebAudio). For where each audio path makes sound (playground / simulator / device) and a ready-made named-effect kit (hit(), coin(), boom()), see Audio.

Buttons: one virtual pad, mapped in settings.toml

Section titled “Buttons: one virtual pad, mapped in settings.toml”

Code against the engine’s logical buttons (UP DOWN LEFT RIGHT A B, optional X Y L1 L2 R1 R2 START SELECT) — never against pins. The same code.py then runs on any board; only the mapping in settings.toml changes (no reflash):

btn = picogame_input.Buttons() # reads GPIO + key matrix + USB, all OR'd
btn.poll() # once per frame, before any query
if btn.is_pressed(btn.LEFT): px -= 2
if btn.just_pressed(btn.A): jump()
if btn.has(btn.X): show_x_hint() # only if this board mapped X
# direct GPIO buttons (a Pico you wired yourself)
PICOGAME_BUTTONS = "UP=GP2 DOWN=GP3 LEFT=GP4 RIGHT=GP5 A=GP6 B=GP7"
# …or a scanned key matrix — NAME=row,col
PICOGAME_MATRIX_ROWS = "GP0 GP1 GP2 GP3"
PICOGAME_MATRIX_COLS = "GP4 GP5 GP6 GP7"
PICOGAME_MATRIX_MAP = "UP=0,1 DOWN=2,1 LEFT=1,0 RIGHT=1,2 A=3,3 B=3,2"

USB gamepad/keyboard (USB-host boards) attach automatically. Full guide + USB remap keys: Input & controls.

Input forgiveness: coyote time + jump buffer

Section titled “Input forgiveness: coyote time + jump buffer”

Both are picogame_input.Timer one-liners; together they make platforming feel fair:

coyote = picogame_input.Timer(6) # jump up to 6 frames after leaving a ledge
jbuf = picogame_input.Timer(5) # honour a jump pressed 5 frames before landing
coyote.feed(on_ground) # in the frame loop
jbuf.feed(btn.just_pressed(btn.A))
if jbuf.consume() and coyote.is_active:
jump()

A panel that repaints only when it changes

Section titled “A panel that repaints only when it changes”

A StripDraw with always_dirty=False does not retain a pixel surface for the panel. It stays idle until you call invalidate():

panel = pg.StripDraw(draw_panel, x, y, w, h, always_dirty=False)
scene.add(panel, fixed=True)
lines[1] = "HP %d" % hp # when the content changes:
panel.invalidate() # repaints once on the next refresh

Sprite x/y are ints; for smooth slow motion use the engine’s fixed-point accessors fx/fy (24.8) instead of keeping your own float position:

ball.fx = ball.fx + 0.4 # fractional-pixel movement without integer jitter
ball.fy = ball.fy + vy * dt