An AI-powered text RPG on a $10 microcontroller.
Version 1.0.0
Pocket RPG is a dark fantasy text RPG that runs entirely from flash on the M5Stack Basic v2.7 (ESP32). An AI narrator drives every turn -- generating narrative text, selecting pixel-art scenes, spawning enemies, and setting the mood -- all rendered on a 320x240 TFT display. The result looks and feels like a Game Boy Color RPG.
The game world is Vaeloria. You create a character, choose a class, name your hero, and venture into dungeons, forests, ruins, and taverns. Every decision you make is sent to an AI backend (OpenRouter API) which returns structured JSON that controls what you see, hear, and experience.
| Main Menu | Forest Exploration |
|---|---|
![]() |
![]() |
| Dungeon Combat | Tavern Rest |
|---|---|
![]() |
![]() |
| Death Screen |
|---|
![]() |
Building this game required solving hard problems at every layer of the stack on a device with no operating system, no GPU, and less RAM than a 1989 Game Boy:
| Constraint | What it means |
|---|---|
| 123KB usable heap | MicroPython VM + WiFi + SSL + game engine + renderer + assets must all coexist in 123KB. Every byte is accounted for. |
| No PSRAM | Unlike ESP32-S3 or M5Stack Fire, the Basic v2.7 has zero external RAM. What you see is what you get. |
| HTTPS on 20KB free heap | After loading game modules, only ~20KB remains. SSL/TLS handshakes need ~40-50KB. Required pre-compiled bytecode (.mpy), aggressive gc.collect(), deferred imports, and memory-aware boot sequencing. |
| Shared SPI bus | The display and SD card share a single SPI bus with a mutex lock. Every draw call and every file read must coordinate. |
| No filesystem at boot | UIFlow2 doesn't expose /flash in sys.path. Game modules needed manual path injection before any imports could work. |
| AI integration in 5KB | Full OpenRouter API integration -- system prompt construction, HTTP POST, 3-stage JSON response parsing, conversation history -- in a module that fits in 5KB of compiled bytecode. |
| Cooperative async on single core | The 50Hz game loop runs input polling, engine state machine, animation ticks, and audio ticks in a single-threaded uasyncio loop. Blocking calls freeze everything. |
| RGB565 assets, RGB888 API | BMP files are stored as RGB565 (16-bit) for the ILI9342C display, but the UIFlow2 drawing API accepts RGB888 (24-bit). Every color constant lives in two worlds. |
The Characters persist across sessions. The AI generates unique adventures every turn. Sprites animate, scenes transition, dice roll on shake, and mood-driven ambient tones play through the speaker -- all on a $10 chip with 123KB of breathing room.
- AI-driven narration -- Every turn generates unique narrative text, scene changes, enemy encounters, and game state deltas via the OpenRouter API
- 4 character classes -- Warrior, Mage, Rogue, and Ranger with distinct stats and playstyles
- Pixel-art scene system -- 13 background BMPs driven by AI scene selection
- Sprite compositing -- 48x48 player and enemy sprites with frame animation and transparency keying
- Animated d20 dice rolls -- Shake the device to roll; result is injected into the AI prompt
- Typewriter text reveal -- Narrative text scrolls in character-by-character in the dialog panel
- Screen transition animations -- Scene wipes, screen flashes, HP drain, level-up bursts, death sequences
- Ambient mood audio -- PWM tone system with mood-driven ambient soundscapes (calm, tense, combat, eerie, triumph, silent)
- SFX library -- Button clicks, combat hits, level-up fanfares, death knells, dice rolls, item pickups
- IMU gesture input -- Shake detection for dice rolls, tilt detection for scrolling
- Offline fallback mode -- Branching adventure tree from offline.json when WiFi is unavailable
- 3-stage JSON parsing -- Robust AI response parser with direct parse, whitespace strip, and brace extraction fallback
- Save/load system -- Up to 3 save slots with conversation history preservation
- Custom bitmap font engine -- Large readable font rendered from a font atlas BMP
- Settings menu -- In-game configuration for text speed, volume, tilt sensitivity, and save slot
- Character overlays -- Full-screen stats and inventory screens accessible via long-press
- Flash-only deployment -- All code and assets are flashed to the device; no SD card required
| Component | Spec | Role |
|---|---|---|
| Device | M5Stack Basic v2.7 | Game platform |
| MCU | ESP32-D0WDQ6, 240MHz dual-core, 520KB SRAM | Game logic, WiFi, rendering |
| Flash | 16MB | 3MB app + 12.9MB VFS for assets |
| Display | ILI9342C, 320x240, RGB565, SPI @ 40MHz | All visuals |
| Buttons | 3x tactile (A/B/C) on GPIO 39/38/37 | All player input |
| IMU | MPU6886 6-axis accelerometer + gyroscope | Shake = dice roll, tilt = scroll |
| Speaker | DAC via GPIO25, mono | SFX and ambient tones |
| WiFi | 802.11 b/g/n 2.4GHz | OpenRouter API calls |
| MicroSD | SPI slot (OPTIONAL) | Override assets or config if present |
- Firmware: UIFlow2 v2.4.2 for M5Stack Basic v2.7 (16MB flash)
- Runtime: MicroPython v1.25.0 (bundled with UIFlow2)
- Deploy tools: Python 3.8+ with
mpremoteandesptool(pip install mpremote esptool) - API Key: OpenRouter account with an API key (optional -- offline mode works without it)
- AI Model:
arcee-ai/trinity-large-preview:free(free tier, no cost)
The deploy tool flashes the firmware and uploads your configuration in one step.
python tools/deploy.pyIt will interactively prompt for WiFi credentials, OpenRouter API key, and game settings. The tool detects the serial port automatically on both Windows (COM ports) and Linux (/dev/ttyUSB*).
Options:
python tools/deploy.py --port COM3 # Specify serial port
python tools/deploy.py --config-only # Skip firmware flash
python tools/deploy.py --flash-only # Skip config upload
Requires: pip install esptool mpremote
If you want to build the firmware yourself (requires WSL or Linux with ESP-IDF):
./firmware/build.sh setup # First time: install toolchain
./firmware/build.sh build # Build firmware
./firmware/build.sh flash # Flash to device via USB
./firmware/build.sh all # setup + build + flashThe build script freezes all Python source into ROM and writes all assets into a 12.9MB VFS partition.
Hold BtnA (leftmost) during power-on to return to the UIFlow2 startup menu.
Settings are loaded from config.json on the flash VFS. Missing or invalid values fall back to defaults. Numeric values are clamped to valid ranges.
| Key | Type | Default | Range | Description |
|---|---|---|---|---|
wifi_ssid |
string | "" |
-- | WiFi network name |
wifi_password |
string | "" |
-- | WiFi password |
openrouter_api_key |
string | "" |
-- | OpenRouter API key |
text_speed_ms |
int | 40 |
10-200 | Typewriter delay per character (ms) |
volume |
int | 3 |
0-10 | Master volume level |
tilt_sensitivity_deg |
int | 25 |
5-90 | IMU tilt threshold in degrees |
shake_threshold_g |
float | 2.5 |
0.5-10.0 | Shake detection threshold in g-force |
active_slot |
int | 0 |
0-2 | Active save slot |
offline_mode |
bool | false |
-- | Force offline fallback mode |
world_file |
string | "world.txt" |
-- | World lore file name |
ble_enabled |
bool | false |
-- | Enable BLE (experimental) |
| Button | Short Press | Long Press (hold 1.2s) |
|---|---|---|
| A (left) | Select choice 1 / Navigate left | Show character stats overlay |
| B (center) | Select choice 2 / Confirm | Manual save to active slot |
| C (right) | Select choice 3 / Navigate right | Show inventory overlay |
| Button | Action |
|---|---|
| A | Previous character |
| B | Accept current character |
| C | Next character |
| Hold B | Finish name entry |
| Button | Action |
|---|---|
| A | Previous class |
| B | Select class |
| C | Next class |
| Gesture | Action |
|---|---|
| Shake | Roll a d20 dice (sustained >2.5g for 150ms). Result is injected into the next AI prompt. Triggers a d20 animation overlay. |
| Tilt | Tilt detection for UI scrolling (configurable sensitivity). |
| Class | HP | MP | STR | DEX | INT | HP/Level | Weapon |
|---|---|---|---|---|---|---|---|
| Warrior | 25 | 5 | 4 | 2 | 1 | +4 | Sword |
| Mage | 15 | 20 | 1 | 1 | 5 | +2 | Staff |
| Rogue | 18 | 8 | 2 | 5 | 2 | +3 | Dagger |
| Ranger | 20 | 10 | 2 | 4 | 2 | +3 | Bow |
XP progression follows the formula: XP_next = 100 * 1.5^(level - 1)
On death, characters respawn at 30% of max HP (minimum 1).
The game runs as a single-threaded async loop at 50Hz using uasyncio. See
docs/architecture.md for the full technical deep-dive.
+------------------+ 0x000000
| Bootloader |
+------------------+ 0x009000
| NVS (24KB) |
+------------------+ 0x00F000
| PHY init (4KB) |
+------------------+ 0x010000
| |
| App / factory | 3 MB (MicroPython + frozen modules)
| |
+------------------+ 0x310000
| |
| VFS (FAT) | 12.9 MB (scenes, sprites, ui, fonts,
| | config, saves, world data)
| |
+------------------+ 0x1000000 (16 MB)
+-------------------------------------+ y=0
| |
| SCENE ZONE (320x168) | Pixel-art background + sprites
| |
+-------------------------------------+ y=168
| HP bar | MP bar | LVL | WiFi | STATUS STRIP (12px)
+-------------------------------------+ y=180 (gold divider)
| |
| DIALOG PANEL (320x48) | Narrative text, 3 lines max
| |
+----------+----------+---------------+ y=229 (gold divider)
| [A] Verb | [B] Verb | [C] Verb | CHOICE BAR (10px)
+----------+----------+---------------+ y=239
menu-- Main menu (New Game / Continue / Settings)char_create_class-- Class selection carouselchar_create_name-- Character name entrychar_create_roll-- Bonus stat rollidle-- Awaiting player inputai_pending-- Waiting for AI API responseanimating-- Playing turn animationsoverlay-- Displaying stats or inventorysettings-- Settings menudeath-- Death sequence with respawn option
main.py (boot + 50Hz loop)
+-- engine.py (state machine, turn orchestration)
| +-- ai_client.py (OpenRouter API, JSON parsing)
| +-- world.py (RPG classes, stats, deltas)
| +-- fallback.py (offline adventure tree)
| +-- storage.py (flash VFS I/O, save/load)
+-- renderer.py (display controller, all screen zones)
| +-- font_engine.py (bitmap font rendering)
| +-- sprite.py (sprite sheet loading, transparency)
+-- animator.py (typewriter, wipes, flashes, HP drain)
+-- input_handler.py (buttons + IMU polling)
+-- audio.py (SFX + ambient mood system)
+-- config.py (config.json loader with validation)
+-- paths.py (flash/SD path abstraction)
+-- wifi.py (WiFi connection manager)
Pocket-RPG/
README.md
src/
main.py # Boot entry point, 50Hz game loop
engine.py # Game engine state machine
renderer.py # Display controller (all screen zones)
ai_client.py # OpenRouter API client + JSON parser
world.py # RPG class definitions, stat logic
config.py # Configuration loader with validation
paths.py # Flash/SD path abstraction layer
audio.py # SFX and ambient mood audio engine
input_handler.py # Button polling + IMU shake/tilt
fallback.py # Offline branching adventure navigator
animator.py # Animation system (typewriter, wipes, etc.)
sprite.py # Sprite sheet loader with transparency
font_engine.py # Bitmap font atlas renderer
storage.py # Flash VFS I/O, save/load
wifi.py # WiFi connection manager
world.txt # World lore text for AI context
offline.json # Offline adventure tree data
assets/
scenes/ # Background BMPs (320x168)
sprites/ # Character and enemy sprite sheets
ui/ # UI element BMPs (splash, panels, font, d20)
firmware/
build.sh # Custom firmware build script
partitions_16mb_game.csv # Flash partition table
create_game_main.py # Frozen main.py generator
tools/
deploy.py # Interactive deploy tool (flash + config)
generate_builtin_assets.py # Regenerate bundled asset pack
convert_assets.py # PNG to RGB565 BMP converter
docs/
architecture.md # System design deep-dive
configuration.md # Config reference and setup
game-mechanics.md # Classes, combat, XP, inventory
development.md # Contributing guide and coding patterns
Detailed documentation is available in the docs/ directory:
| Document | Description |
|---|---|
| Architecture | System design, state machine, async patterns, flash layout, display zones |
| Configuration | Config reference, API setup, deployment, WiFi, audio, offline mode |
| Game Mechanics | Character classes, combat, XP/leveling, death, inventory, dice rolls |
| Development Guide | Contributing guide, MicroPython constraints, coding patterns, asset specs |
MIT License. See LICENSE file for details.





