Fest: spørgsmål til unge, zoomer-hjerne og zoomer-drillerier
CI / test (push) Successful in 57s

Fest-pakken handler nu om ungdomskultur: "Fest: slang" (årets ord, slangens
oprindelse, dansk ungdomssprog) og "Fest: internettet" (memes, creators, spil),
ved siden af "Fest: nu". "Fest: retro" er taget ud.

Hjernen får sin egen fest-instruks (zoomer-speaker, tår må nævnes), så
afsløringens replikker og kommentarer også taler zoomer. Telefonens
drillerier har en fest-pulje, og de sidste faste replikker har fået
fest-udgaver.

Claude-Session: https://claude.ai/code/session_01JmhJG6JnhXfzNiKv8f2vHu
This commit is contained in:
agw
2026-10-18 12:59:29 +02:00
parent a40243ed23
commit 87ef9a6ba0
7 changed files with 1396 additions and 612 deletions
File diff suppressed because it is too large Load Diff
+45
View File
@@ -140,6 +140,13 @@
"[suspicious] Der var en, der skrev det rigtige svar. Det tæller ikke.",
"Sandheden er ikke en løgn. Prøv igen."
],
"lie_was_truth_fest": [
"Bro skrev sandheden. Det er ikke rizz, det er snyd. Prøv igen.",
"[sarcastic] Wow, ærlighed. Super mid. Skriv en løgn, bestie.",
"Nogen ramte sandheden. Lowkey sus. Gem den til gættefasen.",
"Det var faktisk sandt, no cap. Vi skal bruge en løgn, ikke en TED talk.",
"[gasps] Sandheden sneg sig ind. Delulu er fint her, skriv noget falsk."
],
"guess_start": [
"Én af dem er sand. Held og lykke. I får brug for det.",
"Vælg det sande svar. Selvtillid er desværre en dårlig kildehenvisning.",
@@ -345,6 +352,16 @@
"{svar}. Tænk lige over den.",
"Og {svar}. Hvorfor ikke?"
],
"present_answer_fest": [
"{svar}.",
"Måske {svar}.",
"Eller hvad med {svar}?",
"{svar}. Lowkey plausibelt.",
"{svar}? Hmm, it's giving.",
"Så er der {svar}.",
"[whispers] {svar}.",
"{svar}. Tænk lige over den, bestie."
],
"summary_cleanup": [
"De her løgne troede ingen på. Lad os lige rydde op.",
"[sighs] Ingen faldt for de her. Væk med dem.",
@@ -386,6 +403,13 @@
"{svar} fik flest reaktioner. Selv jeg måtte trække på smilebåndet.",
"Stuen elskede {svar}. Så er det sagt."
],
"summary_favorite_fest": [
"Publikums favorit var {svar}. I ate den op.",
"[laughs] {svar} fik hele festen til at gå amok. W.",
"Chatten var vild med {svar}. Main character energy.",
"{svar} fik flest reaktioner. Aura plus tusind.",
"I elskede {svar}. Slay, no cap."
],
"lie_progress_fast": [
"{navn} er allerede færdig. Mistænkeligt hurtigt.",
"[suspicious] {navn} har allerede løjet. Det gik lidt for let.",
@@ -398,6 +422,13 @@
"{navn} er færdig. Resten af jer kan godt mærke presset nu.",
"[impressed] {navn} har allerede løjet. Imponerende. Og lidt bekymrende."
],
"lie_progress_fast_fest": [
"{navn} er allerede færdig. Speedrun, any procent.",
"[suspicious] {navn} har løjet på rekordtid. Lowkey sus.",
"{navn} er klar. Sigma grindset.",
"[chuckles] {navn} er færdig allerede. Bro tænker ikke, bro gør bare.",
"Første løgn er inde, tak til {navn}. Resten af jer er cooked."
],
"lie_progress_retry": [
"{navn} er færdig. Andet forsøg sad i skabet.",
"{navn} fik det til sidst. Øvelse gør mester.",
@@ -408,6 +439,12 @@
"[sighs] {navn} er endelig færdig. Jeg var lige ved at blive bekymret.",
"{navn} fandt en ny løgn. Den gamle var for ærlig."
],
"lie_progress_retry_fest": [
"{navn} er færdig. Andet forsøg, and we ball.",
"[chuckles] {navn} prøvede igen, og nu sidder den. Character development.",
"{navn} kom i mål efter lidt bøvl. Det tæller stadig, bestie.",
"{navn} fandt en ny løgn. Den gamle var alt for ærlig."
],
"lie_progress": [
"{navn} er færdig. Lad os håbe, den er god.",
"{navn} har løjet. Jeg tager det ikke personligt.",
@@ -420,6 +457,14 @@
"{navn} er inde. Nu mangler vi bare resten af familien.",
"[whispers] {navn} er færdig. Og jeg tror, den er lumsk."
],
"lie_progress_fest": [
"{navn} er færdig. Lad os håbe, den slayer.",
"{navn} har løjet. Aura farmet.",
"[mischievously] {navn} har skrevet noget. Jeg siger intet, men it's giving.",
"{navn} er inde. Selvsikker. Det er et red flag.",
"[whispers] {navn} er færdig. Og den er lowkey lumsk.",
"En løgn mere i posen. W, {navn}."
],
"lobby_welcome_fest": [
"[excited] Yo yo yo, velkommen til festen! Telefonen frem, glasset fyldt, hjernen må gerne blive hjemme.",
"Hvad så, besties? Det her er Fup og Fakta, festudgaven. Jeres lever har ikke sagt ja, men vi kører alligevel.",
+38 -8
View File
@@ -36,6 +36,32 @@ Regler for alt, du skriver:
"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."""
# Fest-spillet (18+): samme spil, men speakeren er en hypet zoomer, og det handler om tår frem for point.
SYSTEM_FEST = """Du er speakeren i "Fup og Fakta: Fest", et dansk partyspil for unge voksne (18+) i stil med Fibbage.
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. Falder man for en løgn, tager man en tår (to i runde to, en shot i
finalen). Finder ingen sandheden, drikker alle. Tårene betyder mere end pointene.
Din stil: en hypet, memet zoomer-streamer fra København med brainrot-energi. Du taler dansk blandet med
zoomer- og internetslang: no cap, for real, lowkey, highkey, it's giving, slay, ate, rizz, aura, cooked,
delulu, NPC, main character energy, Ohio, sus, bestie, based, mid, W og L, skibidi, sigma, "hold da op, bro".
Brug slangen naturligt, et eller to udtryk pr. replik, ikke en ordsalat. Du roaster kærligt og hyper folk op.
Regler for alt, du skriver:
- Mundret, skrevet til at blive LÆST HØJT af en stemme. Korte sætninger.
- Aldrig grimt, aldrig om udseende, køn, krop eller lignende. Intet groft eller seksuelt. Du må gerne nævne
tår, shots og at nogen skal drikke, men opfordr aldrig til at drikke sig fuld, og hyld gerne vand.
- Ingen tankestreger (hverken — eller - som indskud). Brug komma eller punktum.
- Skriv tal med bogstaver ("tre tår", ikke "3 tår").
- Du må give stemmen en lyd-tag i firkantede klammer, fx "[laughs] Bro er cooked." Kun disse: [laughs]
[chuckles] [giggles] [sighs] [whispers] [excited] [mischievously] [sarcastic] [suspicious] [impressed]
[gasps] [dramatic] [cheering] [curious] [snorts]. Højst én pr. replik. Tags vises ikke på skærmen.
- 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.
- Gode eksempler på tonen: "Sus faldt for den. Lowkey cooked, tag en tår, bestie."
"Ingen fandt sandheden. Kollektiv Ohio-moment. Alle drikker." "Mads skrev den løgn. Aura plus tusind, no cap."
- 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}
@@ -50,12 +76,14 @@ REVIEW = _schema({"lines": {"type": "array", "items": _schema({"player_id": {"ty
class GameBrain:
def __init__(self, provider: Provider):
def __init__(self, provider: Provider, fest: bool = False):
self.provider = provider
self.fest = fest
self.system = SYSTEM_FEST if fest else SYSTEM
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)
return await self.provider.complete_json(self.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."""
@@ -104,9 +132,11 @@ class GameBrain:
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.",
"forkert ting er IKKE sandheden. inappropriate: er løgnen upassende for "
+ ("en fest blandt unge voksne (hadsk, nedsættende, grov seksuel, voldelig)? Fræk og plat er fint. "
if self.fest else "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)
@@ -137,8 +167,8 @@ class GameBrain:
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:
@lru_cache(maxsize=2)
def get_brain(fest: bool = False) -> 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
return GameBrain(provider, fest=fest) if provider else None
+2 -2
View File
@@ -7,7 +7,7 @@ from django.conf import settings
from fupogfakta.engine import RULES, RULES_FEST
from fupogfakta.models import Category, Question
from party import tts
from party.narrator import LINES, TEASES, Narrator, speakable_prompt
from party.narrator import LINES, TEASES, TEASES_FEST, Narrator, speakable_prompt
def _expand_categories(lines: list[str], categories: list[str]) -> list[str]:
@@ -44,7 +44,7 @@ class Command(BaseCommand):
# fest-speakeren siger "<begivenhed>_fest", og den almindelige replik, hvor der ikke er en fest-udgave
keys = [k for k in LINES if k.endswith("_fest") or k + "_fest" not in LINES]
fest_static = [line for k in keys for line in LINES[k] if not re.search(r"\{\w+\}", line)]
fest_static += TEASES + [r["say"] for r in RULES_FEST]
fest_static += TEASES_FEST + [r["say"] for r in RULES_FEST]
ok = sum(1 for line in fest_static if tts.synth_sync(line, fest))
self.stdout.write(self.style.SUCCESS(f"{ok} af {len(fest_static)} faste fest-replikker ligger i cachen."))
if budget <= 0:
+7
View File
@@ -81,6 +81,13 @@ TEASES = ["Buh!", "Hov hov, der røg du i!", "Avs, avs, avs!", "Snydt!", "Hihi,
"Ups, der røg pointene.", "[mischievously] Tak for pointene!", "Godtroende!", "Sikke en naiv sjæl!",
"Narret!", "Fanget med fingrene i løgnen!", "[gasps] Den troede du på?", "Lidt for tillidsfuld!",
"Hjem og læs lektier!", "[sighs] Åh nej, ikke dig også.", "Den løgn sad lige i skabet!", "Bum! Fanget!"]
# Fest-spillets drillerier: samme idé, bare på zoomer, og der skal drikkes.
TEASES_FEST = ["Bro er cooked!", "[laughs] L, tag en tår!", "Ohio-moment! Skål!", "NPC-behavior!", "Aura minus tusind!",
"Du blev scammet, bestie!", "[gasps] Du troede på den? Delulu!", "Skibidi-snydt!", "Lowkey pinligt. Drik!",
"Det var ikke rizz, det var en løgn!", "[sarcastic] Big brain move. Not.", "Emotional damage! Skål!",
"Fanum tax på din tår!", "[chuckles] Mid gæt, fuld tår!", "Rip bozo. Drik!", "Ratio! Tag en tår!",
"Caught in 4K!", "[whispers] Sus. Meget sus.", "Den var for nem, bestie!", "It's giving godtroende!",
"Bussin' løgn, cooked gæt!", "[mischievously] Tak for tåren!", "No cap, du blev fooled!", "Main character? Nah, NPC!"]
class Narrator:
+3 -3
View File
@@ -23,7 +23,7 @@ from fupogfakta.engine import Game
from . import stt, tts
from .assets import tease_images
from .brain import get_brain
from .narrator import TEASES, Narrator, untag
from .narrator import TEASES, TEASES_FEST, Narrator, untag
log = logging.getLogger(__name__)
@@ -77,7 +77,7 @@ class Room:
self._phase_key = game.phase_key()
self._pending: dict[str, list[float]] = {} # fasenøgle -> starttider for replikker under generering
# Hjernen og hukommelsen
self.brain = get_brain()
self.brain = get_brain(fest=game.fest)
self.memory: list[str] = []
self._tease_texts: dict[str, str] = {}
self.fooled_pairs: dict[tuple[str, str], int] = {} # (narret, forfatter) -> antal gange i aften
@@ -642,7 +642,7 @@ class Room:
g = self.game
if g.phase != "reveal" or (g.round_idx, g.q_idx) != ident:
return
text = self._tease_texts.get(pid) or self.narrator.rng.choice(TEASES)
text = self._tease_texts.get(pid) or self.narrator.rng.choice(TEASES_FEST if self.game.fest else TEASES)
asyncio.get_running_loop().create_task(self._send_tease(pid, text))
async def _send_tease(self, pid: str, text: str):
+37
View File
@@ -210,3 +210,40 @@ class PitchTests(SimpleTestCase):
pitch = {engine.normalize(k): v for k, v in q["pitch"].items()}
covered = {a.id for a in answers if engine.normalize(a.text) in pitch}
self.assertEqual(covered, {"a1", "a2"}) # spillerens løgn (a3) er den eneste, hjernen skal skrive til
class SystemRecorder:
name = "fake"
def __init__(self):
self.systems, self.users = [], []
async def complete_json(self, system, user, schema, **kw):
self.systems.append(system)
self.users.append(user)
return {}
class FestBrainTests(SimpleTestCase):
def test_fest_brain_speaks_zoomer(self):
from party.brain.game import SYSTEM, SYSTEM_FEST
fake = SystemRecorder()
asyncio.run(GameBrain(fake, fest=True).reveal_lines({"prompt": "x"}, [], [], []))
asyncio.run(GameBrain(fake).reveal_lines({"prompt": "x"}, [], [], []))
self.assertEqual(fake.systems, [SYSTEM_FEST, SYSTEM])
self.assertIn("no cap", SYSTEM_FEST)
def test_fest_judge_is_not_a_family_game(self):
fake = SystemRecorder()
asyncio.run(GameBrain(fake, fest=True).judge_lie({"prompt": "x", "answer": "y"}, "z"))
self.assertNotIn("børn", fake.users[0])
def test_fest_room_gets_fest_brain_and_teases(self):
from fupogfakta.engine import Game
from party.narrator import TEASES, TEASES_FEST
with mock.patch.object(rooms, "get_brain", return_value=None) as gb:
rooms.create_room([], "agw", "http://testserver", fest=True)
gb.assert_called_with(fest=True)
self.assertGreaterEqual(len(TEASES_FEST), 10)
self.assertFalse(set(TEASES_FEST) & set(TEASES))
self.assertTrue(Game("ABCD", [], fest=True).fest)