- Fase 0: env var allineate, SYSTEM_PROMPT deduplicato, .gitignore pulito - Fase 1: GameContext isolato, ciclo DM unificato, provider thread-safe (contextvars) - Fase 2: DB schema esteso (current_area_id, combat_state, character_items) - Fase 3: thread-local connection pool + WAL + busy_timeout - Fase 4: test suite (32 test, 100% pass) - heuristic_fallback come parser di riserva (non secondo pilota)
112 lines
3.4 KiB
Python
112 lines
3.4 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)
|
|
|
|
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)
|
|
|
|
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)
|