Skip to content
Wouter Van de Wiele edited this page Aug 5, 2026 · 1 revision

Games

Six mini-games live in lib/GAMES/, launched from Menu → Games. They run as carousel overlays (CarouselManager::activateGame) — the underlying carousel screen stops ticking/animating while a game is active (only background housekeeping like the status bar, idle-sleep and input polling keep running) and every tick/button routes to the game instead, until it exits. Eye LEDs are suppressed while a game is on screen.

Framework (GameElement)

stateDiagram-v2
    [*] --> IDLE: activateGame → init()
    IDLE --> PLAYING: OK (onStart)
    IDLE --> [*]: CANCEL (clearGame)
    PLAYING --> GAME_OVER: endGame(result)
    PLAYING --> [*]: CANCEL
    GAME_OVER --> PLAYING: OK (restart)
    GAME_OVER --> [*]: CANCEL
Loading

GameElement (base class) provides:

  • Idle/title screen with per-game splash art (drawSplashArt(), top ~30 px) and a blinking "OK start / CANCEL exit" hint.
  • Score screen on endGame(msg) — shows _score and an optional result message; OK restarts, CANCEL leaves.
  • Subclass contract: onStart() (reset state), onTick(now) (frame), onInput(button) (game keys). The base class already consumes OK/CANCEL in IDLE/GAME_OVER states.

The panic-stop key (FUNC_1_DOWN) still works during games — it is handled by the carousel before input routing.

The games

Game Menu label Play mechanics (keys = right joystick — games receive GEM_KEY_UP/DOWN/LEFT/RIGHT/OK, the same channel as every other screen; the left joystick's FUNC keys are swallowed by the carousel while a game is active, except panic stop)
GameDino Dino Runner Endless runner: jump (with grace window) and duck under birds; speed ramps up
GameSnake Snake 25×10 grid, 3 lives (running into a border or yourself costs a life and resets the snake, doesn't end the run outright); step timer speeds up as you eat
GameBreakout Breakout Paddle + bricks
GameConnect4 Connect 4 You vs. a built-in AI (win-if-possible → block-player-win → center-preference heuristic) on a 7-column board
GameInvaders Invaders 5×3 alien grid marching, player ship + single bullet
GameWumpus Hunt Wumpus Classic dodecahedron cave (20 rooms), 5 arrows, pits/bats/wumpus clues; cursor picks one of 3 tunnels, shoot mode toggles

All games draw with Adafruit_GFX primitives at text size 1 into the shared 100×48 canvas and flush with updateWholeScreen().

Screenshots

Pixel-accurate renderings of each game's title, in-game and game-over screens, generated by tools/lcd_render (a host-side reimplementation of Adafruit_GFX driven by the real draw-call sequences — see that folder's render_all.py for how to regenerate these after a UI change).

Dino Runner

Dino Runner title screen Dino Runner gameplay Dino Runner game over

Snake

Snake title screen Snake gameplay Snake game over

Breakout

Breakout title screen Breakout gameplay Breakout game over

Connect 4

Connect 4 title screen Connect 4 gameplay Connect 4 game over

Invaders

Invaders title screen Invaders gameplay Invaders game over

Hunt Wumpus

Hunt Wumpus title screen Hunt Wumpus gameplay Hunt Wumpus game over

Adding a game

  1. Subclass GameElement, implement onStart/onTick/onInput (+ optional drawSplashArt).
  2. Instantiate it in main.cpp and register before carousel.begin(): menuEl.addGame(&myGame, "My Game");
  3. MenuElement::MAX_GAMES is 7 (already bumped from 6 for game_tiltmaze.cpp, which exists in lib/GAMES/ but is currently commented out of registration in main.cpp — not reachable from the menu) — raise it further if you add another.

A lazy-loading scheme (placement-new on launch) was evaluated and rejected: it would only save ~500 B of static RAM and adds lifetime-management risk.

Clone this wiki locally