Files
weirsoe-party-protocol/party/brain/game.py
T
agw d149aa323e
CI / test (push) Failing after 39s
Udbyder-agnostisk spilhjerne, rummets mikrofon og rigtig musik
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
2026-10-01 21:59:50 +02:00

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