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:
1 parent
ae73fe43d3
commit
90f823fdb9
17 files changed
+4082
No files matched your search
@@ -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)
|
||||
Reference in new issue
Block a user