Skip to content

Designing the Gameplay Loop — From Design Doc to Program Structure

The User Engagement Plan was 1,380 lines of game design. Today it became 11 structured documents of pseudocode, data structures, and module interfaces that define exactly how the Dilder gameplay firmware will be built.

Why Plan This Now?

The breadboard prototype displays 16 emotional states, cycles through 823 quotes, and reads joystick input. But it doesn't play — there's no stat decay, no care actions, no progression. Phase 3A (core game loop) is next, and implementing it without a program architecture would mean rewriting everything once the pieces don't fit together.

The User Engagement Plan describes what the game does. The Gameplay Planning documents describe how the code does it — structs, function signatures, module boundaries, and data flow.

The Architecture

The firmware is organized as 13 modules that communicate through a lightweight event bus:

Game Loop (orchestrator)
  ├── Stat System         — hunger, happiness, energy, hygiene, health
  ├── Emotion Engine      — 16 emotions resolved from stats + sensors
  ├── Sensor Manager      — HAL for 7 sensors (light, touch, mic, temp, accel, GPS, mag)
  ├── Input & Menu & UI   — buttons, menu FSM, e-ink screen composition
  ├── Life Stages          — egg → hatchling → juvenile → adolescent → adult → elder
  ├── Progression          — XP, bond levels, 20+ achievements, decor unlocks
  ├── Dialogue             — context-aware quote selection with intelligence gating
  ├── Activity Tracker     — steps, daily/weekly/monthly targets, streaks
  ├── Treasure Hunts       — GPS-based spawning, compass navigation, collection
  ├── Decor & Cosmetics    — 4 equip slots, unlock inventory
  ├── Persistence          — LittleFS save/load with CRC integrity + backup
  └── Time Manager         — RTC wrapper, day/night, age tracking

Every module owns its own state and exposes a clean public API. No module reaches into another's internals. Side effects flow through events — when the stat system detects a critical hunger level, it fires EVENT_STAT_CRITICAL, and the emotion engine, dialogue system, and UI each handle it independently.

The Core Loop

The game runs on a 1-second base tick. Not all systems need to run every tick — sensors poll at different rates to spread CPU load:

System Rate Why
Button input Every tick (1s) Responsiveness
Mic level Every tick Volume tracking for sound events
Stat decay Every tick Fractional accumulators need consistent timing
Emotion resolution Every 5s E-ink can't refresh faster anyway
Touch sensor Every 10s Petting detection doesn't need sub-second resolution
Light sensor Every 10s Ambient light changes slowly
Accelerometer Every 10s Read hardware pedometer register
Temperature Every 60s Room temperature changes on minute timescales
Dialogue check Every 30s Idle dialogue trigger
Flash save Every 5 min Wear leveling — don't write flash every second

Buttons are the exception: they use GPIO interrupts for instant response, with a ring buffer that the main loop drains each tick.

Stat Decay — The Math

Stats decay fractionally. Hunger drops 1 point every 10 minutes, but the game ticks every second. A fixed-point accumulator handles this without floating-point drift:

// Base decay: 1 point per 600 ticks (10 minutes)
// Accumulator threshold: 600,000 (600 * 1000 for fixed-point precision)
accum.hunger += base_rate * stage_modifier * bond_modifier * env_modifier;
if (accum.hunger >= 600000) {
    stats.hunger -= 1;
    accum.hunger -= 600000;
}

Modifiers stack multiplicatively — a hatchling (2x decay) in hot weather (1.3x) during active hours (1.2x) decays at 3.12x base rate. An elder (0.6x) with a strong bond (0.85x) at night (0.8x) decays at only 0.41x.

Emotion Resolution — Weighted Priority

The emotion engine evaluates all 16 emotions every 5 seconds. Each has a trigger function that returns a weight (0.0–1.0) based on current stats, sensor readings, and recent events. The highest weight wins, with a hysteresis margin of 0.15 to prevent flickering:

Example tick:
  Hungry weight:  0.8  (hunger at 15/100)
  Tired weight:   0.3  (energy at 40/100)
  Current:        Chill (0.6)

  Hungry (0.8) > Chill (0.6) + hysteresis (0.15) = 0.75
  → Transition to Hungry

Each emotion has a minimum dwell time (10–60 seconds) to prevent rapid switching on e-ink. Forced overrides handle immediate responses — feeding a hungry pet forces 10 seconds of Excited before returning to normal resolution.

Evolution — Deterministic, Not Random

When the pet reaches adulthood (day 14), its accumulated stats determine which of 6 adult forms it evolves into. This is fully deterministic — the player's care pattern produces a knowable outcome:

Adult Form Key Drivers
Deep-Sea Scholar Intelligence + bond + discipline
Reef Guardian Fitness + exploration
Tidal Trickster Low discipline + high happiness
Abyssal Hermit High discipline + low social
Coral Dancer Happiness + music exposure
Storm Kraken Lots of scolding + survived

Each form is scored numerically. The highest score wins. A heritage bias from rebirth gives a slight nudge toward the parent's form, creating multi-generational lineage.

MCU Migration: Write Once, Port Later

The ESP32-S3 board is in PCB design. The firmware is being written for the Pico W now, but 7 of 13 modules (stat system, emotion engine, life stages, progression, dialogue, decor, event bus) are pure C with zero hardware calls — they'll compile identically on the ESP32-S3 without a single change.

Hardware-touching code goes through a HAL abstraction layer. Sensor drivers call hal_i2c_read() instead of Pico SDK's i2c_read_blocking(). When the ESP32-S3 board arrives, only the HAL implementations swap — not the gameplay logic, not the sensor protocols, not the UI rendering.

The biggest structural change is FreeRTOS: the ESP32-S3 requires it, turning the bare-metal while(true) game loop into a FreeRTOS task. The game logic inside that task stays the same.

AHT20 Replaces BME280

One hardware update made during this planning session: the temperature/humidity sensor changed from BME280 ($4, I2C 0x76) to AHT20 ($0.43, I2C 0x38). The AHT20 is a JLCPCB basic part, has better accuracy (+/-0.3C vs +/-1C), uses less power during measurement (23uA vs 350uA), and costs less than a tenth of the price. The only tradeoff is losing the barometric pressure sensor, which wasn't being used for gameplay anyway.

All documentation — engagement plan, PCB research, sensor interfaces, and this blog — has been updated to reflect the change.

The Documents

The full gameplay architecture lives in 11 planning documents:

Doc Covers
00 — Architecture Overview Module map, data flow, memory budgets, file structure
01 — Core Game Loop Tick system, game states, sleep/wake, event bus
02 — Stat System Decay math, care actions, modifiers, thresholds
03 — Emotion Engine 16 trigger functions, blending, transitions
04 — Sensor Interfaces HAL for 7 sensors, polling rates, event classification
05 — Input, Menu & UI Button debounce, menu FSM, screen rendering
06 — Life Stages & Evolution Stage FSM, evolution scoring, rebirth
07 — Progression & Unlocks XP, bond levels, achievements, decor
08 — Dialogue System Quote selection, context triggers, intelligence gating
09 — Persistence & Storage Flash layout, save/load, wear leveling
10 — Activity Tracker Steps, targets, streaks, location, treasure hunts
11 — MCU Migration Impact RP2040 → ESP32-S3 porting guide

Next step: implement Phase 3A — the core game loop with stat decay, care actions, and emotion resolution. The architecture is designed. Time to write the firmware.