- update_quest: aggiorna stato/nome/descrizione missioni - db: get_quest(), update_quest con nome+desc - prompt: quest_id nel contesto, esempio nei tool examples - Switch da deepseek-v4-flash (go) a big-pickle (zen) — gratuito
153 lines
7.3 KiB
Python
153 lines
7.3 KiB
Python
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)."""
|