Temaer: fælles temabank i party/data/themes.json

Tegn og Gæts temaer flyttes til ét sted (party/data/themes.json med
loaderen party/themes.py), så Spionen kan bruge de samme i stedet for en
kopi. Navne og ord er uændrede, så Tegn og Gæt og dets forvarmede
{tema}-replikker opfører sig som før.

Spionen skal bruge ord, det er nemt at give stikord til: spy_themes() giver
de temaer, der har mindst 15 konkrete navneord i ét ord. "spion": false
udelader et tema (film og serier: titler), og "spion_skip" enkelte ord (si,
and, frikvarter, lektier, hygge). Det giver 13 temaer med 15 til 18 ord.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
agw
2026-10-20 21:59:49 +02:00
co-authored by Claude Opus 5.5
parent 477e10557a
commit 2ceb5290f8
11 changed files with 421 additions and 61 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ Serveren er autoritativ. Hver skærm har én WebSocket, sender handlinger og få
| Mappe | Indhold |
| --- | --- |
| `fupogfakta/` | Spilmotor, modeller for kategorier og spørgsmål, spørgsmålsbank i `data/*.json` |
| `tegnoggaet/` | Spilmotor for Tegn og Gæt, temaer i `data/themes.json`, speakerens replikker |
| `tegnoggaet/` | Spilmotor for Tegn og Gæt og speakerens replikker (temaerne er fælles: `party/data/themes.json`) |
| `party/` | Rum, WebSocket-consumers, sider, Authentik-header-login, templates og JS/CSS; spilregistret i `games.py` |
| `partyhub/` | Django-projekt (settings, ASGI) |
| `deploy/` | systemd-unit, miljøfil-skabelon og `deploy.sh` |
+1 -1
View File
@@ -312,5 +312,5 @@ styrer selv tempoet ud fra `started_at` og `plan[].ms`.
Point: +100 pr. hjerte til den, der lavede bidraget, og +500 til ejeren af en kæde, der overlevede. Priser:
"Bedste tegning" og "Bedste gæt" (flest hjerter, ved lighed det første i afsløringen), "Kæden der overlevede" og
"Største forvandling" (mindst lighed mellem første ord og sidste gæt). Temaerne står i `tegnoggaet/data/themes.json`
"Største forvandling" (mindst lighed mellem første ord og sidste gæt). Temaerne står i `party/data/themes.json`
og speakerens replikker i `tegnoggaet/narrator/lines.json`.
+355
View File
@@ -0,0 +1,355 @@
[
{
"id": "dyr",
"name": "Dyr",
"icon": "🐘",
"words": [
"kat",
"hund",
"elefant",
"giraf",
"pingvin",
"krokodille",
"blæksprutte",
"flodhest",
"pindsvin",
"edderkop",
"sommerfugl",
"kænguru",
"hval",
"ugle",
"snegl",
"zebra",
"papegøje",
"isbjørn"
]
},
{
"id": "film",
"name": "Film og serier",
"icon": "🎬",
"words": [
"løvernes konge",
"frost",
"star wars",
"harry potter",
"titanic",
"jurassic park",
"find nemo",
"shrek",
"toy story",
"ringenes herre",
"olsen banden",
"spiderman",
"batman",
"pippi langstrømpe",
"minions",
"mary poppins",
"ghostbusters"
],
"spion": false
},
{
"id": "koekken",
"name": "Ting i køkkenet",
"icon": "🍳",
"words": [
"gaffel",
"pande",
"køleskab",
"brødrister",
"piskeris",
"kaffemaskine",
"grydeske",
"ovn",
"mikroovn",
"kagerulle",
"si",
"osteskærer",
"opvaskemaskine",
"tekande",
"skærebræt",
"kasserolle",
"køkkenrulle"
],
"spion_skip": [
"si"
]
},
{
"id": "jul",
"name": "Jul",
"icon": "🎄",
"words": [
"juletræ",
"julemand",
"nisse",
"rensdyr",
"snemand",
"gave",
"risalamande",
"pebernødder",
"stjerne",
"kravlenisse",
"slæde",
"julehjerte",
"lys",
"and",
"juletræsfod",
"mandelgave",
"kalenderlys",
"sne"
],
"spion_skip": [
"and"
]
},
{
"id": "sport",
"name": "Sport",
"icon": "⚽",
"words": [
"fodbold",
"håndbold",
"cykelløb",
"svømning",
"tennis",
"golf",
"skihop",
"boksning",
"badminton",
"basketball",
"bueskydning",
"surfing",
"ishockey",
"kunstskøjteløb",
"maraton",
"vægtløftning",
"ridning"
]
},
{
"id": "mad",
"name": "Mad",
"icon": "🍕",
"words": [
"pizza",
"pandekager",
"spaghetti",
"hotdog",
"frikadeller",
"sushi",
"burger",
"is",
"banan",
"vandmelon",
"æbleskiver",
"smørrebrød",
"popcorn",
"rugbrød",
"flæskesteg",
"kanelsnegl",
"tacos",
"lagkage"
]
},
{
"id": "strand",
"name": "På stranden",
"icon": "🏖️",
"words": [
"sandslot",
"parasol",
"badebold",
"krabbe",
"solcreme",
"fyrtårn",
"bølge",
"måge",
"badehåndklæde",
"vandmand",
"luftmadras",
"solbriller",
"spand og skovl",
"muslingeskal",
"isbod",
"badebukser",
"redningsvest"
]
},
{
"id": "job",
"name": "Job",
"icon": "👩‍🚒",
"words": [
"brandmand",
"læge",
"politibetjent",
"kok",
"pilot",
"frisør",
"tandlæge",
"landmand",
"maler",
"astronaut",
"postbud",
"skuespiller",
"lærer",
"dyrlæge",
"tømrer",
"dommer",
"klovn"
]
},
{
"id": "skole",
"name": "I skolen",
"icon": "🏫",
"words": [
"tavle",
"lommeregner",
"skoletaske",
"lineal",
"blyant",
"frikvarter",
"madpakke",
"globus",
"lektier",
"viskelæder",
"gymnastiksal",
"skolebus",
"mikroskop",
"saks",
"atlas",
"klokke",
"penalhus"
],
"spion_skip": [
"frikvarter",
"lektier"
]
},
{
"id": "eventyr",
"name": "Eventyr",
"icon": "🏰",
"words": [
"drage",
"prinsesse",
"slot",
"trold",
"heks",
"enhjørning",
"havfrue",
"trylledrik",
"ridder",
"frø",
"glasko",
"den grimme ælling",
"tinsoldat",
"kejserens nye klæder",
"fe",
"skattekiste",
"tryllestav",
"ulv"
]
},
{
"id": "transport",
"name": "Transport",
"icon": "🚂",
"words": [
"tog",
"cykel",
"helikopter",
"ubåd",
"traktor",
"rumraket",
"sejlbåd",
"brandbil",
"løbehjul",
"luftballon",
"skraldebil",
"færge",
"motorcykel",
"flyvemaskine",
"rulleskøjter",
"taxa",
"kran"
]
},
{
"id": "musik",
"name": "Musik",
"icon": "🎸",
"words": [
"guitar",
"trommer",
"klaver",
"trompet",
"violin",
"mikrofon",
"harmonika",
"dirigent",
"kor",
"koncert",
"høretelefoner",
"saxofon",
"blokfløjte",
"rockstjerne",
"luftguitar",
"kontrabas",
"tamburin"
]
},
{
"id": "danmark",
"name": "Danmark",
"icon": "🇩🇰",
"words": [
"den lille havfrue",
"dannebrog",
"vikingeskib",
"lego",
"rundetårn",
"tivoli",
"cykelsti",
"pølsevogn",
"kronborg",
"dronning",
"hygge",
"klitter",
"vindmølle",
"kongekrone",
"fastelavn",
"sankthans",
"kolonihave"
],
"spion_skip": [
"hygge"
]
},
{
"id": "rummet",
"name": "Rummet",
"icon": "🚀",
"words": [
"astronaut",
"planet",
"måne",
"rumvæsen",
"stjerneskud",
"satellit",
"sort hul",
"saturn",
"teleskop",
"rumstation",
"ufo",
"komet",
"månelanding",
"solsystem",
"mars",
"rumdragt"
]
}
]
+1 -1
View File
@@ -16,7 +16,7 @@ from pathlib import Path
from fupogfakta.engine import GameError
from tegnoggaet.engine import EMPTY_GUESS, MAX_PLAYERS, MIN_PLAYERS, RULES, RULES_STEP_MS, Game
from tegnoggaet.themes import load_themes
from party.themes import load_themes
from . import rooms, tts
from .narrator import Narrator, generic_texts, one_of, untag
+27
View File
@@ -0,0 +1,27 @@
"""Den fælles temabank (party/data/themes.json): Tegn og Gæt som før, og nok gode ord til Spionen."""
from django.test import SimpleTestCase
from party.themes import SPY_MIN_WORDS, load_themes, spy_themes, spy_words
class ThemeBankTests(SimpleTestCase):
def test_tegn_og_gaet_gets_the_same_themes(self):
themes = load_themes()
self.assertEqual(len(themes), 14)
self.assertEqual(len({t["id"] for t in themes}), 14)
for t in themes:
self.assertTrue(t["name"] and t["icon"] and len(t["words"]) >= 16, t["id"])
def test_spionen_has_enough_concrete_words(self):
themes = spy_themes()
self.assertGreaterEqual(len(themes), 10)
for t in themes:
self.assertGreaterEqual(len(t["words"]), SPY_MIN_WORDS, t["id"])
self.assertTrue(all(" " not in w for w in t["words"]), t["id"])
self.assertNotIn("film", {t["id"] for t in themes}) # titler er dårlige at give stikord til
def test_skip_list(self):
self.assertNotIn("si", spy_words({"words": ["si", "ovn"], "spion_skip": ["si"]}))
self.assertEqual(spy_words({"words": ["sort hul", "måne"]}), ["måne"])
self.assertEqual(spy_words({"words": ["x"], "spion": False}), [])
+1 -1
View File
@@ -17,7 +17,7 @@ from party.narrator import Narrator, instead_of, one_of
from party.rooms import Room
from party.tegn import TegnRoom
from tegnoggaet.engine import Game as TegnGame
from tegnoggaet.themes import load_themes
from party.themes import load_themes
QUESTIONS = [{"id": i, "category_id": i % 3, "category": f"Kat {i % 3}", "prompt": f"Hvad _____ {i}?", "answer": f"svar{i}",
"decoys": ["x", "y", "z"]} for i in range(9)]
+1 -1
View File
@@ -17,7 +17,7 @@ from tegnoggaet.engine import RULES as TEGN_RULES
from party.narrator import LINES as FOF_LINES, TEASES, TEASES_FEST, Narrator
from party.tegn import COUNT_LINES, FIXED_LINES, LINES, THEME_LINES, TegnRoom, warm_texts
from tegnoggaet.engine import Game
from tegnoggaet.themes import load_themes
from party.themes import load_themes
STROKE = {"color": "#1d1b2e", "width": 0.01, "points": [[0.1, 0.1], [0.5, 0.5]]}
NAMES = ("Qxa", "Zyb", "Wvc") # spillernavne og -ord, der ikke findes i replikkerne
+33
View File
@@ -0,0 +1,33 @@
"""Temabanken, fælles for spillene: party/data/themes.json.
Tegn og Gæt bruger alle ordene (eksempel-ordene til "Giv mig et forslag"). Spionen skal bruge ord, det er nemt at give
stikord til: konkrete navneord i ét ord. Et tema med "spion": false er ikke med i Spionen, og "spion_skip" udelader
enkelte ord.
"""
import json
from pathlib import Path
THEMES_FILE = Path(__file__).resolve().parent / "data" / "themes.json"
SPY_MIN_WORDS = 15 # et tema skal have mindst så mange gode ord for at være med i Spionen
def load_themes() -> list[dict]:
return json.loads(THEMES_FILE.read_text(encoding="utf-8"))
def spy_words(theme: dict) -> list[str]:
if theme.get("spion") is False:
return []
skip = set(theme.get("spion_skip", []))
return [w for w in theme.get("words", []) if " " not in w and w not in skip]
def spy_themes() -> list[dict]:
"""Temaerne til Spionen, med words = de gode stikordsord."""
out = []
for t in load_themes():
words = spy_words(t)
if len(words) >= SPY_MIN_WORDS:
out.append({"id": t["id"], "name": t["name"], "icon": t.get("icon", ""), "words": words})
return out
-45
View File
@@ -1,45 +0,0 @@
[
{"id": "dyr", "name": "Dyr", "icon": "🐘", "words": [
"kat", "hund", "elefant", "giraf", "pingvin", "krokodille", "blæksprutte", "flodhest", "pindsvin", "edderkop",
"sommerfugl", "kænguru", "hval", "ugle", "snegl", "zebra", "papegøje", "isbjørn"]},
{"id": "film", "name": "Film og serier", "icon": "🎬", "words": [
"løvernes konge", "frost", "star wars", "harry potter", "titanic", "jurassic park", "find nemo", "shrek",
"toy story", "ringenes herre", "olsen banden", "spiderman", "batman", "pippi langstrømpe", "minions",
"mary poppins", "ghostbusters"]},
{"id": "koekken", "name": "Ting i køkkenet", "icon": "🍳", "words": [
"gaffel", "pande", "køleskab", "brødrister", "piskeris", "kaffemaskine", "grydeske", "ovn", "mikroovn",
"kagerulle", "si", "osteskærer", "opvaskemaskine", "tekande", "skærebræt", "kasserolle", "køkkenrulle"]},
{"id": "jul", "name": "Jul", "icon": "🎄", "words": [
"juletræ", "julemand", "nisse", "rensdyr", "snemand", "gave", "risalamande", "pebernødder", "stjerne",
"kravlenisse", "slæde", "julehjerte", "lys", "and", "juletræsfod", "mandelgave", "kalenderlys", "sne"]},
{"id": "sport", "name": "Sport", "icon": "⚽", "words": [
"fodbold", "håndbold", "cykelløb", "svømning", "tennis", "golf", "skihop", "boksning", "badminton",
"basketball", "bueskydning", "surfing", "ishockey", "kunstskøjteløb", "maraton", "vægtløftning", "ridning"]},
{"id": "mad", "name": "Mad", "icon": "🍕", "words": [
"pizza", "pandekager", "spaghetti", "hotdog", "frikadeller", "sushi", "burger", "is", "banan", "vandmelon",
"æbleskiver", "smørrebrød", "popcorn", "rugbrød", "flæskesteg", "kanelsnegl", "tacos", "lagkage"]},
{"id": "strand", "name": "På stranden", "icon": "🏖️", "words": [
"sandslot", "parasol", "badebold", "krabbe", "solcreme", "fyrtårn", "bølge", "måge", "badehåndklæde",
"vandmand", "luftmadras", "solbriller", "spand og skovl", "muslingeskal", "isbod", "badebukser", "redningsvest"]},
{"id": "job", "name": "Job", "icon": "👩‍🚒", "words": [
"brandmand", "læge", "politibetjent", "kok", "pilot", "frisør", "tandlæge", "landmand", "maler", "astronaut",
"postbud", "skuespiller", "lærer", "dyrlæge", "tømrer", "dommer", "klovn"]},
{"id": "skole", "name": "I skolen", "icon": "🏫", "words": [
"tavle", "lommeregner", "skoletaske", "lineal", "blyant", "frikvarter", "madpakke", "globus", "lektier",
"viskelæder", "gymnastiksal", "skolebus", "mikroskop", "saks", "atlas", "klokke", "penalhus"]},
{"id": "eventyr", "name": "Eventyr", "icon": "🏰", "words": [
"drage", "prinsesse", "slot", "trold", "heks", "enhjørning", "havfrue", "trylledrik", "ridder", "frø",
"glasko", "den grimme ælling", "tinsoldat", "kejserens nye klæder", "fe", "skattekiste", "tryllestav", "ulv"]},
{"id": "transport", "name": "Transport", "icon": "🚂", "words": [
"tog", "cykel", "helikopter", "ubåd", "traktor", "rumraket", "sejlbåd", "brandbil", "løbehjul", "luftballon",
"skraldebil", "færge", "motorcykel", "flyvemaskine", "rulleskøjter", "taxa", "kran"]},
{"id": "musik", "name": "Musik", "icon": "🎸", "words": [
"guitar", "trommer", "klaver", "trompet", "violin", "mikrofon", "harmonika", "dirigent", "kor", "koncert",
"høretelefoner", "saxofon", "blokfløjte", "rockstjerne", "luftguitar", "kontrabas", "tamburin"]},
{"id": "danmark", "name": "Danmark", "icon": "🇩🇰", "words": [
"den lille havfrue", "dannebrog", "vikingeskib", "lego", "rundetårn", "tivoli", "cykelsti", "pølsevogn",
"kronborg", "dronning", "hygge", "klitter", "vindmølle", "kongekrone", "fastelavn", "sankthans", "kolonihave"]},
{"id": "rummet", "name": "Rummet", "icon": "🚀", "words": [
"astronaut", "planet", "måne", "rumvæsen", "stjerneskud", "satellit", "sort hul", "saturn", "teleskop",
"rumstation", "ufo", "komet", "månelanding", "solsystem", "mars", "rumdragt"]}
]
+1 -1
View File
@@ -5,7 +5,7 @@ from django.test import SimpleTestCase
from fupogfakta.engine import GameError
from tegnoggaet import engine
from tegnoggaet.engine import Game, clean_drawing
from tegnoggaet.themes import load_themes
from party.themes import load_themes
THEMES = [{"id": "dyr", "name": "Dyr", "words": ["kat", "hund", "ko", "gris"]},
{"id": "mad", "name": "Mad", "words": ["pizza", "is"]}]
-10
View File
@@ -1,10 +0,0 @@
"""Temaerne til Tegn og Gæt ligger i data/themes.json (eksempel-ordene bruges til "Giv mig et forslag")."""
import json
from pathlib import Path
THEMES_FILE = Path(__file__).resolve().parent / "data" / "themes.json"
def load_themes() -> list[dict]:
return json.loads(THEMES_FILE.read_text(encoding="utf-8"))