A Tetris implementation for the Pimoroni PicoSystem, with an SDL build so it can be developed and played on the desktop.
Grab the latest picosystem-tetris.uf2 from the
releases page, or
from the artifacts of any build run
if you want the bleeding edge.
To flash: hold X while powering the PicoSystem on to enter bootloader mode,
then copy the .uf2 onto the RPI-RP2 drive that appears. The device reboots
into the game on its own.
Built for a 240x240 pixel display at 16bpp, with the game logic kept separate from rendering so the same code drives both the handheld and the desktop build. The board is a 10x20 grid of square 11x11 cells; blocks are drawn inset by one pixel over black, so the gutter between them forms the grid for free.
The project is structured to allow for multiple display targets:
- Game Logic: Contained in
tetris_game.c/h- handles all the game mechanics - Display Interface: Contained in
tetris_display.c/h- provides a unified drawing API and game-specific rendering - Renderers: Platform-specific implementations
- PicoSystem:
tetris_display_picosystem.cpp- PicoSystem implementation - SDL:
tetris_display_sdl.c- Desktop implementation using SDL3
- PicoSystem:
The implementation consists of the following files:
| File | Description |
|---|---|
tetris_game.h |
Game logic definitions and declarations |
tetris_game.c |
Game logic implementation |
tetris_display.h |
Display API and drawing function declarations |
tetris_display.c |
Implementation of display functions and renderer interface |
tetris_display_renderers.h |
Header defining renderer functions |
tetris_display_picosystem.cpp |
PicoSystem-specific rendering implementation |
tetris_display_sdl.c |
SDL3-specific rendering implementation |
tetris_main_pico.cpp |
Entry point and game loop for PicoSystem |
tetris_main_sdl.cpp |
Entry point and game loop for SDL |
- Standard 10×20 Tetris grid
- 7 classic tetromino shapes (I, O, T, S, Z, J, L)
- Piece movement and rotation with collision detection
- Line clearing and score calculation
- Level progression based on lines cleared
- Game states (active, paused, game over)
initGame()- Initialize the game stateupdateGame(float tick)- Update game state based on elapsed timemoveTetromino(Direction dir)- Move the current piecerotateTetromino()- Rotate the current piecehardDrop()- Drop the current piece straight to its landing rowghostDropY()- Row the current piece would land on (used to draw the ghost)clearLines()- Clear full lines and update score
- Game board: 110×220 pixels at (6, 10), a 10×20 grid of square 11×11 cells
- Side panel: 108 pixels wide at x=126, holding the next-piece preview and stats
- No title bar; the space is spent on the board instead
Blocks are drawn inset by 1 pixel over a black board, so the 1-pixel gutter between them forms the grid without any separate grid-line drawing.
The display system is now abstracted through a renderer interface:
typedef struct {
void (*init)();
void (*clear)(uint16_t color);
void (*drawRect)(int x, int y, int width, int height, uint16_t color);
void (*fillRect)(int x, int y, int width, int height, uint16_t color);
void (*drawText)(int x, int y, const char* text, uint16_t color);
void (*update)();
} DisplayRenderer;drawBoard()- Draw the board, the landing ghost and the current piecedrawNextPiece()- Show preview of the next piece, centred on its bounding boxdrawStats()- Display score, level, and lines cleareddrawGameState()- Show game over or paused messagedrawGame()- Main drawing function that calls all others
- PicoSystem initialization
- PicoSystem input handling
- PicoSystem lifecycle functions (init, update, draw)
- SDL initialization
- SDL input handling
- SDL main loop with timing control
Needs SDL3 and a C/C++ compiler.
cmake -S . -B build_sdl
cmake --build build_sdl
./build_sdl/picosystem-tetrisNeeds the ARM bare-metal toolchain, the
Pico SDK (2.x) and the
PicoSystem SDK (main, the v1.0.0
tag predates Pico SDK 2.x and will not build against it).
# Arch; see the Pico SDK docs for other distributions
sudo pacman -S arm-none-eabi-gcc arm-none-eabi-newlib arm-none-eabi-binutils
# Clone both SDKs next to this repo
cd ..
git clone https://github.com/crustovsky/picosystem-tetris.git
git clone --depth 1 --branch 2.3.0 https://github.com/raspberrypi/pico-sdk.git
git clone --depth 1 https://github.com/pimoroni/picosystem.git
cd picosystem-tetris
cmake -S . -B build_pico -DUSE_PICOSYSTEM=ON
cmake --build build_picoCMakeLists.txt picks up pico-sdk and picosystem automatically when they
sit next to the repo; set PICO_SDK_PATH if yours live elsewhere. The Pico SDK
builds picotool on first configure to produce the .uf2, which needs
libusb-1.0 development headers.
Use Pico SDK 2.3.0 or newer on a modern host compiler. Earlier 2.x versions
fail to build the pioasm host tool under GCC 15/16, because recent libstdc++
no longer includes <cstdint> transitively; 2.3.0 adds the missing include.
This produces build_pico/picosystem-tetris.uf2; flash it as described under
Download.
- Left/Right/Down: Move piece
- A: Rotate piece
- Y: Hard drop
- B: Pause/Unpause
- X: Restart after game over
- Arrow Keys: Move and rotate piece
- Space: Hard drop
- P: Pause/Unpause
- R: Restart
- ESC: Quit
The SDL window opens at 3x scale (720×720) and is resizable; drawing always happens in 240×240 coordinates and is integer-scaled to fit, so the desktop build is pixel-identical to the PicoSystem one.
This project uses an abstraction layer for display rendering to make it easy to port to different platforms. The display renderer interface is defined in tetris_display.h and includes functions for basic drawing operations.
To add support for a new platform:
- Create a new renderer implementation file (e.g.,
tetris_display_myplatform.c) - Implement the
DisplayRendererinterface functions for your platform - Add a function to get your renderer (e.g.,
getMyPlatformRenderer()) - Update
tetris_display_renderers.hto expose your new renderer - Update CMakeLists.txt to include your platform-specific build options
- Frame buffer: 240×240×2 bytes (115.2 KB) for 16bpp color
- Game state: Minimal (~1 KB)
- Code size: Small, suitable for embedded applications
- Drawing optimizations for speed (using primitives like
fillRect) - Minimal dynamic memory allocation
- Efficient update logic with time-based movement
.github/workflows/build.yml builds the PicoSystem firmware on every push to
main and every pull request, uploading the .uf2 as a build artifact. Pushing
a v* tag additionally publishes it to a GitHub release:
git tag v1.0.0 && git push origin v1.0.0SDK versions are pinned in the workflow so builds stay reproducible. Only the firmware is built in CI; the desktop SDL build is for local development.
MIT, see LICENSE.