# Codebase Reference ## Directory Layout ``` rpg/ ├── src/ │ ├── engine/ │ │ ├── __init__.py # Public API del modulo engine │ │ ├── config.py # ProviderConfig, load_from_env() │ │ ├── db.py # SQLite schema, CRUD, adventure data access │ │ ├── dice.py # DCC dice system (roll, dice_chain) │ │ ├── dm.py # DM cycle orchestrator + CLI interactive │ │ ├── game_context.py # GameContext dataclass (campagna + stato) │ │ ├── heuristic_fallback.py# Keyword parser per fallback LLM │ │ ├── llm.py # LLM abstraction, ask(), parse_tool_call() │ │ ├── prompts.py # System prompt, context builder, tool docs │ │ ├── providers.py # Ollama/OpenRouter/Local providers │ │ └── tools.py # @tool registry, 10 Approved Tools │ │ │ └── web/ │ ├── __init__.py │ ├── server.py # FastAPI app, REST, WebSocket, main() │ └── static/ │ └── index.html # SPA frontend (888 righe CSS+JS+HTML) │ ├── data/ │ ├── dcc_rpg.db # SQLite database (auto-creato) │ ├── adventure/ │ │ └── avventura.json # Avventura machine-readable (9 aree) │ └── crit_fumble_tables.json # Tabelle critici e fumble (207 righe) │ ├── docs/ │ └── plans/ │ └── 2026-06-25-dcc-rpg-design.md # Design doc originale (314 righe) │ ├── docs_dev/ # Documentazione sviluppo (questo) ├── knowledge/ # 15 file KB (regole DCC, ~3708 righe) ├── input/ # PDF sorgente, reference booklet ├── ocr_full/ # OCR output (58K parole) ├── graphify-out/ # Grafo concettuale └── tests/ # Test (pytest) ├── test_dice.py ├── test_tools.py ├── test_integration.py └── test_websocket.py ``` ## Modulo: `src/engine/config.py` ### `ProviderKind` = `"ollama" | "openrouter" | "local"` ### `ProviderConfig` (dataclass) | Campo | Default | Env Var | |---|---|---| | `kind` | `"openrouter"` | `DCC_LLM_PROVIDER` | | `ollama_base_url` | `http://192.168.0.50:11434` | `DCC_OLLAMA_URL` | | `ollama_model` | `gemma2:9b` | `DCC_OLLAMA_MODEL` | | `openrouter_api_key` | `""` | `DCC_OPENROUTER_KEY` | | `openrouter_model` | `"openrouter/free"` | `DCC_OPENROUTER_MODEL` | | `openrouter_free_fallback` | `True` | `DCC_OR_FREE_FALLBACK` | | `local_base_url` | `http://192.168.0.240/v1` | `DCC_LOCAL_URL` | | `local_api_key` | `""` | `DCC_LOCAL_KEY` | | `local_model` | `"big-pickle"` | `DCC_LOCAL_MODEL` | | `timeout` | `60` | `DCC_LLM_TIMEOUT` | ## Modulo: `src/engine/dice.py` ### `DICE_CHAIN` ``` d3 → d4 → d5 → d6 → d7 → d8 → d10 → d12 → d14 → d16 → d20 → d24 → d30 ``` ### `SPECIAL_DICE` `d%` (100), `d100` (100), `d0` (0), `d1` (1), `d2` (2) ### API | Funzione | Parametri | Returns | |---|---|---| | `roll(die, count=1, modifier=0)` | die: str | `RollResult {die, count, modifier, results[], total, crit, fumble, raw}` | | `dice_chain(die, steps)` | steps: int (+=su, -=giù) | `DiceChainResult {input_die, steps, output_die, chain}` o `ValidationError` | | `roll_str(raw)` | "2d20+3" | `RollResult` | | `validate_die(die)` | die: str | `str | None` (normalizzato o None) | ### Regole crit/fumble - Solo su `d20`: 20 = crit, 1 = fumble - Critico: moltiplica danno, tiro su tabella critici per classe - Fumble: tiro su tabella fumble (dado dipende da armatura) ## Modulo: `src/engine/tools.py` ### `@tool` decorator Ogni funzione decorata viene automaticamente registrata in `TOOL_REGISTRY`. ### `run(tool_name, **kwargs)` — dispatcher centralizzato Cerca in `TOOL_REGISTRY`, chiama la funzione, cattura eccezioni. ### 10 Approved Tools | Tool | Funzione | Parametri Chiave | |---|---|---| | `roll_dice` | Tira dado/i | `die`, `count`, `modifier` | | `dice_chain` | Risolve catena dadi | `die`, `steps` | | `combat` | Ciclo combattimento completo | `character_id`, `target_ac`, `attack_bonus`, `damage_die`, `mighty_deed_die`, `monster_id`, `armor_type` | | `ability_check` | Prova abilità | `character_id`, `dc`, `modifier` | | `saving_throw` | Tiro salvezza | `character_id`, `save_type` (Tempra/Riflessi/Volontà), `dc` | | `movement` | Movimento | `distance_m`, `terrain`, `stealth` | | `spellcasting` | Incantesimo | `character_id`, `spell_name`, `spell_check`, `dc` | | `update_inventory` | Gestione inventario | `character_id`, `item`, `action` (add/remove/equip/unequip/list) | | `award_xp` | Assegna PE | `character_id`, `xp` | | `dm_override` | DM override | `command`, `params` | ### Tool `combat` — Dettaglio flusso 1. Carica PG e mostro (con PF persistenti da combat_state) 2. Tiro iniziativa (d20) 3. Tiro attacco (d20 + bonus) 4. Mighty Deed (se guerriero, dado d3/d4/d5 aggiuntivo) 5. Crit/Fumble su 20/1 naturale 6. Lookup tabella critici (per classe/livello) o fumble (per armatura) 7. Calcolo danno + danno extra da critico 8. Persistenza PF mostro in combat_state ### Tool `spellcasting` — Tier | Tiro | Risultato | |---|---| | 1 | Fallimento critico (corruzione!) | | < DC | Fallimento | | ≤ 11 | Successo minore (effetto base) | | ≤ 17 | Successo moderato (effetto migliorato) | | ≤ 19 | Successo maggiore (effetto potente) | | 20+ | Successo massimo (effetto devastante) | ## Modulo: `src/engine/providers.py` ### Provider Classes | Classe | Endpoint | Auth | |---|---|---| | `OllamaProvider` | `{base_url}/api/generate` | Nessuna | | `OpenRouterProvider` | `openrouter.ai/api/v1/chat/completions` | Bearer API key | | `LocalProvider` | `{base_url}/chat/completions` | Bearer key (opzionale) | ### `create_provider(cfg)` → Factory Legge `cfg.kind`, istanzia provider corretto con parametri appropriati. ## Modulo: `src/engine/dm.py` ### `dm_cycle(campaign_id, player_action)` → ciclo principale 1. `GameContext.load(campaign_id)` — assembla contesto 2. `_llm_decision(ctx, action)` — LLM + fallback 3. `tools.run(tool_name, **params)` — esecuzione 4. `db.advance_turn()` + `db.log_turn()` — persistenza 5. Return response dict ### `interactive(campaign_id)` — CLI interattiva Loop input/output con prompt `⚔️ », comandi: esci, stato ## Modulo: `src/engine/prompts.py` ### `SYSTEM_PROMPT` — Prompt di sistema (~60 righe) - Stile narrazione - Meccaniche base DCC - Tool disponibili con parametri (json) - Esempi di chiamata - Formato risposta obbligatorio ### `build_prompt(ctx, player_action, current_area_id)` → prompt completo - `format_context(ctx)`: contesto campagna - `format_area_context(area_id)`: descrizione area attuale - Azione giocatore ## Modulo: `src/engine/db.py` Vedi `database-schema.md` per schema completo. ### Lifecycle 1. `init_db()` → init_schema + migrate + populate_from_json 2. Connessioni thread-local con `threading.local()` 3. WAL mode + busy_timeout per concorrenza ## Modulo: `src/web/server.py` ### Entry Point ```bash python -m src.web.server # oppure uvicorn src.web.server:app --host 0.0.0.0 --port 8300 ``` ### REST Endpoints | Metodo | Path | Descrizione | |---|---|---| | GET | `/api/campaigns` | Lista campagne | | GET | `/api/campaign/{id}` | Stato campagna completo | | POST | `/api/campaign?name=...` | Crea campagna | | POST | `/api/campaign/{id}/character?name=...&char_class=...` | Aggiunge PG default | | GET | `/health` | Health check | | GET | `/` | Frontend SPA | ### WebSocket `/ws/{campaign_id}` | Messaggio | Direzione | Descrizione | |---|---|---| | `{"type": "action", "payload": "..."}` | Client→Server | Azione giocatore | | `{"type": "ping"}` | Client→Server | Keepalive | | `{"type": "state", "campaign": {...}}` | Server→Client | Stato completo | | `{"type": "response", "turn": N, ...}` | Server→Client | Risultato turno | | `{"type": "pong"}` | Server→Client | Keepalive response | | `{"type": "error", "message": "..."}` | Server→Client | Errore | ## File: `src/web/static/index.html` Single-page application (~888 righe) con: - **Welcome screen**: lista campagne, creazione nuova - **Sidebar** (320px): scheda PG (statistiche, HP bar), missioni, mappa aree - **Chat area**: messaggi player/DM con stili differenziati - **Input bar**: campo testo + bottone invio - **WebSocket** con auto-reconnect ogni 3s - Tema scuro "sword & sorcery" con font Cinzel + Cormorant Garamond