fix: JSON repair, WS thread pool, switch to DeepSeek V4 Flash, funnel Lv0

LLM fixes:
- _try_repair_json() — ripara JSON malformato (apici, virgole trailing)
- _strip_json_from_text() — rimuove residui JSON dalla narration
- parse_tool_call() — rimuove troncamento 200 caratteri dal fallback

DM fixes:
- Fallback euristico quando params vuoti (dm.py)
- Estrazione narrazione da dm_override/narrate params.params.text

Prompt (prompts.py):
- Vietati termini tecnici/scientifici/anacronistici (HPLC, laser, ...)
- Aggiunto vocabolario fantasy obbligatorio

Server (server.py):
- dm_cycle in ThreadPoolExecutor per non bloccare WS ping
- WebSocketDisconnect gestiti
- Rimosso import inesistente db_schema

Frontend (index.html):
- Nascosti tool name dalla chat
- Personaggio -> PG, connessioni aree con nomi

Infrastruttura:
- Switch da openrouter/free a DeepSeek V4 Flash via OpenCode Go
- pyproject.toml, Dockerfile, docker-compose.yml, start.sh
- .env auto-load (start.sh)
- Funnel Lv0 (funnel.py) — creazione PG livello 0

Docs: architecture, database, setup, game mechanics, codebase ref
This commit is contained in:
truetype committed 2026-06-28 08:18:24 +02:00
1 parent 6b048dc0cc
commit eacc946868
23 files changed
+1786 -61

No files matched your search

+216
View File
@@ -0,0 +1,216 @@
# 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