Files
weirsoe-party-protocol/party/brain/game.py
T
2026-10-03 18:32:47 +02:00

140 lines
9.1 KiB
Python

"""Spilhjernen: kommentarer, dommer, drillerier og slut-anmeldelse. Udbyder-agnostisk (se providers.py).
Alt her er valgfrit og asynkront. Spillet kalder hjernen i baggrunden og bruger svaret, hvis det
kommer i tide; ellers bruges de faste replikker. Hjernen ser kun spillets egne data og et kort
resumé af aftenen (rummets hukommelse), aldrig lyd.
"""
from __future__ import annotations
import json
from functools import lru_cache
from django.conf import settings
from .providers import Provider, build_provider
SYSTEM = """Du er speakeren i "Fup og Fakta", et dansk familie-partyspil i stil med Fibbage fra Jackbox.
Spillet: et kuriøst, sandt faktum vises med et hul. Alle skriver en troværdig løgn. Løgnene blandes med
sandheden, og man skal gætte sandheden. Man får point for at gætte rigtigt og for at narre andre.
Din stil: kæk, tør og kærligt drillende, som Cookie Masterson fra Fibbage, men med dansk humor.
Du driller med varme, aldrig ondt. Spillerne er familie og venner, også børn fra 10 år.
Regler for alt, du skriver:
- Dansk, mundret, skrevet til at blive LÆST HØJT af en stemme. Korte sætninger.
- Aldrig grimt, aldrig om udseende, køn, alder, krop eller lignende. Intet groft, intet om alkohol.
- Ingen tankestreger (hverken — eller - som indskud). Brug komma eller punktum.
- Skriv tal med bogstaver ("tre gange", ikke "3 gange").
- Kald spillerne ved navn. Gæt aldrig køn ud fra et navn: ingen "han", "hun", "ham" eller "hende" om en spiller.
- Én pointe pr. replik. Konkret frem for abstrakt. Hellere kort og skarp end lang og forklarende.
- Gode eksempler på tonen: "Velkommen, Sus. Jeg stoler på dig. Det plejer at gå galt."
"Hele sofaen tog fejl. Sikke en sjælden form for enighed." "Ti sekunder. Det behøver ikke være godt. Bare forkert."
- Brug gerne aftenens hukommelse til en pointe, der bygger videre på noget, der er sket tidligere."""
def _schema(props: dict, required: list[str] | None = None) -> dict:
return {"type": "object", "properties": props, "required": required or list(props), "additionalProperties": False}
COMMENTS = _schema({"comments": {"type": "array", "items": _schema({"id": {"type": "string"}, "line": {"type": "string"}})}})
JUDGE = _schema({"is_truth": {"type": "boolean"}, "inappropriate": {"type": "boolean"}, "message": {"type": "string"}})
TEASES = _schema({"teases": {"type": "array", "items": _schema({"player_id": {"type": "string"}, "text": {"type": "string"}})}})
LINE = _schema({"line": {"type": "string"}})
REVEAL = _schema({"lines": {"type": "array", "items": _schema({"answer_id": {"type": "string"}, "line": {"type": "string"}})}})
REVIEW = _schema({"lines": {"type": "array", "items": _schema({"player_id": {"type": "string"}, "line": {"type": "string"}})}})
class GameBrain:
def __init__(self, provider: Provider):
self.provider = provider
async def _ask(self, task: str, payload: dict, schema: dict, *, max_tokens: int, timeout: float):
user = f"{task}\n\nData (JSON):\n{json.dumps(payload, ensure_ascii=False)}"
return await self.provider.complete_json(SYSTEM, user, schema, max_tokens=max_tokens, timeout=timeout)
async def present_lines(self, question: dict, answers: list[dict]) -> dict[str, str]:
"""{answer_id: overbevisende kommentar} til oplæsningen. answers: [{id, text}] uden at sige hvilket der er sandt."""
res = await self._ask(
"Du læser svarene op ét ad gangen, før spillerne gætter. Skriv til HVERT svar én kort kommentar "
"(højst ti ord), der får netop det svar til at lyde troværdigt, som om det kunne være sandt. "
"Alle svar skal lyde lige overbevisende, så ingen kan høre, hvilket der er sandheden. Røb ALDRIG, "
"hvad der er sandt eller løgn, og nævn ingen spillere.",
{"spørgsmål": question.get("prompt"), "svar": answers},
COMMENTS, max_tokens=700, timeout=settings.BRAIN_TIMEOUT_FAST)
if not res:
return {}
ids = {a["id"] for a in answers}
return {c["id"]: c["line"].strip() for c in res.get("comments", []) if c.get("id") in ids and c.get("line")}
async def reveal_lines(self, question: dict, steps: list[dict], stats: list[str], memory: list[str]) -> dict[str, str]:
"""{answer_id: hel replik} til afsløringen, med fuld viden om hvem der skrev og hvem der faldt.
steps: [{answer_id, svar, type (løgn|husets løgn|sandheden), skrevet_af, valgt_af, publikum}]."""
res = await self._ask(
"Skriv replikken til HVERT trin i afsløringen (højst tyve ord). For en løgn: sig hvem der skrev den, "
"og hvem der faldt for den, og roast løgnen konkret. Var løgnen åbenlyst svag eller tåbelig, så drill "
"dem, der alligevel valgte den. Husets løgn er din egen; vær stolt. For sandheden: sig den, og hvem "
"der fandt den. Brug gerne statistikken over, hvem der falder for hvem ('tredje gang'), og aftenens "
"hukommelse. Start ikke alle replikker ens.",
{"spørgsmål": question.get("prompt"), "trin": steps, "statistik": stats, "aftenens_hukommelse": memory[-15:]},
REVEAL, max_tokens=1200, timeout=settings.BRAIN_TIMEOUT_FAST)
if not res:
return {}
ids = {st["answer_id"] for st in steps}
return {x["answer_id"]: x["line"].strip() for x in res.get("lines", []) if x.get("answer_id") in ids and x.get("line")}
async def lie_progress(self, info: dict, memory: list[str]) -> str | None:
"""En kort bemærkning om en SPILLER (ikke løgnen) mens der skrives. info indeholder aldrig løgnens tekst."""
res = await self._ask(
"Spillerne skriver løgne lige nu. Skriv én kort, drillende bemærkning (højst tolv ord) om spilleren i "
"data, fx om tempo, antal forsøg eller tidligere bedrifter i aftenens hukommelse. Du kender IKKE "
"løgnen og må ikke gætte på dens indhold eller sige noget, der kan afsløre den.",
{"spiller": info, "aftenens_hukommelse": memory[-10:]},
LINE, max_tokens=120, timeout=settings.BRAIN_TIMEOUT_FAST)
line = (res or {}).get("line", "").strip()
return line or None
async def judge_lie(self, question: dict, lie: str) -> dict | None:
"""{is_truth, inappropriate, message} eller None, hvis dommeren ikke svarede."""
return await self._ask(
"En spiller har afleveret en løgn. Afgør to ting. is_truth: er løgnen reelt sandheden (samme betydning, "
"synonym, bøjning, stavefejl eller omskrivning af sandheden eller en af dens stavemåder)? En anden, "
"forkert ting er IKKE sandheden. inappropriate: er løgnen upassende for et familiespil med børn "
"(grov, seksuel, nedsættende, voldelig)? Fjollet og frækt-sjovt er fint. message: hvis en af dem er "
"sand, en kort, venlig besked til spilleren på dansk om at skrive en ny løgn; ellers tom streng.",
{"spørgsmål": question.get("prompt"), "sandheden": question.get("answer"),
"andre_stavemåder": question.get("alternatives", []), "løgn": lie},
JUDGE, max_tokens=200, timeout=settings.BRAIN_TIMEOUT_FAST)
async def teases(self, victims: list[dict], memory: list[str]) -> dict[str, str]:
"""{player_id: drilleri}. victims: [{player_id, name, valgte, skrevet_af}]."""
res = await self._ask(
"Disse spillere er lige faldet for en løgn. Skriv en meget kort drilleri-replik (højst seks ord) til "
"hver, som vises og læses op på deres egen telefon. Personlig og kærlig, gerne med navnet og gerne "
"med en henvisning til aftenens hukommelse, hvis det passer.",
{"narret": victims, "aftenens_hukommelse": memory[-15:]},
TEASES, max_tokens=300, timeout=settings.BRAIN_TIMEOUT_FAST)
if not res:
return {}
ids = {v["player_id"] for v in victims}
return {t["player_id"]: t["text"].strip() for t in res.get("teases", []) if t.get("player_id") in ids and t.get("text")}
async def final_review(self, players: list[dict], memory: list[str]) -> list[tuple[str, str]]:
"""[(player_id, replik)] i rækkefølge efter placering."""
res = await self._ask(
"Spillet er slut. Skriv én personlig anmeldelse (højst femten ord) til hver spiller, ud fra deres "
"placering, statistik og aftenens hukommelse. Kærligt drillende, gerne med et konkret minde fra aftenen.",
{"spillere": players, "aftenens_hukommelse": memory[-30:]},
REVIEW, max_tokens=900, timeout=settings.BRAIN_TIMEOUT_SLOW)
if not res:
return []
ids = {p["player_id"] for p in players}
return [(x["player_id"], x["line"].strip()) for x in res.get("lines", []) if x.get("player_id") in ids and x.get("line")]
@lru_cache(maxsize=1)
def get_brain() -> GameBrain | None:
provider = build_provider(settings.BRAIN_PROVIDER, settings.BRAIN_MODEL, settings.BRAIN_API_KEY,
settings.BRAIN_BASE_URL, settings.BRAIN_EFFORT, settings.BRAIN_WORKSPACE)
return GameBrain(provider) if provider else None