Files
dcc-rpg/src/engine/prompts.py
T
truetype b985df0017 feat(tools): add update-quest tool, switch to Big Pickle (opencode/zen)
- 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
2026-06-28 10:01:33 +02:00

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)."""