Stealth Game

A stealth game on a $30 development board. Tilt it to move, tap to throw a sound bomb, and hold a finger down to see where the guards will walk. A hundred stages, ranked by how hard they measurably are, running at fifty frames a second on a microcontroller.

Stage 100 of 100, cleared in 19.1 seconds. This is not a screen recording. An autopilot played it through the game’s own C code, compiled for a laptop, and the soundtrack is the board’s synthesiser rendering the same run. Unmute for the heartbeat.

The game

Sound is the currency

Guards walk patrols across a tile maze, sweeping vision cones ahead of them. You have to free every hostage and reach the exit without ever being fully seen. The premise comes from Stealth, a realtime-tactics puzzler on Steam. This version is rebuilt around the one input a handheld board has that a desktop game doesn’t: which way you’re holding it.

Tilt is the joystick, and speed is continuous. A slight lean creeps; a hard lean sprints at 205 pixels a second, and past about seventy percent of full tilt your footsteps start to carry. Noise doesn’t radiate in a circle. It flood-fills through open floor, so it rounds corners but never passes through walls, and that makes a sound bomb a real tool: one thrown down a side corridor genuinely pulls a guard away from the door it was watching.

Detection is a meter, not a tripwire. Being clipped by the edge of a cone for a moment is survivable; standing in the middle of one is not. The meter traces the outline of the panel itself, starting at the bottom, running up both sides and closing at the top at the instant you’re identified, so it reads in peripheral vision without looking away from the guard. The heartbeat in the speaker climbs with it. And once you’re seen, running won’t save you: a chasing guard moves at least as fast as you do, so the only way out is to break line of sight.

Controls
Tilt the boardMove. The steeper the lean, the faster, and the louder.
Tap the fieldThrow a sound bomb where you tapped.
Press and holdReveal every guard’s patrol route.
BOOT buttonPause: resume, re-level the tilt, restart, or quit.
An A to Z keypad with two initial slots and a GO keyTwo initials, for the records
The title screen: Stealth, see without being seen, with the controls listedEverything you need to know
The briefing for stage 100 of 100, Rough Market: rescue 4Stage 100: rescue four
The maze with every guard's patrol drawn as a thin grey lineHold to see every route
A sound bomb's expanding ring, with a guard marked by a question mark heading towards itA bomb, and a guard who heard it
A guard's cone turned amber on the player, with a red line tracing the bottom and sides of the panelA cone goes hot; the meter climbs
A thin green ring collapsing across the whole maze onto the exitLast hostage: the exit opens
Clear, 0:19.1, new record, TSCleared, with a new record
The pause menu: resume, re-level, restart, quitThe button pauses anywhere
Every screen above came out of the game’s own renderer, most of them from the same run as the video.

The device

Waveshare ESP32-S3-Touch-AMOLED-1.8 · $29.99

It’s the same board as AI Pixel Cat, for the same reason: everything a handheld game needs is already on it. A bright AMOLED panel, touch, an accelerometer, a speaker, a button, and flash to keep records in. There’s no wiring and nothing to add.

What’s on board, and what the game uses it for
HardwarePartWhat it became
Display1.8″ AMOLED, 368×448, over QSPIThe whole play field, redrawn every frame. The alert meter follows its rounded corners.
MotionQMI8658 6-axis IMUThe joystick. Tilt is read from the accelerometer.
TouchCapacitive, FT3168 or CST816Tap to throw, hold to reveal routes, and every menu.
AudioES8311 codec and speakerAlarm, heartbeat and music, all synthesised live.
ButtonBOOT, on the sideThe pause menu, on any screen.
SoCESP32-S3R8, 8 MB PSRAM, 16 MB flash449 KB of firmware. Frames are drawn in the chip’s internal RAM.
StorageFlashYour initials, and the best time on every stage.

Two revisions of the board ship under the same SKU, with different display and touch chips. The firmware probes for the touch controller at boot and adapts, so one binary runs on both.

The requirements

A complete game on one board

We defined Stealth Game around the hardware: a complete handheld game with no wiring or extra controls. Tilt had to feel natural, touch had to make sound bombs easy to aim, and the display and speaker had to make danger clear without pulling attention away from the maze. The game needed a hundred stages with a measured difficulty curve, responsive movement, and records that survive a power cycle.

Those requirements shaped the engineering rules:

