Skip to content

Latest commit

 

History

History
606 lines (428 loc) · 23.1 KB

File metadata and controls

606 lines (428 loc) · 23.1 KB

Pocket RPG -- Configuration and Setup Guide

This document covers every configurable aspect of Pocket RPG: the config file, API integration, WiFi, audio, input tuning, display, save system, flash storage layout, world lore, and offline mode.


Table of Contents

  1. config.json Reference
  2. Setting Up Configuration
  3. API Setup
  4. WiFi Configuration
  5. Audio Settings
  6. Input Tuning
  7. Display Settings
  8. Save Slots
  9. Flash Storage
  10. World Lore
  11. Offline Mode

1. config.json Reference

Configuration is loaded from /flash/config.json at boot. If the file is missing or contains invalid JSON, all defaults are used silently. Unknown keys are ignored. Out-of-range numeric values are clamped to the nearest valid bound.

The paths module auto-detects the base path at startup, probing /flash first, then / as a fallback. All game code uses paths.asset() to resolve file locations, so the config file is always found at the correct base path.

All Config Keys

Key Type Default Valid Range Description
wifi_ssid string "" any string WiFi network name (SSID)
wifi_password string "" any string WiFi network password
openrouter_api_key string "" any string OpenRouter API key for AI narrative generation
text_speed_ms int 40 10 -- 200 Delay in milliseconds between each character in the typewriter text animation
volume int 3 0 -- 10 Master volume level (0 = mute, 10 = maximum)
tilt_sensitivity_deg int 25 5 -- 90 Tilt angle in degrees required to trigger a tilt event from the IMU
shake_threshold_g float 2.5 0.5 -- 10.0 Acceleration magnitude (in g) required to register a shake gesture
active_slot int 0 0 -- 2 Which save slot is used for saving and loading
offline_mode bool false true/false Force offline mode (uses offline.json adventure tree instead of API)
world_file string "world.txt" any string Filename of the world lore file (relative to flash root)
ble_enabled bool false true/false Enable Bluetooth Low Energy (reserved for future use)

Validation Rules

Numeric keys are validated and clamped automatically:

  • If a value cannot be coerced to its expected type (int or float), the default is used.
  • If a value falls outside the valid range, it is clamped to the nearest bound. For example, "volume": 15 becomes 10, and "volume": -3 becomes 0.
  • String and boolean keys are not validated beyond type checking.

Example config.json

{
  "wifi_ssid": "MyNetwork",
  "wifi_password": "secret123",
  "openrouter_api_key": "sk-or-v1-abc123...",
  "text_speed_ms": 40,
  "volume": 5,
  "tilt_sensitivity_deg": 25,
  "shake_threshold_g": 2.5,
  "active_slot": 0,
  "offline_mode": false,
  "world_file": "world.txt",
  "ble_enabled": false
}

2. Setting Up Configuration

There are two ways to get your config onto the device.

Option A: deploy.py (recommended)

The interactive deployment tool prompts for WiFi, API key, and game settings, then uploads config.json to the device over serial:

python tools/deploy.py --config-only

This writes /flash/config.json via mpremote (or raw serial REPL as a fallback). You can also specify a port explicitly:

python tools/deploy.py --config-only --port COM3

If the upload fails, deploy.py saves the config locally as config.json in the project root and prints a manual upload command.

Option B: Thonny / mpremote (manual)

Edit config.json in a text editor, then upload it to the device:

python -m mpremote connect <PORT> cp config.json :/flash/config.json

Or use Thonny's file manager to drag the file to /flash/config.json on the device.


3. API Setup

Pocket RPG uses the OpenRouter API to generate AI-driven narrative, choices, and game state changes each turn.

Endpoint and Model

Setting Value
API URL https://openrouter.ai/api/v1/chat/completions
Model arcee-ai/trinity-large-preview:free
Request timeout 20 seconds

The model and endpoint are hardcoded in src/ai_client.py. To change them, edit the _MODEL and _API_URL constants at the top of that file.

API Key

Set openrouter_api_key in /flash/config.json to your OpenRouter API key. Free keys are available at https://openrouter.ai/keys. If the key is empty or missing, AI turns are skipped and a fallback response is used instead.

When running tools/deploy.py, you are prompted for the key interactively. If no key is provided, deploy.py automatically enables offline mode.

Request Headers

