Files
dcc-rpg/src/engine/dm.py
T
truetype eacc946868 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
2026-06-28 08:18:24 +02:00

131 lines
4.3 KiB
Python

from __future__ import annotations
import logging
from src.engine import db, tools
from src.engine.config import load_from_env
from src.engine.game_context import GameContext
from src.engine.heuristic_fallback import parse_action as fallback_parse
from src.engine.llm import ask as llm_ask, parse_tool_call
from src.engine.prompts import SYSTEM_PROMPT, build_prompt
log = logging.getLogger(__name__)
def _llm_decision(ctx: GameContext, player_action: str, current_area_id: str = "") -> tuple[str, dict, str]:
prompt = build_prompt(ctx, player_action, current_area_id=current_area_id)
try:
raw = llm_ask(prompt, system=SYSTEM_PROMPT)
except Exception as e:
log.warning("LLM call failed (%s), fallback a heuristic parser", e)
tool, params = fallback_parse(player_action, ctx.characters)
return tool, params, ""
decision = parse_tool_call(raw)
tool = decision.get("tool", "")
params = decision.get("params", {})
narration = decision.get("narration", "")
if not tool:
log.info("LLM non ha prodotto tool valido, fallback heuristico")
tool, params = fallback_parse(player_action, ctx.characters)
# Se il JSON era malformato e params sono vuoti, usa l'euristico per i parametri
elif not params and tool not in ("dm_override",):
log.info("Tool %s senza params, integro con fallback euristico", tool)
_, fallback_params = fallback_parse(player_action, ctx.characters)
params = fallback_params
# dm_override/narrate: il LLM spesso mette il testo in params.params.text
# invece che nel campo narration JSON — lo estraiamo qui
if tool == "dm_override" and not narration:
inner = params.get("params", {}) if isinstance(params, dict) else {}
if isinstance(inner, dict):
text = inner.get("text", "")
if text:
narration = text
return tool, params, narration
def dm_cycle(campaign_id: int, player_action: str) -> dict:
ctx = GameContext.load(campaign_id)
tool_name, tool_params, narration = _llm_decision(ctx, player_action, current_area_id=ctx.current_area_id)
# Inietta campaign_id per strumenti che necessitano persistenza
if tool_name == "combat":
tool_params.setdefault("campaign_id", campaign_id)
result = tools.run(tool_name, **tool_params)
mechanic_msg = result.get("message", "")
full_msg = f"{narration}\n{mechanic_msg}".strip()
turn = db.advance_turn(campaign_id)
db.log_turn(campaign_id, turn, player_action, full_msg)
return {
"turn": turn,
"action": player_action,
"tool": tool_name,
"params": tool_params,
"narration": narration,
"mechanics": mechanic_msg,
"full_output": full_msg,
}
def interactive(campaign_id: int):
cfg = load_from_env()
print("=" * 60)
print("DCC RPG — AI Dungeon Master")
print(f"Provider: {cfg.kind} | Modello: {getattr(cfg, f'{cfg.kind}_model', '?')}")
print("Scrivi 'esci' per uscire, 'stato' per vedere la scheda.")
print("=" * 60)
print()
ctx = GameContext.load(campaign_id)
print(ctx.summary())
print()
while True:
try:
action = input("⚔️ » ").strip()
except (EOFError, KeyboardInterrupt):
print("\nFine partita.")
break
if not action:
continue
if action.lower() in ("esci", "exit", "quit"):
print("Alla prossima avventura!")
break
if action.lower() in ("stato", "stat", "status"):
print(GameContext.load(campaign_id).summary())
continue
print("🎲 Elaborazione...")
out = dm_cycle(campaign_id, action)
print(f"\n📖 {out['narration'] or '(azione riconosciuta)'}")
if out['mechanics']:
print(f"⚙️ {out['mechanics']}")
print()
if __name__ == "__main__":
import sys
db.init_db()
camp_id = db.create_campaign("Avventura 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,
armor_type="light",
)
interactive(camp_id)