Files
idriz/idriz/apps/superheroji/manifest.py
idriz e21b08c1b7 Refaktor 3/4: sažetak i bojanka u plugine; jezgro app-agnostično
Assistant više NEMA nijednu app-metodu ni app-specifičan prompt:
- idriz/apps/sazetak/ i idriz/apps/bojanka/ (shema + handler + vlastiti prompt/greeting).
- idriz/util.py: slug() (dijeljeno).
- Jezgro INSTRUCTIONS sada opisuje samo razgovor + generička pravila za alate
  (potvrda prije fizičkog rezultata, dugi poslovi), bez imena app-ova. Finalni
  prompt i pozdrav se sklapaju iz jezgra + fragmenata svih app-ova.
- Svi alati dolaze iz registra plugina; uklonjeni TOOLS i _slug iz assistant.py.

Dodavanje novog malog app-a = samo novi folder u idriz/apps/ + upis u load_apps().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzKpYaEsuWDevuNnvSjcFW
2026-09-11 12:29:13 +02:00

175 lines
9.6 KiB
Python

"""Mini-app 'Superheroji': doprinos promptu, alati i handleri.
Sva logika partije je u logic.py; ovdje je samo spoj na runtime preko AppContext-a.
"""
import asyncio
import logging
from ..base import AppContext, AppSpec, AppTool
from . import logic
log = logging.getLogger("idriz.superheroji")
STATE = "superheroji_match" # ključ per-razgovor stanja (trenutna partija)
INSTRUCTIONS = """IGRA "SUPERHEROJI" (glasom, bez štampanja). Alati: superheroji_nova_partija, superheroji_izbor, superheroji_kraj.
Kako se igra Superheroji:
- NE objašnjavaj pravila na početku. Pretpostavi da djeca znaju igrati. Pravila objasni samo ako te izričito pitaju.
- Kad djeca kažu da žele igrati: prvo pitaj koliko igrača igra (od 2 do 5, mora biti najmanje dvoje), pa ih pitaj za imena. Onda pozovi superheroji_nova_partija sa listom imena. Alat ti vrati broj rundi i redoslijed biranja za prvu rundu.
- U svakoj rundi traži od igrača, TAČNO onim redoslijedom koji ti je alat dao, da svaki kaže jednog lika. Definicija je široka: bilo koji izmišljeni lik iz bilo kog medija — ne samo klasični superheroji. To su i crtani likovi, likovi iz video igara, animea, filmova, knjiga, memova (npr. Kuromi, Pikaču, Goku, La Linea, Mario, Gandalf, Baba Jaga, Mad Max). Zadnji igrač ima prednost jer bira zadnji, zato se redoslijed mijenja svaku rundu — uvijek koristi redoslijed koji ti alat da.
- VRLO VAŽNO ZA IMENA: djeca često biraju likove neobičnih ili stranih imena. Slušaj TAČNO ime koje dijete kaže i NE zamjenjuj ga poznatim superherojem koji zvuči slično (ako dijete kaže "Kuromi", ne smiješ čuti "Hulk"). Ako ime nisi jasno čuo ili ga ne razumiješ, NE POGAĐAJ — reci "Nisam dobro čuo ime, možeš li ponoviti?" i sačekaj. Tek kad si siguran u ime, pozovi alat.
- Čim igrač jasno kaže lika, pozovi superheroji_izbor (igrac, heroj) sa imenom TAČNO kako ga je dijete reklo. Alat ti onda kaže šta dalje. UVIJEK prvo naglas potvrdiš ime tako što ga izgovoriš ("Kuromi, zabilježeno!"), da dijete čuje jesi li dobro razumio — to radiš za SVAKOG igrača, i za zadnjeg u rundi.
- Ako dijete kaže da si pogriješio ime (npr. "nisam rekao Hulk, rekao sam Kuromi"), ODMAH ponovo pozovi superheroji_izbor sa ispravnim imenom za istog igrača. Ne prelazi dalje dok ime nije tačno.
- Ako alat odbije izbor (lik je već pobijedio ranije, ili je već izabran u rundi), kratko reci zašto i traži od tog igrača drugog lika.
- Kad ZADNJI igrač u rundi izabere, presuda kreće SAMA — ne zoveš nikakav alat za nju. Ti tada samo: (1) potvrdiš ime zadnjeg lika naglas i (2) veselo kažeš da sada gledaš ko pobjeđuje ("Da vidim ko je jači..."). Zatim SAČEKAJ — rezultat runde ću ti ja poslati porukom, pa ga tek onda objavi djeci: ko je pobijedio i zašto, veselo. Pobjednički lik se više NE može birati do kraja partije.
- Poruku sa rezultatom (počinje sa REZULTAT RUNDE) obavezno objavi djeci. Ona ti kaže i da li slijedi nova runda (s kojim redoslijedom) ili je partija gotova. Kad je gotova, objavi konačnog pobjednika partije i čestitaj.
- Ako djeca žele stati ranije, pozovi superheroji_kraj.
- Za igru ne treba potvrda kao za štampanje — samo lijepo vodi igru."""
NOVA_SCHEMA = {
"type": "function",
"name": "superheroji_nova_partija",
"description": "Započinje novu partiju igre Superheroji. Pozvati NAKON što si pitao koliko igrača (2-5) i njihova imena. Vraća broj rundi i redoslijed biranja za prvu rundu.",
"parameters": {
"type": "object",
"properties": {
"igraci": {"type": "array", "items": {"type": "string"}, "description": "Imena igrača redom, od 2 do 5 (najmanje dvoje)."},
},
"required": ["igraci"],
},
}
IZBOR_SCHEMA = {
"type": "function",
"name": "superheroji_izbor",
"description": "Prijavljuje heroja kojeg je jedan igrač izabrao u trenutnoj rundi. Pozvati čim igrač kaže ime lika. Vraća potvrdu ili razlog odbijanja.",
"parameters": {
"type": "object",
"properties": {
"igrac": {"type": "string", "description": "Ime igrača koji bira."},
"heroj": {"type": "string", "description": "Ime lika kako ga je igrač rekao."},
},
"required": ["igrac", "heroj"],
},
}
KRAJ_SCHEMA = {
"type": "function",
"name": "superheroji_kraj",
"description": "Prekida trenutnu partiju ranije ako igrači žele stati. Vraća trenutni rezultat.",
"parameters": {"type": "object", "properties": {}},
}
_FILLERS = [
"Reci kratko i veselo, u jednoj rečenici, da još razmišljaš ko bi pobijedio. Ne otkrivaj pobjednika.",
"Reci u jednoj rečenici da je borba tijesna i da još vagaš ko je jači. Ne otkrivaj pobjednika.",
"Reci kratko da samo što nisi odlučio, još malo. Ne otkrivaj pobjednika.",
]
async def nova(ctx: AppContext, args: dict) -> str:
players = args.get("igraci") or []
if isinstance(players, str):
players = [players]
players = [p.strip() for p in players if p and p.strip()]
if len(players) < 2:
return ("Za igru Superheroji treba najmanje dvoje igrača. Reci djeci da moraju biti bar dvoje, "
"pitaj koliko će igrati (2 do 5) i njihova imena, pa ponovo pozovi superheroji_nova_partija.")
match = logic.Match(ctx.client, ctx.cfg.game_model, players)
ctx.set_state(STATE, match)
info = match.start_info()
order = ", ".join(info["redoslijed"])
prvi = info["redoslijed"][0]
return (f"Partija je počela. Igrači: {', '.join(info['igraci'])}. Broj rundi: {info['broj_rundi']}. "
f"RUNDA 1 — redoslijed biranja: {order}. "
f"Traži od igrača {prvi} da kaže svog superheroja, pa redom ostale tim redoslijedom. "
f"Kad ti igrač kaže heroja, pozovi superheroji_izbor.")
async def izbor(ctx: AppContext, args: dict) -> str:
match = ctx.get_state(STATE)
if not match:
return "Nema aktivne partije. Prvo pokreni igru sa superheroji_nova_partija."
res = await match.record_pick(args.get("igrac", ""), args.get("heroj", ""))
if not res.get("ok"):
return (res.get("poruka", "Taj izbor ne može proći.") +
" Kad ti dijete kaže drugog lika, ponovo pozovi superheroji_izbor.")
name = res["heroj"]
confirm = (f'OBAVEZNO prvo reci djetetu tačno ovo ime naglas: "{name}, zabilježeno!" — koristi baš to ime '
f'da dijete čuje kako si ga razumio. Ako dijete kaže da to nije taj lik, ponovo pozovi '
f'superheroji_izbor sa ispravnim imenom za istog igrača.')
if match.round_complete:
ctx.background(_adjudicate_and_announce(ctx, match))
return (confirm + ' Svi su izabrali. Zatim veselo reci da SADA gledaš ko pobjeđuje '
'(npr. "Da vidim ko je jači, samo tren..."). NE zovi nijedan alat — '
'rezultat runde ću ti poslati porukom, pa ga onda objavi.')
nxt = match.next_picker()
return confirm + f" Zatim pitaj igrača {nxt} da kaže svog lika."
async def kraj(ctx: AppContext, args: dict) -> str:
match = ctx.get_state(STATE)
if not match:
return "Trenutno nema aktivne partije."
st = match.standings()
poredak = ", ".join(f"{r['igrac']} {r['pobjede']}" for r in st["poredak"])
ctx.set_state(STATE, None)
if st["pobjednici"] and not st["nerijeseno"]:
return f"Partija je prekinuta. Do sada vodi {st['pobjednici'][0]}. Poredak: {poredak}. Reci to djeci."
return f"Partija je prekinuta. Poredak: {poredak}. Reci to djeci."
async def _adjudicate_and_announce(ctx: AppContext, match) -> None:
"""Presudi rundu (može potrajati zbog prepoznavanja/web searcha). Ubacuj filler dok traje."""
adj = asyncio.create_task(match.resolve_round())
i = 0
try:
while True:
try:
res = await asyncio.wait_for(asyncio.shield(adj), timeout=9)
break
except asyncio.TimeoutError:
await ctx.nudge(_FILLERS[min(i, len(_FILLERS) - 1)])
i += 1
except Exception: # noqa: BLE001
log.exception("presuda runde nije uspjela")
await ctx.notify("Došlo je do greške pri presudi runde. Reci djeci da još jednom kažu svoje likove.")
return
await ctx.notify(_format_verdict(ctx, res))
def _format_verdict(ctx: AppContext, res: dict) -> str:
if not res.get("ok"):
return res.get("poruka", "Runda još nije spremna za presudu.")
txt = (f"REZULTAT RUNDE {res['runda']}: pobjeđuje {res['pobjednik_heroj']} "
f"(izabrao ga {res['pobjednik_igrac']}). Obrazloženje: {res['obrazlozenje']} "
f"Objavi ovo djeci veselo — ko je pobijedio i zašto. "
f"Napomeni da se {res['pobjednik_heroj']} više ne može birati u ovoj partiji.")
if res.get("gotovo"):
st = res["konacni_rezultat"]
poredak = ", ".join(f"{r['igrac']} {r['pobjede']}" for r in st["poredak"])
if st["nerijeseno"]:
txt += f" PARTIJA JE GOTOVA! Neriješeno je (poredak: {poredak}). Čestitaj svima."
else:
txt += f" PARTIJA JE GOTOVA! Pobjednik partije je {st['pobjednici'][0]}! Poredak: {poredak}. Čestitaj pobjedniku."
ctx.set_state(STATE, None)
else:
order = ", ".join(res["sljedeci_redoslijed"])
prvi = res["sljedeci_redoslijed"][0]
txt += (f" Zatim kreće RUNDA {res['sljedeca_runda']} — redoslijed: {order}. "
f"Traži od igrača {prvi} novog heroja.")
return txt
SPEC = AppSpec(
name="superheroji",
title="Superheroji",
instructions=INSTRUCTIONS,
greeting="igrati igru Superheroji",
tools=[
AppTool(NOVA_SCHEMA, nova, "inline"),
AppTool(IZBOR_SCHEMA, izbor, "inline"),
AppTool(KRAJ_SCHEMA, kraj, "inline"),
],
)