Every API request includes:

Header Value
Authorization Bearer <your_api_key>
Content-Type application/json
HTTP-Referer https://pocket-rpg.local
X-Title Pocket RPG

System Prompt

The system prompt instructs the AI to respond as a dark-fantasy RPG narrator for the world of Vaeloria. It enforces:

  • Maximum 57 characters across 3 lines of 19 characters each (to fit the 320x240 display).
  • Responses must be valid JSON only -- no preamble, no markdown.
  • JSON must include: narrative, choices (3 items), scene_id, enemy_id, mood, and game_state_delta.

The system prompt is assembled dynamically each turn by build_system_prompt(), which injects:

  • Character stats (HP, MP, STR, DEX, INT, level, XP, inventory, location)
  • World flags (persistent state markers set by the AI)
  • World lore text (from world.txt)
  • Dice result (if the player shook the device to roll a d20)

Response Parsing

AI responses go through a 3-stage JSON parser:

  1. Direct json.loads() on the raw content
  2. Strip whitespace, then json.loads()
  3. Extract the substring from the first { to the last }, then json.loads()

If all three stages fail, a hardcoded fallback response is used.

Conversation History

The History class maintains up to 16 user/assistant message pairs. When the limit is exceeded, the oldest pair is dropped. History is included in every API request and is persisted in save files.

Failure Handling

After 3 consecutive API failures (MAX_API_FAILS), the engine switches to offline fallback mode automatically (using offline.json). The failure counter resets on any successful API call.


4. WiFi Configuration

Setup

Set wifi_ssid and wifi_password in /flash/config.json, either manually or via python tools/deploy.py --config-only. WiFi connection is attempted once during the boot sequence.

Timeout Behavior

Setting Value
Default timeout 15,000 ms (15 seconds)
Poll interval 100 ms

The connection function polls network.WLAN.isconnected() every 100ms until either the connection succeeds or the timeout is reached.

Connection Flow

  1. Activate the station interface (STA_IF)
  2. Disconnect any existing connection
  3. Call connect(ssid, password)
  4. Poll for up to 15 seconds
  5. On success: log IP address, set WiFi status indicator to green
  6. On failure: log error, set WiFi status indicator to red

Offline Fallback

If WiFi fails to connect (or wifi_ssid is empty), the game still boots normally. The status strip shows a red WiFi indicator. API calls will fail, and after 3 consecutive failures the engine switches to the offline adventure tree.

If offline_mode is set to true in config, the API is never called regardless of WiFi status.

Status Indicator

A small circle in the top-right of the status strip shows connection state:

  • Green: WiFi connected
  • Red: WiFi not connected

5. Audio Settings

Volume

The volume config key controls master volume on a scale of 0 to 10. This maps linearly to the hardware range of 0 to 255:

Volume Level Hardware Value Description
0 0 Mute
1 25 Minimum audible
5 128 Mid-range (approximately)
10 255 Maximum

The ambient/mood channel is additionally capped at 20% of maximum (hardware value 51) to keep background drones from overpowering SFX.

Channel Layout

Channel Purpose Volume
0 SFX (button clicks, combat, items) Full master volume
1 Ambient mood (background drones) Capped at 20% (hardware value 51)

SFX Definitions

Each SFX is a list of (frequency_hz, duration_ms) tuples played sequentially. A frequency of 0 means silence (rest).

SFX Name Pattern When Played
SFX_BUTTON_CLICK 1200 Hz, 20ms Any button press
SFX_CONFIRM 800 Hz 60ms, 1200 Hz 80ms Confirm actions, boot complete
SFX_COMBAT_HIT 150 Hz 80ms, 120 Hz 60ms Enemy takes damage
SFX_COMBAT_MISS 900 Hz 40ms, 700 Hz 40ms Attack misses
SFX_PLAYER_HURT 200-160 Hz descending with rest Player takes damage
SFX_ENEMY_DEATH 600-200 Hz descending Enemy defeated
SFX_LEVEL_UP C5-E5-G5-C6 arpeggio Level up
SFX_DEATH 300-100 Hz slow descent Player death
SFX_ITEM_GET 1047 Hz 60ms, 1319 Hz 80ms Item acquired
SFX_SAVE 600 Hz 40ms, 800 Hz 60ms Game saved
SFX_MENU_MOVE 900 Hz, 15ms Menu navigation
SFX_DICE_ROLL 400-1200 Hz rapid ascending Shake to roll d20

