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:
1 parent
6b048dc0cc
commit
eacc946868
23 files changed
+1786
-61
No files matched your search
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 |
|
||||
@@ -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
|
||||
@@ -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).
|
||||
@@ -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
|
||||
```
|
||||
Reference in new issue
Block a user