CI / test (push) Failing after 39s
Hjernen (party/brain) skriver kommentarer til løgnene, dømmer løgne (sandheden eller upassende), personlige drillerier og en slut-anmeldelse, alt i baggrunden med faste replikker som fallback. Udbyderen vælges med BRAIN_PROVIDER: anthropic (Claude), openai (OpenAI-kompatibel, fx Codex) eller ollama (fx Gemma). Rummets mikrofon er eksperimentel: storskærmen sender korte lydstumper til talegenkendelse (STT_PROVIDER), og kun et kort resumé af latter og udsagn huskes til hjernen. Musikken er nu lavet med en musikmodel. Claude-Session: https://claude.ai/code/session_01JmhJG6JnhXfzNiKv8f2vHu
110 lines
6.8 KiB
Python
110 lines
6.8 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").
|
|
- É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"}})}})
|
|
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 comment_answers(self, question: dict, answers: list[dict], memory: list[str]) -> dict[str, str]:
|
|
"""{answer_id: kommentar}. answers: [{id, text, kind}] med kind = løgn | husets løgn | sandheden."""
|
|
res = await self._ask(
|
|
"Skriv én kort kommentar (højst tolv ord) til HVERT svar. Kommentaren læses op, lige når svaret "
|
|
"afsløres som løgn eller sandhed. Kommentér selve svaret, ikke hvem der faldt for det (det ved du "
|
|
"ikke endnu), og nævn ikke hvem der skrev det. Til sandheden: reagér på hvor vildt faktummet er.",
|
|
{"spørgsmål": question.get("prompt"), "sandheden": question.get("answer"),
|
|
"forklaring": question.get("note", ""), "svar": answers, "aftenens_hukommelse": memory[-15:]},
|
|
COMMENTS, max_tokens=900, timeout=settings.BRAIN_TIMEOUT_SLOW)
|
|
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 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)
|
|
return GameBrain(provider) if provider else None
|