Mood BGM System

The mood system provides ambient background drones that loop on channel 1. The AI sets the mood each turn via the mood field in its response.

Mood Tone(s) On Duration Off Duration Description
calm 80 Hz 1000 ms 1000 ms Slow, steady low drone
tense 65 Hz 250 ms 250 ms Fast pulsing sub-bass
combat 55 Hz 62 ms 62 ms Rapid heartbeat-like pulse
triumph 523, 659, 784 Hz 200 ms 0 ms C-E-G arpeggio, continuous
eerie 45 Hz 150-550 ms 300-1200 ms Irregular timing via PRNG
silent -- -- -- No ambient sound

The eerie mood uses a linear congruential PRNG to vary on/off durations each cycle, creating an unsettling irregular rhythm.


6. Input Tuning

Hardware

  • Buttons: A (GPIO39), B (GPIO38), C (GPIO37) -- active LOW
  • IMU: MPU6886 6-axis accelerometer/gyroscope, I2C address 0x68

Timing Constants

Constant Value Description
DEBOUNCE_MS 50 ms Minimum time between accepted button events
LONG_PRESS_MS 1200 ms Hold duration to trigger a long press
IMU_POLL_INTERVAL Every 3rd tick IMU reads at ~17 Hz (main loop runs at 50 Hz)
SHAKE_SUSTAIN_MS 150 ms Duration acceleration must exceed threshold to register a shake

Tilt Sensitivity

The tilt_sensitivity_deg config key (default: 25, range: 5--90) controls how far the device must be tilted before a tilt event registers.

  • Pitch angle is calculated as: atan2(ay, sqrt(ax^2 + az^2))
  • Positive pitch (tilted toward you) = down
  • Negative pitch (tilted away from you) = up
  • If the absolute pitch is below the threshold, tilt direction is None

Lower values make tilt more sensitive. Higher values require a more deliberate tilt.

Shake Threshold

The shake_threshold_g config key (default: 2.5, range: 0.5--10.0) controls the acceleration magnitude required to begin tracking a shake.

  • Acceleration magnitude: sqrt(ax^2 + ay^2 + az^2)
  • The magnitude must exceed the threshold continuously for SHAKE_SUSTAIN_MS (150 ms)
  • Once fired, the shake callback does not fire again until acceleration drops below the threshold and then exceeds it again

Lower values make shake easier to trigger. A value of 1.0 is near resting gravity and will produce false positives. Values above 5.0 require vigorous shaking.

Button Mapping

Context Button A Button B Button C
Gameplay Choice 1 Choice 2 Choice 3
Gameplay (long press) Show stats Manual save Show inventory
Name entry Previous character Accept character Next character
Name entry (long press B) -- Finish name --
Class selection Previous class Select class Next class
Main menu New Game Continue Settings
Settings Change value Save settings Next setting

Shake Gesture

Shaking the device during gameplay triggers a d20 dice roll. The result (1--20) is injected into the next AI prompt as "DICE: player rolled d20, result: N". A d20 spin animation plays on screen if the d20 spritesheet is loaded.


7. Display Settings

Text Speed

The text_speed_ms config key (default: 40, range: 10--200) controls the typewriter animation speed in the dialog panel. This is the delay in milliseconds between each character appearing.

Value Effect
10 Very fast, nearly instant
40 Default, readable pace
100 Slow, deliberate reveal
200 Very slow

Screen Layout

The 320x240 ILI9342C display is divided into fixed zones:

Zone Y Range Height Content
Scene 0 -- 167 168 px Background BMP + sprites
Status Strip 168 -- 179 12 px HP/MP bars, level, WiFi icon
Gold Divider 180 1 px Separator line
Dialog Panel 181 -- 228 48 px Narrative text (3 lines x 19 chars)
Choice Divider 229 1 px Separator line
Choice Bar 230 -- 239 10 px Three button prompts: [A] [B] [C]

Sprite Positions

Sprite X Y Size
Player 8 100 48x48
Enemy 240 100 48x48

8. Save Slots

Overview

The game supports 3 save slots (0, 1, 2). The active_slot config key determines which slot is used.

Save files are stored at /flash/saves/slot_0.json, /flash/saves/slot_1.json, and /flash/saves/slot_2.json. The saves/ directory is created automatically at boot if it does not exist.

