feat: AI Dungeon Master engine + web frontend

- Engine: stato SQLite, state machine, 10 approved tools, parser azioni
- LLM: multi-provider (Ollama/OpenRouter/Local), system prompt DCC
- DM cycle: LLM decide tool → engine esegue → narrazione + meccanica
- Dati avventura: JSON machine-readable con 9 aree, mostri, enigmi, quest
- Web: FastAPI + WebSocket backend, SPA frontend dark fantasy
- Ref: architecture-patterns/neon-scratch-lounge (separazione reasoning/execution)
This commit is contained in:
truetype committed 2026-06-26 20:31:34 +02:00
1 parent ae73fe43d3
commit 90f823fdb9
17 files changed
+4082

No files matched your search

+110
View File
@@ -0,0 +1,110 @@
from __future__ import annotations
import logging
from src.engine import db, tools
from src.engine.config import load_from_env
from src.engine.llm import ask as llm_ask, parse_tool_call
from src.engine.prompts import SYSTEM_PROMPT, build_prompt
from src.engine.state_machine import GameContext, _parse_action
log = logging.getLogger(__name__)
def _llm_decision(ctx: GameContext, player_action: str) -> tuple[str, dict, str]:
prompt = build_prompt(ctx, player_action)
try:
raw = llm_ask(prompt, system=SYSTEM_PROMPT)
except Exception as e:
log.warning("LLM call failed (%s), fallback a _parse_action", e)
tool, params = _parse_action(player_action, ctx)
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 = _parse_action(player_action, ctx)
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)
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)