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

+82
View File
@@ -0,0 +1,82 @@
# DCC RPG — AI Dungeon Master: Documentazione
## Panoramica
Questo progetto implementa un **Dungeon Master virtuale** per il gioco di ruolo
**Dungeon Crawl Classics** (regolamento introduttivo italiano). Il sistema usa un
LLM (Ollama/OpenRouter) per la narrazione e l'interpretazione delle azioni,
mentre un motore Python deterministico gestisce le meccaniche di gioco
(tiri, combattimento, inventario).
**Pattern architetturale**: Neon Scratch Lounge (separazione Reasoning/Execution)
**Avventura**: La Tomba del Mago Guerriero (9 aree)
**Target**: On-premise, CPU-only, singolo giocatore, browser
## Documenti
| File | Contenuto |
|---|---|
| `architecture.md` | Architettura, pattern NSL, data flow, flusso turno |
| `codebase-reference.md` | Tutti i moduli, API pubbliche, entry point |
| `database-schema.md` | Schema SQLite, tabelle, migrazioni, CRUD |
| `setup-guide.md` | Come avviare, configurare, connettere LLM |
| `adventure-data.md` | Struttura dati avventura, aree, mostri, enigmi |
| `game-mechanics.md` | Regole DCC implementate (dadi, classi, combattimento) |
| `development-guide.md` | Come estendere: nuovi tool, provider, aree |
## Stack Tecnologico
| Componente | Tecnologia |
|---|---|
| LLM | Ollama (gemma2:9b) / OpenRouter / OpenAI-compatible |
| Backend | Python 3.10+ + FastAPI + uvicorn |
| Frontend | HTML + CSS + JS vanilla |
| Database | SQLite (WAL mode) |
| HTTP | httpx (LLM calls) |
| WebSocket | FastAPI WebSocket (real-time game) |
## Scheletro del Turno
```
Giocatore "Cerco nella stanza"
→ LLM reasoning (prompt + contesto)
→ LLM risponde: {"tool": "ability_check", "params": {...}, "narration": "..."}
→ Esecuzione tool deterministico
→ Persistenza stato (SQLite)
→ Risposta al giocatore (narrazione + meccaniche)
```
## Struttura Directory
```
rpg/
├── src/
│ ├── engine/ # Core game engine (DM, LLM, tools, dice, DB)
│ └── web/ # FastAPI server + frontend statico
├── data/ # SQLite DB, avventura JSON, tabelle critici
├── docs/ # Design doc originale
├── docs_dev/ # Questa documentazione
├── knowledge/ # 15 file knowledge base DCC
├── input/ # PDF sorgente, reference booklet
├── ocr_full/ # OCR output files
├── graphify-out/ # Grafo concettuale (62 nodi)
└── tests/ # Test (da popolare)
```
## Stato Progetto (CHECKLIST.md)
**Fasi completate:**
- ✅ Pattern architetturale NSL analizzato e documentato
- ✅ OCR pipeline (22 screenshot + 41 pagine PDF = ~58K parole)
- ✅ Pulizia OCR (60 correzioni dadi, 11 testuali)
- ✅ Knowledge Base strutturata (15 file, ~3708 righe)
- ✅ Grafo concettuale (Graphify, 62 nodi, 27 archi)
- ✅ Avventura completata (9 aree + background)
**Fase 0-4 implementata (codice):**
- ✅ Backend core: DM cycle, tool registry, dice engine
- ✅ LLM integration: multi-provider, prompt template, parser
- ✅ Dati adventure machine-readable: JSON aree/mostri/quest
- ✅ DB schema: 8 tabelle, CRUD, migrazioni
- ✅ Frontend: WebSocket, chat, stato personaggio, mappa
- ✅ Heuristic fallback: keyword parser per quando LLM fallisce
+159
View File
@@ -0,0 +1,159 @@
# Adventure Data
**Avventura**: La Tomba del Mago Guerriero
**File**: `data/adventure/avventura.json` (~790 righe)
## Struttura JSON
```json
{
"meta": {
"name": "La Tomba del Mago Guerriero",
"description": "Avventura per personaggi di livello 0/1",
"setting": "dungeon sotterraneo di un antico mago guerriero",
"theme": "Weird Fantasy / sci-fantasy",
"level_range": "0-1",
"warrior_mage_name": "Vorkhul Ksh'tharan",
"warrior_mage_epithet": "Il Vincolato alle Stelle",
"structure": "9 aree numerate (1.1-1.9)",
"mortality_style": "DCC classico: alta mortalita, perdita media 7-8 PG su 15"
},
"areas": [...],
"monsters": [...],
"quests": [...],
"special_items": [...]
}
```
## Aree (9 totali)
| ID | Nome | Collegamenti | PG? | Mostri | Enigmi |
|---|---|---|---|---|---|
| 1.1 | Portale | 1.2 | Sì | - | Allineamento stellare |
| 1.2 | Sala del Guardiano | 1.1, 1.3 | Sì | Statua lanciafiamme | Nome mago guerriero |
| 1.3 | Sala Monumentale | 1.2, 1.4 | Sì | Guardiano spettrale | Colonna centrale (frammento nome) |
| 1.4 | Fucina del Demone-Serpente | 1.3, 1.5 | Sì | Demone-serpente | - |
| 1.5 | Camera della Mente Senza Tempo | 1.4, 1.6 | Sì | - | Prova mentale |
| 1.6 | La Forgia delle Anime | 1.5, 1.7 | Sì | Anima forgiata | - |
| 1.7 | Ossario | 1.6, 1.8 | Sì | Non-morti | - |
| 1.8 | Anticamera del Potere | 1.7, 1.9 | Sì | Golem guardiano | - |
| 1.9 | Sala del Tesoro | 1.8 | Sì | - | Tomo del nome completo |
## Ogni area contiene:
```json
{
"id": "1.1",
"name": "Portale",
"order": 1,
"description": "Corridoio d'ingresso visibile soltanto attraverso il portale stellare...",
"atmosphere": "Aria stantia, polvere millenaria, silenzio pressante...",
"connections": ["1.2"],
"features": [
"Porta rinforzata in ferro con gioielli incastonati",
"Lastricato di pietra",
"Iscrizioni stellari"
],
"puzzles": [
{
"name": "Apertura della Porta",
"description": "...",
"solution": "Attendere 2 ore l'allineamento",
"skill_checks": [
{"skill": "Intelligenza", "dc": 14, "description": "Accorgersi dell'allineamento"}
]
}
],
"traps": [
{
"name": "Luce Bruciante",
"trigger": "Forzare la porta",
"effect": "Luce accecante, 1d8 danni",
"detect_dc": 20,
"damage": "1d8",
"save": {"type": "Riflessi", "dc": 10, "effect_on_save": "danno dimezzato"}
}
],
"monsters": [{"ref_id": "statue_guardian", "count": 1, "tactics": "attacca chi entra"}],
"items": [],
"treasure": []
}
```
## Mostri (stat block)
```json
{
"monsters": [
{
"id": "statue_guardian",
"name": "Statua Guardiana",
"level": 2,
"stats": {
"ac": 16,
"hp": 25,
"attack": "d20+4",
"damage": "1d8+2",
"saves": {"Tempra": 5, "Riflessi": 2, "Volontà": 0},
"move": "nessuno (fissa)"
},
"special": "Lanciafiamme ogni 1d4 round (d8+2 danni, Riflessi CD 12 dimezza)",
"xp": 75,
"loot": []
}
]
}
```
## Quest
```json
{
"quests": [
{
"name": "Il Nome Perduto",
"description": "Scopri il vero nome del mago guerriero per placare le difese del dungeon",
"objectives": [
"Trova il frammento nella Colonna Centrale (area 1.3)",
"Trova il nome completo nel Tomo (area 1.9)",
"Pronuncia il nome nella Sala del Guardiano (area 1.2)"
]
}
]
}
```
## Special Items
```json
{
"special_items": [
{
"id": "tomo_antico",
"name": "Tomo dalla Rilegatura di Pelle",
"location": "1.9",
"description": "Contiene la seconda meta del nome del mago guerriero",
"type": "quest"
}
]
}
```
## Tabelle Critici / Fumble
**File**: `data/crit_fumble_tables.json` (207 righe)
### Tabella Fumble (`fumble`)
- Dado determinato da armatura: nessuna=d4, leggera=d8, media=d12, pesante=d16
- Modificatore: -Fortuna (sfortuna = peggior fumble)
- 17 entries (da ≤0 a 16+)
### Tabelle Critici (`crit_I` → `crit_V`)
- **crit_I**: Personaggi lv0 e Maghi (d4)
- **crit_II**: Ladri, Elfi (d6)
- **crit_III**: Chierici, Halfling, Guerrieri lv1-2, Nani lv1-3 (d8)
- **crit_IV**: Guerrieri lv3-4, Nani lv4-5 (d10)
- **crit_V**: Guerrieri lv5+, Nani lv6+ (d12)
- Modificatore: +Fortuna
Ogni tabella ha ~19 entries su range diverso.
+133
View File
@@ -0,0 +1,133 @@
# Architettura: Neon Scratch Lounge (On-Premise)
## Pattern: Separazione Reasoning/Execution
L'architettura separa nettamente ciò che fa il **LLM** (ragionamento, narrazione)
da ciò che fa il **motore Python** (meccaniche deterministiche, persistenza).
```
┌─────────────────────────────────────────────────┐
│ CLIENT (Web UI) │
│ Chat testuale + pannello stato laterale │
└──────────────────┬──────────────────────────────┘
│ WebSocket / HTTP REST
┌──────────────────▼──────────────────────────────┐
│ LLM ORCHESTRATOR (src/engine/dm.py) │
│ - Mantiene contesto sessione │
│ - Invia prompt: regole + stato + azione │
│ - Riceve: narrazione + tool calls │
│ - Fallback: heuristic parser su errore LLM │
└──────────────────┬──────────────────────────────┘
│
┌──────────────────▼──────────────────────────────┐
│ TOOL REGISTRY (src/engine/tools.py) │
│ @tool decorator → registro centralizzato │
│ 10 Approved Tools + dm-override │
└──────────────────┬──────────────────────────────┘
│
┌──────────────────▼──────────────────────────────┐
│ STATO PERSISTENTE (SQLite) │
│ src/engine/db.py — 8 tabelle, CRUD completo │
│ WAL mode, thread-local connections │
└─────────────────────────────────────────────────┘
```
## Flusso del Turno (dm_cycle)
```
dm_cycle(campaign_id, player_action)
│
├─ 1. GameContext.load(campaign_id)
│ Carica da DB: campagna, PG, log, quest, aree
│ → dataclass GameContext
│
├─ 2. _llm_decision(ctx, action)
│ build_prompt(ctx, action) → prompt enorme
│ llm_ask(prompt, system=SYSTEM_PROMPT)
│ parse_tool_call(raw_llm_response) → {tool, params, narration}
│ SE LLM fallisce → fallback_parse(action, chars)
│
├─ 3. tools.run(tool_name, **params)
│ TOOL_REGISTRY[tool_name](**params)
│ → ToolResult dict
│
├─ 4. Persistenza turno
│ db.advance_turn(campaign_id)
│ db.log_turn(campaign_id, turn, action, result)
│
└─ 5. Response dict
{turn, action, tool, params, narration, mechanics, full_output}
```
## Multi-Provider LLM
```
ProviderConfig (env vars)
│
├─ kind = "ollama" → OllamaProvider
│ POST {base_url}/api/generate
│ model: gemma2:9b
│
├─ kind = "openrouter" → OpenRouterProvider
│ POST openrouter.ai/api/v1/chat/completions
│ Fallback automatico a modelli free
│
└─ kind = "local" → LocalProvider
POST {base_url}/chat/completions (OpenAI-compatible)
Per inference server locali (vLLM, TGI, llama.cpp)
```
## Prompt Engineering
### System Prompt (SYSTEM_PROMPT)
- Stile narrazione: sword & sorcery, italiano, 2-3 frasi
- Regole base: CD, CA, tiri salvezza, mighty deed
- Tool disponibili: JSON con parametri + esempi
- Formato output obbligatorio: JSON `{tool, params, narration}`
### Context Builder (build_prompt)
- `format_context(ctx)`: campagna, PG, cronologia, quest, aree
- `format_area_context(area_id)`: descrizione area, atmosfera, features, mostri presenti
- Knowledge base completa via DB (non file statici)
## Heuristic Fallback
Quando il LLM non produce un tool valido:
1. `parse_tool_call()` fallisce → regex JSON + keyword extraction
2. Se ancora fallisce → `heuristic_fallback.parse_action()`
3. Keyword matching: "attacco" → combat, "muovo" → movement, "d20" → roll_dice
4. Fallback finale → dm_override (narrazione pura)
## Frontend
### WebSocket Protocol
- `state`: stato completo campagna (all'entrata + dopo ogni azione)
- `response`: risultato turno (narrazione + tool + meccaniche)
- `action`: azione giocatore (input → WebSocket → dm_cycle)
- `ping/pong`: keepalive
- `error`: messaggio errore
### REST API
- `GET /api/campaigns` — lista campagne
- `GET /api/campaign/{id}` — stato completo
- `POST /api/campaign` — crea nuova campagna
- `POST /api/campaign/{id}/character` — aggiunge PG
- `GET /` — frontend
- `GET /health` — health check
## Key Design Decisions
### Perché SQLite invece di DynamoDB
- Zero dipendenze, on-premise
- WAL mode per concorrenza thread-safe
- Thread-local connections via `threading.local()`
### Perché @tool decorator invece di enum
- Aggiungere nuovo tool = scrivere funzione + decorator
- Nessun'altra modifica richiesta
- Auto-registrazione in TOOL_REGISTRY
### Perché heuristic fallback
- Ollama su CPU è lento e talvolta non rispetta il formato JSON
- Il fallback garantisce che l'azione vada sempre a buon fine
- Keyword parser copre ~80% dei casi d'uso base
+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
+202
View File
@@ -0,0 +1,202 @@
# Database Schema
**File**: `data/dcc_rpg.db` (auto-creato)
**Engine**: SQLite 3 con WAL mode
**Connessione**: Thread-local via `threading.local()`, check_same_thread=False
## Tabelle
### `campaigns` — Campagne di gioco
```sql
CREATE TABLE campaigns (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
adventure TEXT,
turn INTEGER NOT NULL DEFAULT 0,
current_area_id TEXT DEFAULT '',
current_area_name TEXT DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
```
### `characters` — Personaggi giocanti
```sql
CREATE TABLE characters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
campaign_id INTEGER NOT NULL REFERENCES campaigns(id),
name TEXT NOT NULL,
level INTEGER NOT NULL DEFAULT 0,
char_class TEXT,
alignment TEXT CHECK(alignment IN ('Legge','Caos','Neutralità')),
occupation TEXT,
str INTEGER NOT NULL DEFAULT 10,
agi INTEGER NOT NULL DEFAULT 10,
sta INTEGER NOT NULL DEFAULT 10,
per INTEGER NOT NULL DEFAULT 10,
int INTEGER NOT NULL DEFAULT 10,
lck INTEGER NOT NULL DEFAULT 10,
hp_current INTEGER NOT NULL DEFAULT 1,
hp_max INTEGER NOT NULL DEFAULT 1,
ac INTEGER NOT NULL DEFAULT 10,
armor_type TEXT DEFAULT 'none',
luck_current INTEGER NOT NULL DEFAULT 10,
xp INTEGER NOT NULL DEFAULT 0,
weapon TEXT,
equipment TEXT DEFAULT '[]',
status TEXT NOT NULL DEFAULT 'alive'
CHECK(status IN ('alive','unconscious','dead'))
);
CREATE INDEX idx_char_campaign ON characters(campaign_id);
```
### `adventure_state` — Stato esplorazione aree
```sql
CREATE TABLE adventure_state (
id INTEGER PRIMARY KEY AUTOINCREMENT,
campaign_id INTEGER NOT NULL REFERENCES campaigns(id),
area_id TEXT NOT NULL,
visited INTEGER NOT NULL DEFAULT 0,
state TEXT DEFAULT '{}'
);
CREATE UNIQUE INDEX idx_area_campaign ON adventure_state(campaign_id, area_id);
```
Il campo `state` contiene JSON con lo stato di enigmi/trappole risolte per l'area.
### `combat_state` — Stato combattimento (PF persistenti mostri)
```sql
CREATE TABLE combat_state (
id INTEGER PRIMARY KEY AUTOINCREMENT,
campaign_id INTEGER NOT NULL REFERENCES campaigns(id),
monster_id TEXT NOT NULL,
monster_name TEXT NOT NULL DEFAULT '',
current_hp INTEGER NOT NULL,
max_hp INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'active'
CHECK(status IN ('active', 'defeated', 'fled')),
UNIQUE(campaign_id, monster_id)
);
```
### `character_items` — Inventario PG
```sql
CREATE TABLE character_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
character_id INTEGER NOT NULL REFERENCES characters(id),
item_def_id TEXT,
name TEXT NOT NULL,
quantity INTEGER DEFAULT 1,
equipped INTEGER DEFAULT 0,
properties TEXT DEFAULT '{}'
);
```
### `quests` — Missioni attive/completate/fallite
```sql
CREATE TABLE quests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
campaign_id INTEGER NOT NULL REFERENCES campaigns(id),
name TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'active'
CHECK(status IN ('active','completed','failed'))
);
```
### `turn_log` — Log azioni e risultati
```sql
CREATE TABLE turn_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
campaign_id INTEGER NOT NULL REFERENCES campaigns(id),
turn INTEGER NOT NULL,
character_id INTEGER REFERENCES characters(id),
action TEXT,
result TEXT,
timestamp TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_turnlog_campaign ON turn_log(campaign_id, turn);
```
### Tabelle di sistema: critici e fumble
```sql
CREATE TABLE crit_fumble_tables (
id TEXT PRIMARY KEY, -- "fumble", "crit_I"..."crit_V"
name TEXT NOT NULL,
description TEXT,
table_type TEXT NOT NULL CHECK(table_type IN ('crit', 'fumble')),
die TEXT,
modifier TEXT
);
CREATE TABLE crit_fumble_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
table_id TEXT NOT NULL REFERENCES crit_fumble_tables(id),
roll_min INTEGER,
roll_max INTEGER,
result_text TEXT NOT NULL
);
CREATE INDEX idx_entries_table ON crit_fumble_entries(table_id);
CREATE TABLE fumble_armor_die (
armor_type TEXT PRIMARY KEY, -- "none", "light", "medium", "heavy"
die TEXT NOT NULL
);
```
## Migrazioni
Le migrazioni sono gestite tramite `_migrate_schema()` chiamata a ogni avvio.
Ogni migrazione è un ALTER TABLE avvolto in try/except (se la colonna esiste già, skippa).
Migrazioni attuali:
```python
migrations = [
"ALTER TABLE campaigns ADD COLUMN current_area_id TEXT DEFAULT ''",
"ALTER TABLE campaigns ADD COLUMN current_area_name TEXT DEFAULT ''",
]
```
## Popolazione Iniziale
`populate_from_json()` carica i dati da `data/crit_fumble_tables.json`:
- 6 tabelle (fumble + crit_I → crit_V)
- 112 entries totali
- 4 mappature armatura→dado fumble
`seed_adventure_state()` e `seed_quests_from_adventure()` caricano aree e quest da `avventura.json`.
## CRUD Pubbliche (`db.py`)
| Funzione | Descrizione |
|---|---|
| `init_db()` | Schema + migrazioni + popolamento |
| `lookup_crit_fumble(table_id, roll, luck_mod=0)` | Cerca risultato crit/fumble con modificatore |
| `get_fumble_die(armor_type)` | Dado fumble per tipo armatura |
| `create_campaign(name, adventure)` | Nuova campagna |
| `get_campaign(id)` | Campagna by ID |
| `create_character(campaign_id, name, class, occupation)` | Nuovo PG |
| `get_characters(campaign_id)` | Tutti i PG di una campagna |
| `get_character(id)` | PG by ID |
| `update_character(id, **kwargs)` | Update qualsiasi campo |
| `advance_turn(campaign_id)` | Incrementa turno |
| `log_turn(...)` | Logga azione |
| `get_log(campaign_id, limit)` | Ultimi N turni |
| `set_area_state(...)` | Marca area visitata/stato |
| `set_puzzle_state(...)` | Stato enigma (solved/unsolved) |
| `is_puzzle_solved(...)` | Check enigma |
| `get_area_state(campaign_id)` | Stato tutte le aree |
| `set_current_area(...)` | Imposta area corrente PG |
| `get_current_area(campaign_id)` | Area corrente PG |
| `set_combat_state(...)` | Inizializza mostro in combattimento |
| `update_combat_hp(...)` | Danno mostro, status defeated |
| `get_combat_state(campaign_id)` | Stato combattimento |
| `add_character_item(...)` | Aggiunge oggetto (stacking) |
| `remove_character_item(...)` | Rimuove oggetto |
| `get_character_items(...)` | Inventario PG |
| `equip_character_item(...)` | Equipaggia/Unequipaggia |
| `get_all_areas()` | Tutte le aree da JSON |
| `get_area_by_id(id)` | Area specifica |
| `get_monster_by_id(id)` | Stat block mostro |
| `get_monsters_in_area(area_id)` | Mostri in area con stats |
| `format_area_context(area_id)` | Testo area per prompt LLM |
| `format_all_areas_context()` | Tutte le aree per prompt iniziale |
+212
View File
@@ -0,0 +1,212 @@
# Development Guide
## Come Aggiungere un Nuovo Tool
1. Apri `src/engine/tools.py`
2. Scrivi una funzione con `@tool` decorator:
```python
@tool
def perception_check(character_id: int, dc: int, modifier: int = 0) -> ToolResult:
char = storage.get_character(character_id)
if not char:
return {"success": False, "tool": "perception-check", "message": "PG non trovato."}
atk = dice.roll("d20", modifier=modifier)
passed = atk["total"] >= dc
return {
"success": True,
"tool": "perception-check",
"character_id": character_id,
"roll": atk["total"],
"dc": dc,
"passed": passed,
"message": f"Percezione: d20+{modifier} → {atk['total']} vs CD {dc} → {'Successo!' if passed else 'Fallimento.'}",
}
```
3. Aggiungi il tool a `_KNOWN_TOOLS` in `src/engine/prompts.py`:
```python
"perception_check": {"character_id": "id pg", "dc": "CD percezione", "modifier": "bonus/penalità"},
```
4. Aggiungi un esempio a `TOOL_EXAMPLES` in `prompts.py`:
```python
- Tiro percezione CD 12 → {"tool": "perception_check", "params": {"character_id": 1, "dc": 12}, "narration": "Tendi l'orecchio..."}
```
5. **Fatto.** Il tool viene automaticamente registrato in `TOOL_REGISTRY`.
## Come Aggiungere un Provider LLM
1. Apri `src/engine/providers.py`
2. Crea una nuova classe che estende `LLMProvider`:
```python
class AnthropicProvider(LLMProvider):
def __init__(self, api_key: str, model: str, timeout: int = 60):
self.api_key = api_key
self.model = model
self.timeout = timeout
def ask(self, prompt: str, system: str = "") -> str:
# Implementa chiamata API Anthropic qui
...
```
3. Registra in `_registry`:
```python
_registry = {
"ollama": OllamaProvider,
"openrouter": OpenRouterProvider,
"local": LocalProvider,
"anthropic": AnthropicProvider, # <-- aggiungi
}
```
4. Aggiungi il case in `create_provider()`:
```python
if cfg.kind == "anthropic":
return cls(cfg.anthropic_api_key, cfg.anthropic_model, cfg.timeout)
```
5. Aggiungi configurazione in `config.py`:
```python
anthropic_api_key: str = ""
anthropic_model: str = "claude-sonnet-4-20250514"
```
## Come Aggiungere una Nuova Area all'Avventura
1. Apri `data/adventure/avventura.json`
2. Aggiungi area all'array `areas`:
```json
{
"id": "1.10",
"name": "Biblioteca Arcana",
"order": 10,
"description": "...",
"atmosphere": "...",
"connections": ["1.9"],
"features": ["Scaffali polverosi", "Globo luminoso fluttuante"],
"puzzles": [...],
"traps": [],
"monsters": [{"ref_id": "ancient_spirit", "count": 1, "tactics": "...", "ambush": false}],
"items": [
{"id": "scroll_of_truth", "name": "Pergamena della Verità", "description": "...", "type": "consumable", "magical": true}
],
"treasure": []
}
```
3. Aggiungi stat block del mostro in `monsters`:
```json
{
"id": "ancient_spirit",
"name": "Spirito Antico",
"level": 3,
"stats": {"ac": 14, "hp": 18, "attack": "d20+3", "damage": "1d6+1", "saves": {"Tempra": 2, "Riflessi": 4, "Volontà": 6}, "move": "9m (volando)"},
"special": "Intangibile, colpito solo da magia o argento",
"xp": 100,
"loot": []
}
```
4. Riavvia il server. La nuova area viene caricata automaticamente.
## Come Aggiungere una Tabella Crit/Fumble
1. Aggiungi entry a `data/crit_fumble_tables.json`
2. Riavvia il server (ricarica da JSON se DB vuoto)
3. Se il DB è già popolato, cancella `data/dcc_rpg.db` (verrà ricreato)
## Test
```bash
# Test dadi
python src/engine/dice.py
# Test tools (con setup DB)
python src/engine/tools.py
# Test LLM parsing
python src/engine/llm.py
# Test DB completo
python src/engine/db.py
# Avvia CLI interattiva
python -c "
from src.engine import dm, db
db.init_db()
camp_id = db.create_campaign('Test', 'Avventura')
char_id = db.create_character(camp_id, 'Eroe', 'guerriero', 'soldato')
db.update_character(char_id, level=1, str=15, agi=11, sta=14, per=9, int=8, lck=10, hp_current=12, hp_max=12, ac=15)
dm.interactive(camp_id)
"
```
### Test esistenti (`tests/`)
- `test_dice.py` — test modulo dadi (pytest)
- `test_tools.py` — test tool execution
- `test_integration.py` — test flusso dm_cycle
- `test_websocket.py` — test WebSocket lifecycle
Per eseguire:
```bash
python -m pytest tests/ -v
```
## Known LSP Issues (non bloccanti)
Il LSP segnala questi errori, ma non impediscono l'esecuzione:
| File | Errore | Causa |
|---|---|---|
| `db.py:762` | `Object of type "None" is not subscriptable` | `cursor.fetchone()` in `get_current_area` può restituire None |
| `test_dice.py:47` | `output_die not in ValidationError` | `DiceChainResult` e `ValidationError` condividono TypedDict — meglio `@dataclass` |
| `tests/test_websocket.py:8` | `Import pytest could not be resolved` | pytest non installato nell'ambiente LSP |
| `game_context.py:29` | `get_current_area not in db` | LSP non riconosce funzioni dinamicamente caricate |
## Estendere il Frontend
Il frontend è un singolo file HTML (`src/web/static/index.html`) con:
- CSS: tema scuro sword & sorcery con variabili CSS
- JS: ~300 righe di vanilla JS (nessun framework)
- WebSocket: connessione persistente, auto-reconnect
Per modificare:
- **Stile**: modifica le variabili CSS in `:root` e classi dei componenti
- **Nuova sezione sidebar**: aggiungi div in HTML + funzione render in JS
- **Nuovo tipo di messaggio**: aggiungi case a `handleMessage()`
## Guida alla Conoscenza Base (knowledge/)
I 15 file in `knowledge/` contengono le regole DCC in formato markdown italiano.
Non sono usati direttamente dal runtime (i dati sono in DB/JSON/code),
ma sono la fonte di verità per lo sviluppo.
Per rigenerare prompt alla luce delle regole:
1. Consulta i file `knowledge/01_meccanica_base.md` → `knowledge/15_dadi.md`
2. Adatta `prompts.py` e `tools.py` di conseguenza
## Checklist Prossimi Miglioramenti
**Priorità alta:**
- [ ] Aggiungere requirements.txt o pyproject.toml
- [ ] Aggiungere Dockerfile + docker-compose.yml
- [ ] Script start.sh per avvio con un comando
- [ ] Supporto per più PG contemporaneamente
- [ ] Persistenza PF multipli mostri per area
**Priorità media:**
- [ ] Test completi per ogni tool
- [ ] Tavola incantesimi dal Reference Booklet
- [ ] Abilità ladro complete (dado dedicato per livello)
- [ ] Scacciare non-morti (Chierico)
- [ ] Equipaggiamento e armi dal knowledge base
**Priorità bassa:**
- [ ] OCR Core Rulebook (498 pagine immagini, ~8h stimate)
- [ ] RAG + embedding per knowledge base DCC
- [ ] Multi-giocatore (più PG in contemporanea)
- [ ] Inventory drag & drop nel frontend
- [ ] Mappa interattiva delle aree
+98
View File
@@ -0,0 +1,98 @@
# Game Mechanics (DCC RPG)
## Sistema Base
### D20 + CD
- CD 12 = facile, CD 15 = media, CD 18 = difficile
- Tiro = d20 + modificatore ≥ CD → successo
- 20 naturale = critico (successo automatico)
- 1 naturale = fumble (fallimento automatico)
### Dadi del Gioco (DCC Dice Chain)
```
d3 → d4 → d5 → d6 → d7 → d8 → d10 → d12 → d14 → d16 → d20 → d24 → d30
```
Usata per progressioni e meccaniche speciali.
## Classi Implementate (7)
| Classe | Dado Vita | Dado Cimento | Abilità Speciale | Critico |
|---|---|---|---|---|
| Guerriero | d8 | d3→d5 | Mighty Deed (d3→d5+), bonus attacco | IV/V |
| Nano | d10 | d3→d5 | Resistenza veleno,感知 trappole | III/IV/V |
| Elfo | d6 | d4→d7 | Infravisione, percezione porte segrete | II |
| Halfling | d6 | d4→d7 | Resistenza, abilità nascondersi | III |
| Ladro | d6 | d4→d7 | Abilità ladro (furtivo, scassinare, ecc.) | II |
| Chierico | d8 | d4→d7 | Scacciare non-morti, incantesimi divini | III |
| Mago | d4 | d4→d7 | Incantesimi arcani, familiar | I |
| Lv0 | d4 | — | Nessuna | I |
### Mighty Deed (Guerriero)
Prima dell'attacco, dichiara un'impresa (es. "sbalzare lo scudo", "accecare").
Tira d3 (lv1-4), poi d4/d5 a lv superiori.
Se deed ≥ 3, attacco colpisce automaticamente SE l'attacco normale colpisce.
Se deed = risultato massimo, effetto speciale narrativo.
## Combattimento
### Flusso
1. Iniziativa: d20 (più alto → primo)
2. Attacco: d20 + bonus attacco vs CA bersaglio
3. Danno: dado arma + bonus (modificatori)
4. PF: sottratti, mostro morto se PF ≤ 0
### CA e Armature
| Armatura | CA | Dado Fumble |
|---|---|---|
| Nessuna | 10 | d4 |
| Leggera (cuoio) | 12 | d8 |
| Media (maglia) | 14 | d12 |
| Pesante (piastre) | 16 | d16 |
| Scudo | +1 CA | — |
### Tiri Salvezza
- **Tempra**: veleno, malattia, resistenza fisica
- **Riflessi**: trappole, esplosioni, schivare
- **Volontà**: magia, paura, illusione
## Incantesimi
### Spell Check
Tiro = d20 + bonus (non modificatore Intelligenza — è un tiro speciale)
| Tiro | Tier | Effetto |
|---|---|---|
| 1 | Corruzione | Fallimento critico + corruzione/ritorsione |
| < CD | Fallimento | Nessun effetto |
| ≤ 11 | Minore | Effetto base |
| ≤ 17 | Moderato | Effetto migliorato |
| ≤ 19 | Maggiore | Effetto potente |
| 20+ | Massimo | Effetto devastante |
### Corruzione
- Su tiro 1 (sempre)
- Su tiro ≥ 12 se d20 subsequente = 1
- Può causare: mutazioni, danni, follia
## PE e Avanzamento
| Risultato | PE |
|---|---|
| Sconfiggere nemico | 50-75 |
| Superare prova difficile | 10-20 |
| Soluzione creativa | 25-50 |
| Sopravvivenza area | 30-50 |
| Avanzamento lv0 → lv1 | 10 PE (automatico) |
## Classi e Abilità Speciali
### Abilità Ladro (dado dedicato)
- Furtività, Scassinare, Percezione, Acrobazia
- Uso dado abilità (d4→d7 a seconda del livello)
### Scacciare Non-Morti (Chierico)
Tiro d20 + bonus (Volontà ?) vs dado vita del non-morto.
Successo: fuga o distruzione.
### Infravisione (Nano, Elfo, Halfling)
Vedere nel buio fino a 18m (Nano), 9m (Elfo/Halfling).
+145
View File
@@ -0,0 +1,145 @@
# Setup Guide
## Prerequisiti
- Python 3.10+
- Ollama (opzionale, per LLM locale) OPPURE una API key di OpenRouter
- Browser moderno
## Installazione
### 1. Clona il repository
```bash
git clone <repo-url> rpg
cd rpg
```
### 2. Crea ambiente virtuale
```bash
python3 -m venv .venv
source .venv/bin/activate
```
### 3. Installa dipendenze
```bash
pip install fastapi uvicorn httpx
```
## Configurazione LLM
### Opzione A: Ollama (locale, CPU-only)
```bash
# Installa Ollama (se non già presente)
curl -fsSL https://ollama.com/install.sh | sh
# Scarica il modello
ollama pull gemma2:9b
```
Configurazione (tramite env var):
```bash
export DCC_LLM_PROVIDER=ollama
export DCC_OLLAMA_URL=http://192.168.0.50:11434 # default
export DCC_OLLAMA_MODEL=gemma2:9b # default
```
### Opzione B: OpenRouter
```bash
export DCC_LLM_PROVIDER=openrouter
export DCC_OPENROUTER_KEY=sk-or-v1-...
export DCC_OPENROUTER_MODEL=openrouter/free # default
```
### Opzione C: OpenAI-compatible (LLM server locale)
```bash
export DCC_LLM_PROVIDER=local
export DCC_LOCAL_URL=http://<host>:<port>/v1
export DCC_LOCAL_KEY=...
export DCC_LOCAL_MODEL=<model-name>
```
## Avvio
### Sviluppo
```bash
python -m src.web.server
```
Il server parte su `http://0.0.0.0:8300`.
Oppure direttamente con uvicorn:
```bash
uvicorn src.web.server:app --host 0.0.0.0 --port 8300 --reload
```
### Prima campagna
1. Apri `http://localhost:8300` nel browser
2. Inserisci nome campagna e clicca "Crea"
3. Verrà creato automaticamente un PG di default (guerriero lv1)
4. Inizia a giocare! Scrivi "Cerco nella stanza" o "Attacco" o "Esploro"
## CLI Interattiva (senza browser)
Per test rapidi senza browser:
```bash
python -c "
from src.engine import dm, db
db.init_db()
camp_id = db.create_campaign('Test', 'Tomba del Mago-Guerriero')
char_id = db.create_character(camp_id, 'Marco il Fidato', 'guerriero', 'soldato')
db.update_character(char_id, level=1, str=15, agi=11, sta=14, per=9, int=8, lck=10, hp_current=12, hp_max=12, ac=15)
dm.interactive(camp_id)
"
```
## Health Check
```bash
curl http://localhost:8300/health
# → {"status":"ok","provider":"ollama"}
```
## Environment Variables (riepilogo)
| Variabile | Default | Descrizione |
|---|---|---|
| `DCC_LLM_PROVIDER` | `"openrouter"` | Provider LLM: ollama, openrouter, local |
| `DCC_OLLAMA_URL` | `http://192.168.0.50:11434` | URL server Ollama |
| `DCC_OLLAMA_MODEL` | `"gemma2:9b"` | Modello Ollama |
| `DCC_OPENROUTER_KEY` | `""` | API key OpenRouter |
| `DCC_OPENROUTER_MODEL` | `"openrouter/free"` | Modello OpenRouter |
| `DCC_OR_FREE_FALLBACK` | `"1"` | Fallback a modelli free |
| `DCC_LOCAL_URL` | `http://192.168.0.240/v1` | URL server locale |
| `DCC_LOCAL_KEY` | `""` | API key server locale |
| `DCC_LOCAL_MODEL` | `"big-pickle"` | Modello locale |
| `DCC_LLM_TIMEOUT` | `60` | Timeout chiamate LLM (s) |
## Troubleshooting
### "Nessuna campagna" all'avvio
- Creare una campagna dal frontend (form "Crea")
- Il database viene creato automaticamente
### LLM non risponde
- Verificare che Ollama sia in esecuzione: `ollama list`
- Verificare timeout: potrebbe servire `DCC_LLM_TIMEOUT=120`
- Il fallback heuristico interviene automaticamente se LLM fallisce
### WebSocket non si connette
- Verificare che il server sia raggiungibile
- Il frontend ha auto-reconnect ogni 3 secondi
### "pip install" bloccato da PEP 668
Usare ambiente virtuale (`.venv`) o:
```bash
pip install --break-system-packages fastapi uvicorn httpx
```