The safety net is one command, ./tools/check.sh, which runs six suites in a few seconds with no board attached: the stage tables, input and menu geometry, patrol-route rendering, a smoke test of all hundred stages, a long numerical soak, and tilt filtering.

Fifty frames a second

Where a 19 millisecond frame goes

The whole 368×448 panel is redrawn every frame by a software rasteriser. There’s no GPU and no sprite hardware. The obvious design, double-buffered frames in the board’s 8 MB of PSRAM, didn’t survive contact: DMA reading one frame while the CPU draws the next saturates the PSRAM bus, and the display link underruns. So the frame is drawn in horizontal bands in the chip’s own internal RAM, two small buffers ping-ponging, one being drawn while the other streams to the panel.

The lesson

Every band costs a synchronous round trip to the panel before its pixels can stream. Going from fourteen bands to four took the game from 34.5 to 50 frames a second, more than every CPU optimisation combined. The frame is now limited by the bus, not the processor, and the one lever left is the display clock, which can only be judged by looking at the panel.

Pitched for a small speaker

No samples, everything synthesised

There are no audio files in the firmware. An alarm, a heartbeat, a music bed and a handful of effects are synthesised live at 22 kHz on the chip’s second core. The game and the synth never share a lock, so a slow frame can never stall the sound.

The first version made the obvious mistake: a 36 Hz drone and a 62 Hz heartbeat thump, which sound great on headphones. On the board, nothing. The speaker rolls off hard below about 300 to 400 Hz, and almost all of the music’s energy sat below 250. The fix borrows a trick from psychoacoustics: every low sound is built from a fundamental plus its second and third harmonics. The harmonics land where the speaker can move air, and the ear fills in the missing fundamental.

Listen

The music bed, then the heartbeat as tension climbs from calm to caught, rendered by the same synthesiser code the board runs.

The detection alarm is the loudest sound in the game, and it fires on the exact frame a guard’s cone turns hot. One flag drives both the colour and the sound, so they can never drift apart.

A hundred stages, measured

Difficulty is a model, not a guess

All hundred stages are generated, and the generator’s real job isn’t making mazes. It’s knowing how hard each one is. Guard count, hostage count and wall density are the obvious knobs, and on their own they’re weak predictors. What gets scored is how hard a stage is to actually move through:

Coverage over time
Each guard is walked around its whole patrol, pauses included, with its real vision cone clipped by walls. A tile’s coverage is the fraction of the cycle it’s watched. A tile glimpsed once and a tile watched constantly aren’t the same, though a map of every cone position would score them identically.
Unavoidable chokepoints
Tiles you can’t route around: remove one and the exit is unreachable. You can only time these, which is the hardest thing the game asks.
Cover
How far the route runs from ground nobody watches. A chasing guard matches your speed, so a route with no cover beside it has no recovery from a mistake.
Relief
Bombs per guard, not bombs. Three against seven is scarcity.

The one hard rule

No stage can be finished without crossing ground a guard watches. The player chooses the route, and will choose whichever keeps its worst moment lowest, so the generator solves for exactly that: for every pair of objectives, the route whose most-watched tile is least watched, tried over every order of rescuing the hostages. If that number is zero, there’s a way through that never enters a cone, and the stage is thrown away.

Averaging exposure along one chosen route wouldn’t catch it. A stage can look dangerous on the direct path and still have a completely safe detour beside it.

0 0.25 0.5 0.75 1.0 Stage 1 25 50 75 100 Forced crossing Difficulty score
Stages are sorted by difficulty score, so it rises by construction. The forced crossing, the most-watched ground the safest route has to cross, as a fraction of a patrol cycle, is one of its inputs and wanders more. The red dots mark the stages the video’s autopilot fails. It follows the shortest path, waits in cover when the next step is watched, and throws a bomb when a guard parks on its route; it’s caught on 4 of the first fifty stages and 15 of the second fifty, an independent check that the ramp is real.
All hundred stages as a table

The generator is deterministic, so regenerating reproduces the same hundred stages byte for byte. Records are keyed to a fingerprint of the stage table, so a new set of stages can never quietly inherit the old set’s best times.

The code

C, ESP-IDF 5.5, 449 KB of firmware, MIT licensed

Everything is in the repository: the game, the stage generator and its difficulty model, the test suite, and the harnesses that render frames and audio without a board, including the recorder that made the video at the top of this page. A bootstrap script installs the toolchain, builds the firmware and runs the tests on a fresh Mac.

View the repository on GitHub