CI / test (push) Successful in 35s
Claude-Session: https://claude.ai/code/session_01JmhJG6JnhXfzNiKv8f2vHu
140 lines
9.1 KiB
Python
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
|