from __future__ import annotations import json from typing import Any from src.engine import db as db_module from src.engine.game_context import GameContext _KNOWN_TOOLS: dict[str, dict[str, str]] = { "roll_dice": {"die": "tipo dado (d3..d30, d%)", "count": "numero dadi (default 1)", "modifier": "modificatore (default 0)"}, "dice_chain": {"die": "dado iniziale", "steps": "passi nella catena dadi DCC"}, "combat": { "character_id": "id pg", "target_ac": "CA bersaglio", "attack_bonus": "bonus attacco (default 0)", "damage_die": "dado danno (default d8)", "damage_bonus": "bonus danno (default 0)", "mighty_deed_die": "dado deed guerriero (es. d3)", "monster_id": "id mostro (per PF persistenti)", "armor_type": "none/light/medium/heavy", }, "ability_check": {"character_id": "id pg", "dc": "CD prova", "modifier": "bonus/penalità"}, "saving_throw": {"character_id": "id pg", "save_type": "Tempra|Riflessi|Volontà", "dc": "CD", "modifier": "bonus/penalità"}, "movement": {"distance_m": "metri", "terrain": "normale|difficile|molto difficile"}, "spellcasting": {"character_id": "id pg", "spell_name": "nome incantesimo", "spell_check": "valore tiro"}, "update_inventory": {"character_id": "id pg", "item": "nome oggetto", "action": "add|remove|equip|unequip|list", "quantity": "quantità (default 1)"}, "award_xp": {"character_id": "id pg", "xp": "quantità PE"}, "update_quest": {"quest_id": "id missione", "status": "active|completed|failed", "name": "nuovo nome (opzionale)", "description": "nuova descrizione (opzionale)"}, "dm_override": {"command": "narrate|set|force", "params": "parametri opzionali"}, } TOOL_EXAMPLES = """ Esempi: - Attacco con spada → {"tool": "combat", "params": {"character_id": 1, "target_ac": 12, "attack_bonus": 0, "damage_die": "d8"}, "narration": "Sguaini la lama e colpisci il guardiano!"} - Tiro d20 per sentire rumori → {"tool": "roll_dice", "params": {"die": "d20"}, "narration": "Tendi l'orecchio nel buio..."} - Esploro la stanza → {"tool": "dm_override", "params": {"command": "narrate", "params": {"text": "La stanza è polverosa..."}}, "narration": "Scrutando nell'oscurità, noti..."} - Prova di percezione CD 12 → {"tool": "ability_check", "params": {"character_id": 1, "dc": 12}, "narration": "Cerchi indizi nella stanza..."} - Missione completata → {"tool": "update_quest", "params": {"quest_id": 1, "status": "completed"}, "narration": "La missione è stata completata con successo!"} """ SYSTEM_PROMPT = f"""Sei il Dungeon Master di DCC RPG (Dungeon Crawl Classics) — narrazione epico-oscura, tono sword & sorcery. STILE: - 2-3 frasi per scena. Non essere prolisso. - Descrivi suoni, odori, sensazioni tattili. - Usa ITALIANO. - Dai sempre una scelta al giocatore alla fine. - Mantieni il tono weird-fantasy di DCC: pericolo, meraviglia, corruzione. - Usa VOCABOLARIO FANTASY: pietra, tenebra, fiamma, sangue, ferro, ossa, ombra, polvere, ruggine, muschio, crepitio, sussurro, pulsare, vibrare, brillare, incrinato, logoro, antico, dimenticato. - VIETATO usare: termini tecnici, scientifici, moderni, anacronistici (niente HPLC, laser, DNA, algoritmi, frequenze, ecc.). - Niente acronimi, niente linguaggio da laboratorio o ingegneria. MECCANICHE: - CD base: 12 = facile, 15 = media, 18 = difficile - Tiri salvezza: Tempra (veleno/malattia), Riflessi (trappole/esplosioni), Volontà (magia/paura) - Combattimento: iniziativa (d20), attacco (d20 + bonus vs CA), danni (dado arma) - Mighty Deed: i Guerrieri possono fare imprese leggendarie con d3/d4/d5 aggiuntivo STRUMENTI DISPONIBILI: {json.dumps(_KNOWN_TOOLS, indent=2, ensure_ascii=False)} {TOOL_EXAMPLES} FORMATO RISPOSTA — SOLO JSON, nient'altro: {{"tool": "nome_strumento", "params": {{...}}, "narration": "descrizione narrativa"}} Se l'azione è puramente narrativa (dialogo, esplorazione, interazione), usa dm_override con command="narrate". """ def _enrich_areas(ctx_areas: list[dict]) -> list[dict]: area_map = {a["id"]: a for a in db_module.get_all_areas()} enriched = [] for sa in ctx_areas: aid = sa["area_id"] full = area_map.get(aid, {}) enriched.append({ "area_id": aid, "name": full.get("name", aid), "description": full.get("description", ""), "connections": full.get("connections", []), "features": full.get("features", []), "visited": sa.get("visited", 0), "monsters": [m.get("ref_id", "") for m in full.get("monsters", [])], }) return enriched def format_context(ctx: GameContext) -> str: lines = [] camp = ctx.campaign or {"name": "?", "description": ""} lines.append(f"Campagna: {camp['name']}") lines.append(f"Descrizione: {camp.get('description', '')}") lines.append(f"Turno: {ctx.turn}") lines.append("") lines.append("PERSONAGGI:") for c in ctx.characters: lv = c.get("level", 0) label = c.get("occupation", "") if lv == 0 else c.get("char_class", "") bl = f" [{c['id']}] {c['name']} — Lv{lv} {label} ({c.get('alignment', '?')})" stat_str = f" For{c.get('str','?')} Agi{c.get('agi','?')} Sta{c.get('sta','?')} Per{c.get('per','?')} Int{c.get('int','?')} Luc{c.get('lck','?')}" hp_str = f" PF {c['hp_current']}/{c['hp_max']} CA {c.get('ac', 10)} PE {c.get('xp', 0)}" lines.extend([bl, stat_str, hp_str]) lines.append("") if ctx.log: lines.append("CRONOLOGIA (ultimi 5 turni):") for entry in ctx.log[-5:]: a = entry.get("action", "?") r = entry.get("result", "")[:80] lines.append(f" «{a}» → {r}") lines.append("") if ctx.quests: lines.append("MISSIONI:") for q in ctx.quests: lines.append(f" [ID:{q['id']}] {q['name']} ({q['status']})") lines.append("") enriched = _enrich_areas(ctx.areas) if enriched: lines.append("LUOGHI DELL'AVVENTURA (tutte le aree):") visited_ids = {a["area_id"] for a in ctx.areas if a.get("visited")} for a in enriched: status = "visitata" if a["area_id"] in visited_ids else "inesplorata" lines.append(f" [{a['area_id']}] {a['name']} [{status}]") if a["visited"] and a.get("description"): lines.append(f" {a['description'][:120]}") lines.append(f" Collegate: {', '.join(a['connections']) if a['connections'] else 'nessuna'}") if a.get("features"): lines.append(f" Elementi: {' | '.join(a['features'][:3])}") if a.get("monsters"): names = [] for mid in a["monsters"]: m = db_module.get_monster_by_id(mid) if m: names.append(m["name"]) if names: lines.append(f" Creature: {', '.join(names)}") lines.append("") return "\n".join(lines) def build_prompt(ctx: GameContext, player_action: str, current_area_id: str = "") -> str: area_block = "" if current_area_id: area_data = db_module.format_area_context(current_area_id) if area_data: area_block = f"\n\nAREA ATTUALE:\n{area_data}\n" return f"""{format_context(ctx)}{area_block}--- AZIONE GIOCATORE: "{player_action}" Rispondi con il JSON nel formato specificato (tool, params, narration)."""