Save File Contents

Each save file contains:

Field Type Description
version string Save format version (currently "2.0")
saved_at int Unix timestamp of when the save was made
character dict Full character state (name, class, HP, MP, stats, inventory, etc.)
world_flags dict String-to-bool map of persistent flags set by the AI
history list Conversation history (list of {role, content} message dicts)
current_scene_id string Active scene background ID
current_enemy_id string or null Current enemy, if any
current_mood string Active ambient mood

Atomic Writes

Save files use atomic writes to prevent corruption from power loss:

  1. Write to slot_N.json.tmp
  2. Remove old slot_N.json
  3. Rename .tmp to slot_N.json

If power is lost mid-write, the old save file remains intact.

Auto-Save and Manual Save

  • Manual save: Long-press button B during gameplay. Saves to the active_slot.
  • Auto-save: The engine saves automatically after significant events (level up, death/respawn).

Loading

On the main menu, pressing [B] Continue loads from the active_slot. The load function validates that all required keys are present (character, world_flags, history, current_scene_id, current_mood). Character data is further validated for required fields (name, char_class, hp, max_hp, mp, max_mp, str_stat, dex_stat, int_stat, level, xp, xp_next, inventory, location). If any key is missing, the save is treated as corrupt and ignored. History is capped at 32 messages on load to limit RAM pressure.


9. Flash Storage

All game files are stored in the ESP32's internal flash. The 16MB flash chip is partitioned as 3MB for the application (firmware + frozen Python) and 12.9MB for a VFS FAT filesystem. No SD card is required.

Directory Layout on Flash

/flash/
  config.json          -- Configuration file (WiFi, API key, settings)
  world.txt            -- World lore text
  offline.json         -- Offline adventure tree
  scenes/              -- Scene background BMPs (320x168, RGB565)
    crossroads.bmp
    dungeon_01.bmp
    dungeon_02.bmp
    forest_day.bmp
    forest_night.bmp
    ruins.bmp
    tavern.bmp
    cave.bmp
    castle.bmp
    combat_dungeon.bmp
    combat_forest.bmp
    death.bmp
    levelup.bmp
  sprites/             -- Character and enemy spritesheets (288x48, RGB565)
    warrior.bmp
    mage.bmp
    rogue.bmp
    ranger.bmp
    goblin.bmp
    skeleton.bmp
    wolf.bmp
    troll.bmp
    wizard.bmp
    dragon.bmp
    bandit.bmp
    slime.bmp
  ui/                  -- UI overlay images
    splash.bmp         -- Boot splash screen (320x240)
    status_strip.bmp   -- Status bar background (320x12)
    dialog_panel.bmp   -- Dialog panel background (320x48)
    choice_bar.bmp     -- Choice bar background (320x10)
    font_16.bmp        -- Bitmap font spritesheet (1536x16)
    d20_sheet.bmp      -- D20 dice animation sheet (240x40)
  saves/               -- Auto-created at boot
    slot_0.json
    slot_1.json
    slot_2.json
  logs/                -- Auto-created at boot
    session.log

The saves/ and logs/ directories are created automatically by storage.init_storage() if they do not exist.

Path Detection

The paths module (src/paths.py) auto-detects the base path at startup. It probes directories in this order:

  1. /flash -- standard ESP32 VFS mount
  2. / -- some firmware configurations mount flash at root
  3. /sd and /sdcard -- legacy SD card fallback

The first path containing a scenes/ subdirectory is chosen. All other modules use paths.asset('relative/path') to build full paths, so no module contains hardcoded absolute paths.

BMP Specifications

All BMP files must be RGB565, 16-bit, little-endian, BMP v3 header (54-byte header).

Asset Type Dimensions Notes
Scene backgrounds 320x168 Fills the scene zone (y=0 to y=167)
Character/enemy spritesheets 288x48 6 frames of 48x48, horizontal strip
D20 spritesheet 240x40 6 frames of 40x40, horizontal strip
Splash screen 320x240 Full-screen boot image
Status strip 320x12 Background for the HP/MP bar area
Dialog panel 320x48 Background for the narrative text area
Choice bar 320x10 Background for the button prompt area
Font spritesheet 1536x16 96 glyphs of 16x16 each in a single row

