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

+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 |