Fest-Z: eget emoji-sæt og nyt navn
CI / test (push) Successful in 1m32s

Fest-spillet hedder nu Fest-Z (kategorierne "Fest-Z: …"), og telefonerne
får zoomer-emojis (💀 😭 🧢 🗿 💅 🍻) i stedet for familie-sættet. Serveren
sender sættet i tilstanden, så klienten altid viser det, spillet tager imod.
Reglerne og speakeren taler om slang og memes frem for pop-sladder.

Claude-Session: https://claude.ai/code/session_01JmhJG6JnhXfzNiKv8f2vHu
This commit is contained in:
agw
2026-10-18 13:17:26 +02:00
parent 87ef9a6ba0
commit c3f9a5c8b4
12 changed files with 102 additions and 81 deletions
+1 -1
View File
@@ -103,7 +103,7 @@ async def _player(ws_base, code, name, n_players, done, rng, stop_at_final):
await asyncio.sleep(rng.uniform(0.05, 0.4))
if st["phase"] in ("present", "guess", "reveal") and rng.random() < 0.25:
for _ in range(rng.randint(1, 8)): # emojis må spammes; serveren drosler
await ws.send(json.dumps({"action": "react", "emoji": rng.choice(["😂", "😱", "👏", "🤯", "🙈", "🔥"])}))
await ws.send(json.dumps({"action": "react", "emoji": rng.choice(st.get("reactions") or ["😂", "😱", "👏", "🤯", "🙈", "🔥"])}))
if st["phase"] == "lobby" and me["vip"] and len(st["players"]) >= n_players and key not in acted:
acted.add(key)
await ws.send(json.dumps({"action": "start"}))
+2 -2
View File
@@ -32,7 +32,7 @@ Alle beskeder er JSON `{"action": "...", ...}`.
| `lie_for_me` | | fase `lie`; svar: `suggestion` |
| `guess` | `answer_id` | fase `guess`; spillere og publikum, én gang |
| `like` | `answer_id` | kun fase `reveal`; slår til/fra; ikke eget svar og ikke sandheden (`answer_invalid`) |
| `react` | `emoji` (en af `😂 😱 👏 🤯 🙈 🔥`) | fase `present`, `guess`, `reveal`; må spammes (serveren dropper stille over 6/s pr. person). Intet svar; storskærmen får `react`. Tælles på det svar, der er i fokus på storskærmen. |
| `react` | `emoji` (en af tilstandens `reactions`: `😂 😱 👏 🤯 🙈 🔥`, i fest-spil `💀 😭 🧢 🗿 💅 🍻`) | fase `present`, `guess`, `reveal`; må spammes (serveren dropper stille over 6/s pr. person). Intet svar; storskærmen får `react`. Tælles på det svar, der er i fokus på storskærmen. |
| `play_again` | | VIP i fasen `final` |
| `ping` | | svar `pong` |
@@ -185,7 +185,7 @@ Rækkefølge: `category → intro → lie → guess → reveal (→ scores)`; i
- Publikum: forfatteren får `round(500 × multiplikator × andel af publikum narret)`.
- Spil igen: samme spillere, point nulstilles.
## Fest-spil (pakken "fest", 18+)
## Fest-Z (pakken "fest", 18+)
Oprettes med `pack=fest` på `/host/new`. Snapshot har altid `fest` (bool). I et fest-spil desuden:
+66 -66
View File
@@ -1,6 +1,6 @@
[
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -26,7 +26,7 @@
}
},
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -52,7 +52,7 @@
}
},
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -79,7 +79,7 @@
}
},
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -107,7 +107,7 @@
}
},
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -135,7 +135,7 @@
}
},
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -161,7 +161,7 @@
}
},
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -187,7 +187,7 @@
}
},
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -213,7 +213,7 @@
}
},
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -240,7 +240,7 @@
}
},
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -268,7 +268,7 @@
}
},
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -295,7 +295,7 @@
}
},
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -321,7 +321,7 @@
}
},
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -348,7 +348,7 @@
}
},
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -374,7 +374,7 @@
}
},
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -400,7 +400,7 @@
}
},
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -427,7 +427,7 @@
}
},
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -453,7 +453,7 @@
}
},
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -480,7 +480,7 @@
}
},
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -507,7 +507,7 @@
}
},
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -534,7 +534,7 @@
}
},
{
"category": "Fest: nu",
"category": "Fest-Z: nu",
"packs": [
"fest"
],
@@ -561,7 +561,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -590,7 +590,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -618,7 +618,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -646,7 +646,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -673,7 +673,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -700,7 +700,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -726,7 +726,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -754,7 +754,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -781,7 +781,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -809,7 +809,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -837,7 +837,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -863,7 +863,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -889,7 +889,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -918,7 +918,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -947,7 +947,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -975,7 +975,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -1004,7 +1004,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -1033,7 +1033,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -1061,7 +1061,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -1088,7 +1088,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -1116,7 +1116,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -1144,7 +1144,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -1171,7 +1171,7 @@
}
},
{
"category": "Fest: slang",
"category": "Fest-Z: slang",
"packs": [
"fest"
],
@@ -1198,7 +1198,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1225,7 +1225,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1253,7 +1253,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1282,7 +1282,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1311,7 +1311,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1338,7 +1338,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1368,7 +1368,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1397,7 +1397,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1424,7 +1424,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1452,7 +1452,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1481,7 +1481,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1511,7 +1511,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1539,7 +1539,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1567,7 +1567,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1596,7 +1596,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1625,7 +1625,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1654,7 +1654,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1683,7 +1683,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1711,7 +1711,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1738,7 +1738,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1766,7 +1766,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
@@ -1795,7 +1795,7 @@
}
},
{
"category": "Fest: internettet",
"category": "Fest-Z: internettet",
"packs": [
"fest"
],
+11 -5
View File
@@ -34,6 +34,7 @@ REVEAL_STEP_MS = 4000
PRESENT_STEP_MS = 2600 # pr. svar under oplæsningen, når der ikke er stemme
PRESENT_INTRO_MS = 1500
REACTIONS = ("😂", "😱", "👏", "🤯", "🙈", "🔥")
REACTIONS_FEST = ("💀", "😭", "🧢", "🗿", "💅", "🍻") # fest: dødt af grin, cap, moai-blik, slay, skål
REACT_PER_SECOND = 6 # man må gerne spamme, men ikke uendeligt
REVEAL_TAIL_MS = 8000 # opsummeringen efter sidste trin: løgne der eksploderer, point der tæller op
@@ -69,15 +70,15 @@ RULES_STEP_MS = 5000
# Fest (18+): samme spil, men det handler om tår. Narret = drik; ingen fandt sandheden = alle drikker.
# Antal tårer følger rundens gange (×1/×2/×3), og finalen kaldes en shot.
RULES_FEST = [
{"icon": "🤥", "title": "Lyv som en influencer", "body": "Et sandt faktum om dansk pop mangler et ord. Skriv en løgn, der lyder sand.",
"say": "Yo, bestie, velkommen til festen. I får noget ægte dansk pop-sladder, men der mangler et ord. "
{"icon": "🤥", "title": "Lyv som en influencer", "body": "Et sandt faktum om slang, memes eller dansk pop mangler et ord. Skriv en løgn, der lyder sand.",
"say": "Yo, bestie, velkommen til festen. I får ægte facts om slang, memes og dansk pop, men der mangler et ord. "
"Jeres job? At lyve så hårdt, at de andre hopper lige i det. Ingen cap."},
{"icon": "🥃", "title": "Narret? Drik!", "body": "Falder du for en løgn, tager du en tår. Point er sekundært.",
"say": "Og nu kommer det vigtige. Falder du for en løgn, så tager du en tår. Det er hele pointen. Point er lowkey ligegyldige."},
{"icon": "🍻", "title": "Ingen fandt sandheden?", "body": "Så drikker alle. Sådan er reglerne.",
"say": "Finder ingen af jer sandheden, så drikker alle sammen. Det er bare sådan, det er."},
{"icon": "🔥", "title": "Det eskalerer", "body": "Runde 2 er to tårer. Finalen er en shot.",
"say": "Runde to er to tårer, og finalen er en hel shot. Drik med omtanke, og drik vand imellem. Lad os gå!"},
{"icon": "🔥", "title": "Det eskalerer", "body": "Runde 2 er to tår. Finalen er en shot.",
"say": "Runde to er to tår, og finalen er en hel shot. Drik med omtanke, og drik vand imellem. Lad os gå!"},
]
@@ -344,6 +345,10 @@ class Game:
return
self._begin_question(now)
@property
def reaction_set(self) -> tuple[str, ...]:
return REACTIONS_FEST if self.fest else REACTIONS
@property
def rules(self) -> list[dict]:
return RULES_FEST if self.fest else RULES
@@ -607,7 +612,7 @@ class Game:
def react(self, member_id: str, emoji: str, now: float) -> dict | None:
"""En emoji fra en telefon. Returnerer reaktionen (til storskærmen) eller None, hvis den droppes."""
if self.phase not in ("present", "guess", "reveal") or emoji not in REACTIONS:
if self.phase not in ("present", "guess", "reveal") or emoji not in self.reaction_set:
return None
member = self._member(member_id)
if not member:
@@ -876,6 +881,7 @@ class Game:
],
"audience_count": len(self.audience),
"fest": self.fest,
"reactions": list(self.reaction_set),
}
if self.phase not in ("lobby", "final"):
name, mult, count, _read = self.round
+3 -3
View File
@@ -482,7 +482,7 @@
"[sighs] {vip}, festen venter på dig. Start nu, bestie."
],
"game_start_fest": [
"{antal_spillere} spillere, en masse sladder og alt for mange tår. Lad os gå!",
"{antal_spillere} spillere, en masse brainrot og alt for mange tår. Lad os gå!",
"[excited] {antal_spillere} spillere er klar. Det her bliver ikke familievenligt. Okay, lidt alligevel."
],
"round1_intro_fest": [
@@ -508,7 +508,7 @@
"lie_start_fest": [
"{kategori}. Skriv en løgn så god, at Se og Hør ville trykke den.",
"Nu er det {kategori}. Lyv som en influencer med en rabatkode.",
"{kategori}, bestie. Find på noget, der lyder som ægte sladder."
"{kategori}, bestie. Find på noget, der lyder ægte, no cap."
],
"lie_hurry_fest": [
"Tiden løber, besties. Skriv noget, hvad som helst!",
@@ -587,7 +587,7 @@
"Igen igen! Fyld op, besties."
],
"present_intro_fest": [
"Okay, her er svarene. Lyt godt efter, én af dem er ægte sladder.",
"Okay, her er svarene. Lyt godt efter, én af dem er den ægte vare.",
"Jeg læser dem op. Den der lyder mest unhinged er måske sandheden."
],
"summary_cleanup_fest": [
+10
View File
@@ -361,3 +361,13 @@ class FestTests(GameTestCase):
normal = Game("N", bank())
self.assertIs(normal.rules, engine.RULES)
self.assertFalse(normal.snapshot(self.now)["fest"])
def test_fest_has_its_own_emojis(self):
for p, lie in [(self.a, "katte"), (self.b, "hunde"), (self.c, "heste")]:
self.g.submit_lie(p.id, lie, self.now)
self.assertEqual(self.g.phase, "guess")
self.assertEqual(self.g.snapshot(self.now)["reactions"], list(engine.REACTIONS_FEST))
self.assertIsNotNone(self.g.react(self.a.id, "💀", self.now))
self.assertIsNone(self.g.react(self.a.id, "😂", self.now + 2)) # familie-sættet gælder ikke her
normal = Game("N", bank())
self.assertEqual(normal.snapshot(self.now)["reactions"], list(engine.REACTIONS))
+1 -1
View File
@@ -37,7 +37,7 @@ Regler for alt, du skriver:
- 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.
SYSTEM_FEST = """Du er speakeren i "Fup og Fakta: Fest-Z", et dansk zoomer-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.
+3 -1
View File
@@ -341,7 +341,9 @@
try { return String(s || '').normalize('NFKC').replace(re, ''); } catch (e) { return String(s || ''); }
};
/* emojis, telefonerne kan spamme under oplæsning, gæt og afsløring (samme liste som serveren) */
WPP.REACTIONS = ['😂', '😱', '👏', '🤯', '🙈', '🔥'];
WPP.REACTIONS_BASE = WPP.REACTIONS = ['😂', '😱', '👏', '🤯', '🙈', '🔥'];
/* fest-spil har sit eget sæt; serveren sender listen i tilstanden */
WPP.useReactions = function (state) { WPP.REACTIONS = (state && state.reactions) || WPP.REACTIONS_BASE; };
WPP.reducedMotion = function () {
try { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; } catch (e) { return false; }
};
+1
View File
@@ -271,6 +271,7 @@
var sips = { p1: 1, p2: 3, p3: 6, p4: 4, p5: 2, p6: 5 };
[host].concat(phones.map(function (ph) { return ph.state; })).forEach(function (st) {
st.fest = true;
st.reactions = ['💀', '😭', '🧢', '🗿', '💅', '🍻'];
(st.players || []).forEach(function (p) { p.sips = sips[p.id] || 0; });
if (st.reveal) {
st.reveal.steps.forEach(function (s) {
+1
View File
@@ -784,6 +784,7 @@
/* ---------- offentlig render ---------- */
function renderHost(root, state, ctx) {
WPP.useReactions(state);
ctx = ctx || {};
state = state || {};
var u = ui(root);
+1
View File
@@ -373,6 +373,7 @@
/* ---------- offentlige render-funktioner ---------- */
function renderPlay(root, state, ctx) {
WPP.useReactions(state);
ctx = ctx || {};
root._ctx = ctx;
bind(root);
+2 -2
View File
@@ -14,8 +14,8 @@
<summary>Flere valg…</summary>
<label><input type="radio" name="pack" value="arbejde"><span>{{ work_pack_label }}</span></label>
<p class="pack-hint">Arbejdsspørgsmål: e-faktura, IT-sikkerhed og kategorier med pakken "arbejde".</p>
<label><input type="radio" name="pack" value="fest"><span>Fest 🥃 18+</span></label>
<p class="pack-hint">Dansk pop-sladder med zoomer-speaker. Narret = tag en tår, finalen er en shot.</p>
<label><input type="radio" name="pack" value="fest"><span>Fest-Z 🥃 18+</span></label>
<p class="pack-hint">Slang, memes og dansk pop med zoomer-speaker. Narret = tag en tår, finalen er en shot.</p>
</details>
</fieldset>
<button type="submit" class="btn btn-go">Start et nyt spil</button>