|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +**PSIC-O-TRONIC** is a dark humor psychology game running on ESP32-S3 hardware with MicroPython. Players receive fictional clinical cases via AI (Google Gemini) and must choose the "most psychotic" response as an unethical psychologist. The game features an LCD display, physical buttons, LEDs, and audio feedback. |
| 8 | + |
| 9 | +**Hardware Platform:** |
| 10 | +- ESP32-S3 microcontroller |
| 11 | +- 20x4 I2C LCD display |
| 12 | +- 3 input buttons (Up, Down, Select) with 3 corresponding LEDs |
| 13 | +- 1 notification LED |
| 14 | +- Piezo speaker on GPIO 9 |
| 15 | +- I2C on GPIO 1 (SDA) and GPIO 2 (SCL) |
| 16 | + |
| 17 | +**Key Technologies:** |
| 18 | +- MicroPython (not standard Python - limited stdlib, ujson instead of json, urequests instead of requests) |
| 19 | +- Google Gemini AI API for dynamic case generation |
| 20 | +- WiFi connectivity for API access |
| 21 | +- OTA updates from GitHub |
| 22 | +- Persistent storage in ESP32 flash |
| 23 | + |
| 24 | +## Architecture |
| 25 | + |
| 26 | +### Main Game Loop |
| 27 | +`main.py` contains the `PsicOTronic` class which implements a state machine architecture. All game states are defined in the `State` class (lines 56-86). The main loop runs at ~12.5 FPS (`FRAME_DELAY = 0.08`). |
| 28 | + |
| 29 | +**State Flow:** |
| 30 | +1. `BOOT` β `WIFI_CHECK` β `WIFI_PORTAL` (if needed) β `INTRO` β `MENU` |
| 31 | +2. From `MENU`, players can enter: game modes, stats, settings, OTA updates, credits |
| 32 | +3. Game flow: `MODE_SELECT` β `PLAYER_SELECT` β `QUOTA_SELECT` β `PASS_DEVICE` β `FETCHING` β `MESSAGE_ANIM` β `READING` β `CHOOSING` β `FEEDBACK` |
| 33 | +4. Special states: `PAUSE` (triggered by UP+DOWN simultaneously), `ERROR`, `OTA_*` states |
| 34 | + |
| 35 | +### LCD Buffer System |
| 36 | +To prevent flickering, the game uses a double-buffer approach: |
| 37 | +- `lcd_buffer`: Working buffer where content is written |
| 38 | +- `lcd_shadow`: Shadow buffer tracking LCD physical state |
| 39 | +- `_lcd_render()`: Only writes changed characters to physical LCD (lines 268-274) |
| 40 | + |
| 41 | +**Important:** Always write to buffer first (`_lcd_put`, `_lcd_centered`, `_lcd_clear_buffer`), then call `_lcd_render()` to update display. |
| 42 | + |
| 43 | +### Game Modes Architecture |
| 44 | + |
| 45 | +Three game modes share common base (`GameSession` in `game_modes.py`): |
| 46 | + |
| 47 | +1. **Classic Mode** (`MODE_CLASSIC`): Multiplayer (1-4 players), solve N cases with 3 lives per player |
| 48 | +2. **Survival Mode** (`MODE_SURVIVAL`): Single player, survive as long as possible, leaderboard tracking |
| 49 | +3. **Career Mode** ("Mi Consulta"): Complex simulation mode with its own state machine in `career_mode.py` |
| 50 | + |
| 51 | +Career mode is significantly more complex with: |
| 52 | +- Patient management system (`career_patients.py`) |
| 53 | +- Daily scheduling and time simulation (`career_scheduler.py`) |
| 54 | +- Achievement/upgrade/mission systems (`career_systems.py`) |
| 55 | +- Persistent career state (`career_data.py`) |
| 56 | +- Economy, inventory, reputation, crafting, tournaments, etc. |
| 57 | + |
| 58 | +### AI Integration |
| 59 | + |
| 60 | +`gemini_api.py` handles all Gemini API communication: |
| 61 | +- `GeminiOracle.get_scenario()`: Generates clinical cases |
| 62 | +- Uses `PROMPT_BASE` to define game's dark humor style |
| 63 | +- History tracking prevents topic repetition (last 5 themes) |
| 64 | +- Cleanses response text to remove leading punctuation |
| 65 | +- Error scenarios returned when API fails |
| 66 | + |
| 67 | +**API Configuration:** |
| 68 | +- Default API key in `config.py`: `DEFAULT_API_KEY` |
| 69 | +- Model: `gemini-2.0-flash` (configurable) |
| 70 | +- Prompts are cleaned to remove accents/Γ± before sending (MicroPython JSON limitation) |
| 71 | + |
| 72 | +### Configuration & Persistence |
| 73 | + |
| 74 | +`config.py` manages two JSON files in flash: |
| 75 | +- `/config.json`: WiFi credentials, API key/model, settings |
| 76 | +- `/stats.json`: Game statistics, records, career progress |
| 77 | + |
| 78 | +**Memory Safety:** All file operations wrapped in try/except with error reporting to `error_handler.py` |
| 79 | + |
| 80 | +### WiFi & Connectivity |
| 81 | + |
| 82 | +WiFi managed by `wifi_portal.py`: |
| 83 | +1. If no saved credentials, creates AP named "PSIC-O-TRONIC" (no password) |
| 84 | +2. Serves captive portal web interface at 192.168.4.1 |
| 85 | +3. User configures WiFi + optional API key via mobile |
| 86 | +4. Credentials saved to `/config.json` |
| 87 | + |
| 88 | +**Portal Features:** |
| 89 | +- Responsive CSS design for mobile |
| 90 | +- WiFi scanning and selection |
| 91 | +- API key configuration |
| 92 | +- Career mode progress viewing |
| 93 | +- Cancel by holding UP+SELECT buttons |
| 94 | + |
| 95 | +### OTA Updates |
| 96 | + |
| 97 | +`ota_update.py` implements GitHub-based OTA: |
| 98 | +- Repository: `rodillo69/psic-o-tronic` (main branch) |
| 99 | +- Version tracked in `/version.json` |
| 100 | +- Downloads files listed in remote `version.json` |
| 101 | +- Accessible from main menu if updates detected |
| 102 | +- Can force update or skip |
| 103 | + |
| 104 | +### Error Handling |
| 105 | + |
| 106 | +`error_handler.py` provides centralized error management: |
| 107 | +- HTTP error mapping (400, 401, 429, etc.) |
| 108 | +- Memory monitoring and warnings |
| 109 | +- Error reporting with context |
| 110 | +- Persistent error log (optional) |
| 111 | + |
| 112 | +## Development Commands |
| 113 | + |
| 114 | +### Deploying to ESP32 |
| 115 | + |
| 116 | +**Prerequisites:** |
| 117 | +- Install `ampy` or `mpremote` for file transfer |
| 118 | +- Connect ESP32 via USB |
| 119 | + |
| 120 | +**Using ampy:** |
| 121 | +```bash |
| 122 | +# Upload single file |
| 123 | +ampy --port /dev/ttyUSB0 put main.py |
| 124 | + |
| 125 | +# Upload all Python files |
| 126 | +for f in *.py; do ampy --port /dev/ttyUSB0 put "$f"; done |
| 127 | + |
| 128 | +# Run without saving |
| 129 | +ampy --port /dev/ttyUSB0 run main.py |
| 130 | +``` |
| 131 | + |
| 132 | +**Using mpremote:** |
| 133 | +```bash |
| 134 | +# Copy all files |
| 135 | +mpremote connect /dev/ttyUSB0 cp *.py : |
| 136 | + |
| 137 | +# Run |
| 138 | +mpremote connect /dev/ttyUSB0 run main.py |
| 139 | +``` |
| 140 | + |
| 141 | +### Testing & Debugging |
| 142 | + |
| 143 | +**REPL Access:** |
| 144 | +```bash |
| 145 | +# Screen (macOS/Linux) |
| 146 | +screen /dev/ttyUSB0 115200 |
| 147 | + |
| 148 | +# Minicom |
| 149 | +minicom -D /dev/ttyUSB0 -b 115200 |
| 150 | +``` |
| 151 | + |
| 152 | +**Standalone Module Tests:** |
| 153 | +Many modules have `if __name__ == "__main__"` blocks for testing: |
| 154 | +```python |
| 155 | +# Test config system |
| 156 | +python config.py # Won't work - must run on ESP32 |
| 157 | + |
| 158 | +# On ESP32 REPL: |
| 159 | +import config |
| 160 | +config.load_stats() |
| 161 | +``` |
| 162 | + |
| 163 | +**Memory Monitoring:** |
| 164 | +```python |
| 165 | +import gc |
| 166 | +gc.collect() |
| 167 | +gc.mem_free() # Check available RAM |
| 168 | +``` |
| 169 | + |
| 170 | +### OTA Update Workflow |
| 171 | + |
| 172 | +1. Update `version.json` with new version number and file list |
| 173 | +2. Push changes to `rodillo69/psic-o-tronic` main branch |
| 174 | +3. Device checks GitHub for `version.json` |
| 175 | +4. Downloads changed files from raw.githubusercontent.com |
| 176 | +5. Prompts user to reboot |
| 177 | + |
| 178 | +## Code Style & Conventions |
| 179 | + |
| 180 | +### MicroPython Constraints |
| 181 | + |
| 182 | +**DO:** |
| 183 | +- Use `ujson` instead of `json` |
| 184 | +- Use `urequests` instead of `requests` |
| 185 | +- Use `os.stat()` to check file existence (no `os.path.exists()`) |
| 186 | +- Call `gc.collect()` before large operations |
| 187 | +- Use simple loops instead of comprehensions when memory-critical |
| 188 | + |
| 189 | +**DON'T:** |
| 190 | +- Use f-strings extensively (prefer format() or %) |
| 191 | +- Import large modules at global scope |
| 192 | +- Create large lists/dicts in memory |
| 193 | +- Use standard library modules unavailable in MicroPython |
| 194 | + |
| 195 | +### Text Handling |
| 196 | + |
| 197 | +Custom character mapping in `lcd_chars.py`: |
| 198 | +- Custom LCD chars 0-7 defined (heart, empty heart, etc.) |
| 199 | +- `convert_text()`: Maps Spanish characters to LCD-safe equivalents |
| 200 | +- Special characters: `chr(0)` = filled heart, `chr(1)` = empty heart |
| 201 | + |
| 202 | +### Input Debouncing |
| 203 | + |
| 204 | +Button reads use time-based debouncing (`DEBOUNCE_MS = 280`): |
| 205 | +```python |
| 206 | +def _get_input(self): |
| 207 | + now = time.ticks_ms() |
| 208 | + if time.ticks_diff(now, self._last_btn_time) < DEBOUNCE_MS: |
| 209 | + return None |
| 210 | + # ... check buttons |
| 211 | +``` |
| 212 | + |
| 213 | +### Audio System |
| 214 | + |
| 215 | +`audio.py` provides sound effects via PWM: |
| 216 | +- `init_audio(pin)`: Initialize speaker |
| 217 | +- `play(sound_name)`: Play predefined sound ('boot', 'click', 'beep', 'mensaje', 'correcto', 'incorrecto', 'victoria', 'game_over') |
| 218 | +- Non-blocking playback |
| 219 | + |
| 220 | +## Important Patterns |
| 221 | + |
| 222 | +### Adding New Game States |
| 223 | + |
| 224 | +1. Add state constant to `State` class in `main.py` |
| 225 | +2. Create handler method: `def _update_<state_name>(self, key):` |
| 226 | +3. Register in `state_handlers` dict in `run()` method |
| 227 | +4. Implement LCD rendering with buffer system |
| 228 | +5. Handle input (UP/DOWN/SELECT) in handler |
| 229 | +6. Set `self.state` to transition |
| 230 | + |
| 231 | +### Career Mode Extensions |
| 232 | + |
| 233 | +Career mode is modular - systems are defined in `career_systems.py`: |
| 234 | +- Achievements: `LOGROS` dict |
| 235 | +- Upgrades: `MEJORAS` dict |
| 236 | +- Missions: `MISIONES_DIARIAS`, `MISIONES_SEMANALES` |
| 237 | +- Events: `EVENTOS` dict |
| 238 | +- Recipes: `RECETAS` dict |
| 239 | + |
| 240 | +Add new content by extending these dicts and implementing corresponding logic. |
| 241 | + |
| 242 | +### AI Prompt Modification |
| 243 | + |
| 244 | +Modify `PROMPT_BASE` in `gemini_api.py` to change case generation behavior: |
| 245 | +- Style defined in "ESTILO DE HUMOR" section |
| 246 | +- Categories in "CATEGORIAS TEMATICAS" |
| 247 | +- Difficulty rules in "REGLAS DE DIFICULTAD" |
| 248 | + |
| 249 | +**Critical:** Response must be valid JSON matching `JSON_TEMPLATE`. The AI generates: theme, sender name, message, 4 options, correct index (0-3), win/lose feedback. |
| 250 | + |
| 251 | +## Hardware Pin Configuration |
| 252 | + |
| 253 | +Defined in `main.py` lines 37-46: |
| 254 | +```python |
| 255 | +PIN_BTN_UP = 4 |
| 256 | +PIN_BTN_SELECT = 5 |
| 257 | +PIN_BTN_DOWN = 6 |
| 258 | +PIN_LED_UP = 7 |
| 259 | +PIN_LED_SELECT = 15 |
| 260 | +PIN_LED_DOWN = 16 |
| 261 | +PIN_LED_NOTIFY = 17 |
| 262 | +PIN_SPEAKER = 9 |
| 263 | +PIN_I2C_SDA = 1 |
| 264 | +PIN_I2C_SCL = 2 |
| 265 | +``` |
| 266 | + |
| 267 | +### LCD Communication |
| 268 | + |
| 269 | +I2C LCD library: `i2c_lcd.py` (standard MicroPython LCD driver) |
| 270 | +- Default address: Auto-detected via `i2c.scan()` |
| 271 | +- 400kHz I2C frequency |
| 272 | +- 4x20 character display |
| 273 | + |
| 274 | +## Common Issues |
| 275 | + |
| 276 | +**Memory Errors:** |
| 277 | +- Call `gc.collect()` before API requests |
| 278 | +- Limit string operations in loops |
| 279 | +- Close `urequests` responses: `res.close()` |
| 280 | + |
| 281 | +**WiFi Issues:** |
| 282 | +- Portal may timeout if no interaction |
| 283 | +- Check saved credentials with `config.get_wifi_config()` |
| 284 | +- Clear config: `config.clear_wifi_config()` |
| 285 | + |
| 286 | +**API Errors:** |
| 287 | +- Default API key may hit rate limits |
| 288 | +- Users should configure their own Gemini key via portal |
| 289 | +- Check `gemini_api.py` error handling for HTTP status codes |
| 290 | + |
| 291 | +**OTA Failures:** |
| 292 | +- Requires stable WiFi during download |
| 293 | +- Large files may cause memory issues |
| 294 | +- Version format must be semver: "X.Y.Z" |
| 295 | + |
| 296 | +## Repository Structure |
| 297 | + |
| 298 | +``` |
| 299 | +/ |
| 300 | +βββ main.py # Main game engine & state machine |
| 301 | +βββ config.py # Configuration & stats persistence |
| 302 | +βββ game_modes.py # Classic & Survival mode logic |
| 303 | +βββ career_mode.py # Career mode main loop (3600+ lines) |
| 304 | +βββ career_data.py # Career persistent data management |
| 305 | +βββ career_patients.py # Patient generation & AI prompts |
| 306 | +βββ career_scheduler.py # Time simulation & scheduling |
| 307 | +βββ career_systems.py # Achievements/upgrades/missions/etc |
| 308 | +βββ gemini_api.py # Gemini AI integration |
| 309 | +βββ wifi_portal.py # Captive portal for setup |
| 310 | +βββ ota_update.py # Over-the-air update system |
| 311 | +βββ error_handler.py # Centralized error management |
| 312 | +βββ audio.py # Sound effects via PWM |
| 313 | +βββ ui_renderer.py # UI helper functions |
| 314 | +βββ lcd_chars.py # LCD character mapping |
| 315 | +βββ lcd_api.py # LCD low-level API |
| 316 | +βββ i2c_lcd.py # I2C LCD driver |
| 317 | +βββ ntp_time.py # NTP time synchronization |
| 318 | +βββ version.json # Version info for OTA |
| 319 | +``` |
| 320 | + |
| 321 | +## Testing Notes |
| 322 | + |
| 323 | +**Standalone Testing:** |
| 324 | +Most modules cannot run on desktop Python due to MicroPython-specific imports. Test on actual ESP32 hardware or use MicroPython Unix port. |
| 325 | + |
| 326 | +**WiFi Testing:** |
| 327 | +Portal can be tested by monitoring serial output while connecting phone to "PSIC-O-TRONIC" AP. |
| 328 | + |
| 329 | +**API Testing:** |
| 330 | +Run `gemini_api.py` standalone on ESP32 REPL after WiFi configured. |
0 commit comments