SPI Bus

The display (ILI9342C) uses the SPI bus on M5Stack Basic v2.7. A shared spi_lock (asyncio.Lock) in storage.py must be acquired before any file read or display write operation. All storage and renderer functions acquire this lock. Concurrent access without the lock corrupts the display.


10. World Lore

How It Works

The file specified by world_file (default: world.txt) is read from flash and injected into every AI system prompt under the heading WORLD LORE:.

This gives the AI context about the game world so its narrative responses are consistent with the setting.

Default Lore

The shipped world.txt describes Vaeloria -- a dark-fantasy land of crumbling ruins, haunted forests, and fading civilization. Key locations include the Crossroads, Hearthstone Tavern, goblin-infested caves, and ashen dragon peaks.

Constraints

  • The world lore text is appended to the system prompt in full, so keep it concise.
  • The AI system prompt already specifies a 57-character narrative limit. Longer world lore does not increase the response length, but it does increase the token count of each API request.
  • Aim for approximately 150 words or fewer for the world lore file to keep API costs and latency low.

Custom Worlds

To create a custom world:

  1. Write your lore in a plain text file (UTF-8, no special formatting needed).
  2. Upload it to the device (for example, /flash/my_world.txt).
  3. Set "world_file": "my_world.txt" in /flash/config.json.

The AI will use your custom lore to flavor its narrative responses. You do not need to change any code.


11. Offline Mode

How It Works

When the API is unavailable (no WiFi, no API key, or 3+ consecutive API failures), the game falls back to a pre-authored branching adventure tree loaded from /flash/offline.json.

You can also force offline mode by setting "offline_mode": true in config.

Node Format

The offline adventure is a JSON array of node objects. Each node has the same fields the AI would normally return:

Field Type Required Description
id string yes Unique node identifier (e.g. "n01")
narrative string yes Display text (use \n for line breaks, max 3 lines of 19 chars)
scene_id string yes Background scene to display
enemy_id string or null no Enemy sprite to show (null for no enemy)
mood string yes Ambient mood: calm, tense, combat, triumph, eerie, or silent
choices array yes List of choice objects

Each choice object:

Field Type Description
icon string Icon identifier for the choice
verb string Short action verb (max 8 characters) displayed in the choice bar
next string ID of the node to navigate to when this choice is selected

Example Node

{
  "id": "n04",
  "narrative": "A snarling wolf\nleaps from the\nunderbrush at you!",
  "scene_id": "combat_forest",
  "enemy_id": "wolf",
  "mood": "combat",
  "choices": [
    {"icon": "sword", "verb": "Fight", "next": "n10"},
    {"icon": "shield", "verb": "Defend", "next": "n10"},
    {"icon": "boot", "verb": "Flee", "next": "n01"}
  ]
}

Navigation

  • The first node in the array is the starting node.
  • When the player selects a choice, the engine looks up the next ID and navigates to that node.
  • If a next ID does not exist in the tree, navigation wraps back to the starting node.
  • If the current node somehow becomes invalid, it also resets to the starting node.
  • Offline nodes do not produce game_state_delta, so HP/MP/XP do not change during offline play.

Creating Custom Adventures

  1. Create a JSON array of node objects following the schema above.
  2. Ensure every next reference points to a valid id in the array.
  3. The first node in the array is the entry point.
  4. Use only valid scene_id values: dungeon_01, dungeon_02, forest_day, forest_night, ruins, tavern, cave, castle, crossroads, combat_dungeon, combat_forest, death, levelup.
  5. Use only valid enemy_id values: goblin, skeleton, wolf, troll, wizard, dragon, bandit, slime, or null for no enemy.
  6. Use only valid mood values: calm, tense, combat, triumph, eerie, silent.
  7. Keep narrative text within 3 lines of 19 characters each (57 characters total).
  8. Upload as /flash/offline.json.

The shipped offline.json contains 15 nodes forming a looping adventure through the Crossroads, forest, ruins, caves, dungeons, and tavern.

Valid Scene IDs

crossroads      dungeon_01      dungeon_02
forest_day      forest_night    ruins
tavern          cave            castle
combat_dungeon  combat_forest   death
levelup

Valid Enemy IDs

goblin    skeleton    wolf      troll
wizard    dragon      bandit    slime

Valid Moods

calm    tense    combat    triumph    eerie    silent