diff --git a/fupogfakta/narrator/generic.json b/fupogfakta/narrator/generic.json new file mode 100644 index 0000000..17c2f86 --- /dev/null +++ b/fupogfakta/narrator/generic.json @@ -0,0 +1,238 @@ +{ + "lobby_waiting": [ + "Vi er klar. VIP'en må gerne trykke start nu.", + "Alle sidder klar. Den med VIP-kronen bestemmer, hvornår vi går i gang.", + "Så er vi nok. VIP, din telefon har en meget vigtig knap.", + "[sighs] Jeg venter stadig på startknappen. Bare så I ved det.", + "Holdet er samlet. Nu mangler vi bare, at VIP'en tager mod til sig." + ], + "lobby_waiting_fest": [ + "Vi er klar, VIP. Tryk start, bestie.", + "Alle venter på VIP'en. Lowkey stressende.", + "VIP, startknappen. Glassene bliver ikke koldere." + ], + "round2_intro": [ + "Runde to. Nu giver alt dobbelt point, så føringen er alt andet end sikker.", + "Velkommen til runde to. Pointene fordobles, og det gør jeres mistænksomhed forhåbentlig også.", + "[excited] Runde to! Dobbelt point. Nu kan det hele vende.", + "Så skruer vi op. Runde to giver dobbelt point.", + "Runde to begynder. Dem, der ligger bagerst, har stadig alle chancer." + ], + "round2_intro_fest": [ + "Runde to, bestie. Narret nu koster to tår.", + "[excited] Runde to! Nu bliver nogen cooked.", + "Dobbelt point og dobbelt tår. No cap, alt kan ske." + ], + "final_intro": [ + "Så er det finalen. Her giver alt tredobbelt point, så intet er afgjort.", + "[dramatic] Finalen. Et enkelt spørgsmål, tredobbelt point. Alt kan vende nu.", + "Velkommen til finalen. Den, der fører, skal passe godt på nu.", + "Finalen er her. Dem bagerst får deres sidste og bedste chance.", + "Sidste spørgsmål, tredobbelt point. Tag en dyb indånding." + ], + "final_intro_fest": [ + "[dramatic] Finalen, bestie. Bliver du narret nu, er det en hel shot.", + "Det er shot-runden. No cap, nu bliver det seriøst.", + "Finalen er her. Tredobbelt point og en shot til dem, der hopper i." + ], + "scores_intro": [ + "Her er stillingen. Nogen smiler, nogen tæller på fingrene.", + "Pointtavlen er klar. Kig selv. Jeg dømmer ikke. Højt.", + "Sådan står det. Føringen er spinkel, og bunden er hyggelig.", + "Lad os se stillingen. Tallene lyver ikke. Det er jeres opgave.", + "[chuckles] Stillingen er på skærmen. Nogle af jer bør se væk.", + "Her er pointene. Der er stadig tid til at vende det hele." + ], + "scores_intro_fest": [ + "Stillingen er her. Men hvem fører på tår? Det er det, der tæller.", + "Pointtavlen, bestie. Nogen er lowkey cooked.", + "Her står det. Den, der ligger sidst, har nok drukket mest." + ], + "category_choose": [ + "Så skal der vælges kategori. Telefonen ved, hvis tur det er.", + "Tid til at vælge en kategori. Gerne noget, I kan lyve selvsikkert om.", + "En af jer vælger nu kategorien. Resten må gerne brokke sig bagefter.", + "Kategorien vælges på telefonen. Mavefornemmelsen er fuldt kvalificeret.", + "Vælg en kategori. Der er ingen forkerte valg. Kun pinlige.", + "[curious] Hvad skal vi høre om? Det afgøres på en telefon lige nu." + ], + "category_choose_fest": [ + "Vælg en kategori, bestie. No pressure.", + "Kategori, tak. Tænk ikke for længe, glasset venter.", + "Tid til at vælge. It's giving beslutningsangst." + ], + "category_timeout": [ + "Tiden løb ud, så jeg valgte selv. Jeg har en god smag.", + "For langsomt. Jeg har valgt kategorien for jer.", + "[sighs] Ingen valgte, så jeg tog en. Den står på skærmen.", + "Uret vandt. Kategorien er valgt, og vi kører videre.", + "Ingen beslutning? Så beslutter jeg. Det er det, jeg er her for." + ], + "category_timeout_fest": [ + "For langsomt, bestie. Jeg valgte.", + "Tiden er gået. Jeg tog en kategori, no cap.", + "[sighs] Beslutningsangst? Jeg har valgt for dig." + ], + "lie_start": [ + "Kategorien er valgt. Her kommer spørgsmålet.", + "Så er vi klar. Lyt godt efter, nu kommer spørgsmålet.", + "Godt valg. Lad os se, hvad I kan finde på.", + "Kategorien står på skærmen. Find jeres bedste løgn frem.", + "[mischievously] Et glimrende valg. Eller også ikke. Det finder vi ud af." + ], + "lie_start_fest": [ + "Kategorien er valgt. Let's go, bestie.", + "Godt valg. Lowkey spændende.", + "Her kommer spørgsmålet. Lock in." + ], + "reveal_lie_fooled": [ + "Den løgn virkede. Nogen hoppede lige i.", + "[chuckles] Narret! Skærmen viser, hvem der blev snydt.", + "Det var en løgn, og den gik rent ind.", + "Snydt! Forfatteren må gerne smile lidt nu.", + "Den var opdigtet. Og den blev købt. Kvitteringen er væk.", + "[laughs] Lige i fælden. Pænt skrevet.", + "Det var ikke sandheden. Men det lød sådan, og det er hele kunsten.", + "Løgn! Og nogen troede på den. Sådan skal det gøres." + ], + "reveal_lie_fooled_fest": [ + "Løgn! Og nogen hoppede i. Tag en tår, bestie.", + "[laughs] Narret! Glasset op, det er reglerne.", + "Den var fake, og den blev købt. Drik!", + "Caught red-handed. Skål for løgneren.", + "Snydt! Lowkey pinligt. Drik." + ], + "reveal_lie_fooled_many": "reveal_lie_fooled", + "reveal_lie_shared": "reveal_lie_fooled", + "reveal_house_lie": [ + "Det var husets løgn. Den skrev jeg selv. Jeg er meget stolt.", + "[mischievously] Husets løgn! Jeg har stadig evnerne.", + "Den løgn kom fra huset. Altså fra mig.", + "Ha! Det var min løgn. Tak for tilliden.", + "Husets løgn snød nogen. Det varmer et gammelt speakerhjerte.", + "Den var min. Jeg håber, I tilgiver mig. Måske." + ], + "reveal_house_lie_fest": [ + "Husets løgn, bestie. Jeg fik dig. Drik!", + "[laughs] Den var min. Tag en tår.", + "Huset vinder. Skål." + ], + "reveal_truth_found": [ + "Det er sandheden! Godt fundet.", + "[impressed] Sandheden. Nogen af jer kan altså tænke.", + "Det var den rigtige. Point til dem, der fandt den.", + "Sandheden er fundet. Det er rart med lidt orden i tingene.", + "Rigtigt! Faktum står fast, og pointene er givet.", + "Den var sand. Og I fandt den. Jeg er næsten rørt." + ], + "reveal_truth_found_fest": [ + "Sandheden! Big brain, bestie.", + "Rigtigt svar. W til dem, der fandt den.", + "[impressed] Det var sandheden. Slay." + ], + "reveal_truth_nobody": [ + "Det var sandheden, og ingen fandt den. Løgnene var for gode.", + "[sighs] Sandheden stod lige der, og ingen ville have den.", + "Det var faktisk sandt. Ingen troede på det. Det siger lidt om sandheden.", + "Ingen fandt sandheden. Den sidder nu alene i et hjørne.", + "Sandt, men ingen gættede det. Virkeligheden er underligere end jeres løgne." + ], + "reveal_truth_nobody_fest": [ + "Sandheden, og ingen fandt den. Så drikker alle. Det er reglerne.", + "[sighs] Ingen fandt den. Alle drikker, bestie.", + "Det var sandt. Hele squad'en er cooked. Skål." + ], + "reveal_truth_everyone": [ + "Alle fandt sandheden! Løgnerne må prøve igen.", + "[impressed] Hele holdet fandt den. Der var ikke meget at hente for løgnerne.", + "Alle som én. Sandheden var åbenbart for nem.", + "Fuldt hus på sandheden. Jeg er imponeret og lidt skuffet.", + "Alle fandt den. Det var vist ikke et godt spørgsmål for løgnere." + ], + "reveal_truth_everyone_fest": [ + "Alle fandt sandheden. Big brain hele squad'en.", + "Alle som én! Ingen tår der. Kedeligt, bestie.", + "[impressed] Fuldt hus. Løgnerne er cooked." + ], + "summary_favorite": [ + "Publikums favorit står på skærmen. Hjerterne har talt.", + "Flest hjerter gik til et svar, der fortjente det.", + "[chuckles] Hjerterne er delt ud. Se lige, hvem der fik flest.", + "Det svar fik flest hjerter. Kunst, siger jeg bare.", + "Hjerterne er talt. Favoritten står på skærmen." + ], + "summary_favorite_fest": [ + "Flest hjerter, bestie. Det svar slayede.", + "Favoritten er fundet. It's giving ikonisk.", + "Hjerterne har talt. Lowkey fortjent." + ], + "winner": [ + "Vi har en vinder! Navnet står på skærmen, og æren er fortjent.", + "[cheering] Tillykke til vinderen! Det skal sofaen nok høre om igen.", + "Sejren er afgjort. Takketalen må gerne indeholde noget sandt.", + "Og vinderen er fundet. Pointene taler for sig selv.", + "Tillykke! Vinderen må nu se ned på os andre. Med måde.", + "Spillet er slut, og vinderen står øverst. Pokalen er usynlig, men ægte." + ], + "winner_fest": [ + "Vi har en vinder! Men den rigtige vinder er den, der drak mest.", + "[cheering] Tillykke, bestie. Skål for sejren.", + "Vinderen er fundet. Main character energy." + ], + "award_biggest_liar": [ + "Prisen for største løgner. Tjek lige jeres lommer.", + "Største løgner! Den her person narrede jer igen og igen.", + "[impressed] Aftenens største løgner. Fantasien har fået faste kunder.", + "Prisen for flest narrede går til en, I aldrig skal stole på igen.", + "Største løgner. Prisen overrækkes med et mistænksomt håndtryk." + ], + "award_biggest_liar_fest": [ + "Største løgner, bestie. Red flag, men respekt.", + "[laughs] Prisen for flest narrede. It's giving manipulation.", + "Største løgner. No cap, det var imponerende." + ], + "award_truth_seeker": [ + "Sandhedsvidnet! Den her person fandt sandheden igen og igen.", + "Prisen for at finde sandheden. Nogen skal jo holde styr på virkeligheden.", + "[impressed] Aftenens sandhedsvidne. Ingen løgn slap forbi.", + "Prisen for flest rigtige går til en skarp hjerne.", + "Sandhedsvidnet. Det er næsten mistænkeligt, hvor godt det gik." + ], + "award_truth_seeker_fest": [ + "Sandhedsvidnet, bestie. Big brain.", + "Flest rigtige. Lowkey et geni.", + "[impressed] Ingen løgn slap forbi. Slay." + ], + "award_most_liked": [ + "Folkets favorit! Flest hjerter i aften.", + "Prisen for flest hjerter. Publikum har talt.", + "[cheering] Folkets favorit. Hjerterne regnede ned.", + "Flest hjerter gik til den her person. Charme virker åbenbart.", + "Folkets favorit. Det er ikke point, men det er kærlighed." + ], + "award_most_liked_fest": [ + "Folkets favorit, bestie. Mest liked. Slay.", + "Flest hjerter. Main character energy.", + "[cheering] Folkets favorit. It's giving ikon." + ], + "award_thirstiest_fest": [ + "Prisen for flest tår. Vand nu, bestie.", + "[laughs] Aftenens tørstigste. Leveren sender en hilsen.", + "Flest tår i aften. No cap, respekt og bekymring." + ], + "lie_progress": [ + "En løgn er afleveret. Den ligger nu i bunken.", + "Der er en, der er færdig. Resten, kom så.", + "[mischievously] Nogen har skrevet noget. Jeg siger ikke hvad.", + "Der kom en løgn. Jeg tager den ikke personligt.", + "En er klar. Der er stadig tid til jer andre.", + "Endnu en løgn er landet. Den er stadig lun." + ], + "lie_progress_fest": [ + "En løgn er inde. Lowkey lumsk.", + "Nogen er færdig. Aura farmet.", + "[whispers] Der kom en løgn. It's giving." + ], + "lie_progress_fast": "lie_progress", + "lie_progress_retry": "lie_progress" +} diff --git a/party/generic_lines.json b/party/generic_lines.json new file mode 100644 index 0000000..a5cd1f8 --- /dev/null +++ b/party/generic_lines.json @@ -0,0 +1,109 @@ +{ + "lobby_join": [ + "Så kom der en til. Velkommen, sæt dig bare.", + "En ny spiller! Sofaen rykker lidt sammen.", + "Velkommen til. Telefonen virker, kan jeg se. Det er en god start.", + "Der var en mere. Jo flere, jo sjovere.", + "[excited] Endnu en er landet!", + "Velkommen. Find en plads og se uskyldig ud.", + "Hej med dig. Du kom lige til tiden. Det gør næsten ingen.", + "Så er vi en mere. Det var nu hyggeligt.", + "Velkommen ombord. Redningsvesten ligger under sofapuden.", + "Der kom en til. Jeg kan allerede mærke stemningen stige.", + "[chuckles] Nå, der var du. Vi har ventet på dig. Lidt.", + "Velkommen. Navnet står på skærmen, så jeg slipper for at udtale det.", + "En mere i flokken. Dejligt.", + "Så blev vi flere. Klogere bliver vi nok ikke.", + "Velkommen til. Husk, alt hvad du gør, kan bruges imod dig.", + "Godt at se dig. Eller, godt at se dit navn.", + "Der er en ny ved bordet. Opfør jer pænt. I hvert fald i starten.", + "[mischievously] Velkommen. Jeg har store forventninger. De plejer ikke at holde.", + "En til! Nu begynder det at ligne en fest.", + "Velkommen. Hold godt fast i telefonen, det bliver vildt.", + "Så kom der forstærkning.", + "Hej og velkommen. Du ser ud til at kunne tåle lidt drilleri." + ], + "lobby_join_fest": [ + "Der kom en til. Main character energy, helt sikkert.", + "[gasps] Ny spiller! Glasset er klar, ikke?", + "Velkommen, bestie. Det her ender med en tår.", + "En mere til festen. Squad'en vokser. Slay.", + "Så er vi flere. Lowkey det bedste, der er sket i aften.", + "Velkommen. No cap, du kommer til at drikke.", + "[excited] Endnu en er joinet! It's giving fest.", + "Der var en mere. Vibe check bestået." + ], + "game_start_2": [ + "Vi er to. Det bliver en ren tvekamp.", + "Kun to i aften. Så kan ingen gemme sig.", + "Et hold på to. Hyggeligt og helt uden nåde." + ], + "game_start_3": [ + "Vi er tre. Lille hold, stor ambition.", + "Så kører vi med tre spillere. Overskueligt. Indtil videre.", + "Et hold på tre. Det er lige nok til en alliance og et forræderi." + ], + "game_start_4": [ + "Vi er fire. Det er en rigtig spilleaften.", + "Så starter vi med fire spillere. Perfekt til et sofabord.", + "Holdet er på fire. Ingen kan gemme sig bag de andre." + ], + "game_start_5": [ + "Vi er fem. Det bliver tæt.", + "Med fem spillere kan alt ske.", + "Holdet tæller fem. Jeg har allerede en favorit. Jeg siger ikke hvem." + ], + "game_start_6": [ + "Vi er seks. Nu er det en rigtig fest.", + "Med seks spillere bliver det kaos. Den gode slags.", + "Så er vi seks. Sofaen er officielt fuld." + ], + "game_start_7": [ + "Vi er syv. Et heldigt tal. For nogen af jer.", + "Med syv spillere bliver det svært at holde styr på, hvem der gjorde hvad.", + "Så er vi syv. Jeg har sat ekstra stole frem." + ], + "game_start_8": [ + "Vi er otte. Nu bliver det larmende.", + "Med otte spillere er der ingen steder at gemme sig. Eller alle steder.", + "Så kører vi med otte. Det bliver en lang aften. Heldigvis." + ], + "game_start_9": [ + "Vi er ni. Det er næsten et fodboldhold.", + "Med ni spillere bliver det vildt.", + "Så er vi ni. Jeg håber, I har ladet telefonerne op." + ], + "game_start_10": [ + "Vi er ti! Fuldt hus.", + "Med ti spillere er der fest i hele stuen.", + "Så er vi ti. Det er rekord. I hvert fald i aften." + ], + "game_start_2_fest": [ + "Vi er kun to. Det bliver en drikkeduel, bestie.", + "Kun to til festen. Lowkey intimt. Skål." + ], + "game_start_3_fest": [ + "Vi er tre. Lille squad, stor tørst.", + "Så er vi tre. No cap, nogen drikker meget i aften." + ], + "game_start_4_fest": [ + "Vi er fire. Det er fire glas, der skal tømmes.", + "Squad'en er på fire. Vibe check bestået." + ], + "game_start_5_fest": [ + "Vi er fem. Glassene er klar, undskyldningerne er ikke.", + "Med fem spillere bliver det kaos. Det gode kaos." + ], + "game_start_6_fest": [ + "Vi er seks. Nu bliver det en rigtig fest, bestie.", + "Med seks spillere er der nok vidner til alt." + ], + "game_start_7_fest": [ + "Vi er syv. Den her aften kommer på stories.", + "Så er vi syv. Lowkey for mange vidner." + ], + "game_start_8_fest": [ + "Vi er otte! Fuldt hus, fulde glas.", + "Med otte spillere bliver nogen cooked. Det er bare matematik." + ] +} diff --git a/party/narrator.py b/party/narrator.py index 637f209..502fe7a 100644 --- a/party/narrator.py +++ b/party/narrator.py @@ -5,8 +5,14 @@ import random import re from pathlib import Path -LINES = json.loads((Path(__file__).resolve().parents[1] / "fupogfakta" / "narrator" / "lines.json") - .read_text(encoding="utf-8")) +ROOT = Path(__file__).resolve().parents[1] +LINES = json.loads((ROOT / "fupogfakta" / "narrator" / "lines.json").read_text(encoding="utf-8")) +# Generiske replikker: samme begivenheder uden navne og spillertekst, så de kan forvarmes. Speakeren siger dem, +# når den rigtige replik ikke kan laves (ingen live-stemme). Nøglen er begivenheden; "game_start_" for +# antal spillere; en tekst i stedet for en liste peger på en anden begivenheds replikker. Fælles for alle spil +# (party/generic_lines.json) plus spillets egne. +GENERIC_COMMON = json.loads((ROOT / "party" / "generic_lines.json").read_text(encoding="utf-8")) +GENERIC = json.loads((ROOT / "fupogfakta" / "narrator" / "generic.json").read_text(encoding="utf-8")) ONES = ["nul", "én", "to", "tre", "fire", "fem", "seks", "syv", "otte", "ni", "ti", "elleve", "tolv", "tretten", "fjorten", "femten", "seksten", "sytten", "atten", "nitten"] @@ -90,10 +96,30 @@ TEASES_FEST = ["Bro er cooked!", "[laughs] L, tag en tår!", "Ohio-moment! Skål "Bussin' løgn, cooked gæt!", "[mischievously] Tak for tåren!", "No cap, du blev fooled!", "Main character? Nah, NPC!"] +class Line(str): + """En replik, der kender sin generiske afløser: generic(accept) giver en udgave uden navne og spillertekst + (kun blandt dem, accept godtager, fx dem der ligger i lydcachen), eller None. Rummet spørger, når replikken + selv ikke kan laves.""" + + def generic(self, accept=None) -> str | None: + return None + + +def with_generic(text: str | None, generic) -> Line | None: + """Giv en tekst (fx hjernens) en generisk afløser: generic(accept) -> str | None.""" + if text is None: + return None + line = Line(text) + line.generic = generic + return line + + class Narrator: - def __init__(self, rng: random.Random | None = None, fest: bool = False, lines: dict | None = None): + def __init__(self, rng: random.Random | None = None, fest: bool = False, lines: dict | None = None, + generic: dict | None = None): self.rng = rng or random.Random() - self.lines = LINES if lines is None else lines # et andet spil har sin egen lines.json + self.lines = LINES if lines is None else lines # et andet spil har sin egen lines.json ... + self.generic_lines = {**GENERIC_COMMON, **(GENERIC if lines is None else generic or {})} # ... og generic.json self.fest = fest # fest: replikken "_fest" bruges, når den findes self._recent: dict[str, list[int]] = {} @@ -105,22 +131,49 @@ class Narrator: filled = {k: (number_words(v) if isinstance(v, int) else safe(v)) for k, v in values.items()} return capitalize(re.sub(r"\{(\w+)\}", lambda m: filled.get(m.group(1), ""), variant)) - def line(self, event: str, **values) -> str | None: - event = self._event(event) - variants = self.lines.get(event) - if not variants: - return None - recent = self._recent.setdefault(event, []) - fresh = [i for i in range(len(variants)) if i not in recent] or list(range(len(variants))) + def _pick(self, key: str, variants: list[str], allowed: list[int] | None = None) -> int: + allowed = list(range(len(variants))) if allowed is None else allowed + recent = self._recent.setdefault(key, []) + fresh = [i for i in allowed if i not in recent] or allowed i = self.rng.choice(fresh) recent.append(i) del recent[:-max(1, len(variants) // 2)] # undgå gentagelser i den seneste halvdel - return self.fill(variants[i], values) + return i + + def line(self, event: str, **values) -> Line | None: + key = self._event(event) + variants = self.lines.get(key) + if not variants: + return None + return with_generic(self.fill(variants[self._pick(key, variants)], values), + lambda accept=None: self.generic(event, accept, **values)) def variants(self, event: str, **values) -> list[str]: """Alle udgaver af replikken, som line() ville sige dem (til forvarmning af lyden).""" return [self.fill(v, values) for v in self.lines.get(self._event(event), [])] + # ── generiske replikker ────────────────────────────────────────────────── + def _generic_key(self, event: str, values: dict) -> str | None: + if event == "game_start" and isinstance(values.get("antal_spillere"), int): + event = f"game_start_{values['antal_spillere']}" + v = self.generic_lines.get(event) + if isinstance(v, str): # henvisning til en anden begivenheds replikker + event = v + if self.fest and isinstance(self.generic_lines.get(event + "_fest"), list): + return event + "_fest" + return event if isinstance(self.generic_lines.get(event), list) else None + + def generic(self, event: str, accept=None, **values) -> str | None: + """En generisk udgave af begivenhedens replik (samme rotation som line()); accept(tekst) begrænser valget.""" + key = self._generic_key(event, values) + variants = self.generic_lines.get(key, []) if key else [] + allowed = [i for i, v in enumerate(variants) if accept is None or accept(v)] + return variants[self._pick("generic:" + key, variants, allowed)] if allowed else None + + def generic_variants(self, event: str, **values) -> list[str]: + key = self._generic_key(event, values) + return list(self.generic_lines.get(key, [])) if key else [] + # ── afsløringen ────────────────────────────────────────────────────────── def reveal_step(self, step: dict, names: dict[str, str], n_players: int, comment: str | None = None) -> str | None: picked = [names[p] for p in step["guessed_by"] if p in names] diff --git a/party/tegn.py b/party/tegn.py index 8c19bee..fda543d 100644 --- a/party/tegn.py +++ b/party/tegn.py @@ -23,8 +23,9 @@ from .narrator import Narrator, untag log = logging.getLogger(__name__) -LINES = json.loads((Path(__file__).resolve().parents[1] / "tegnoggaet" / "narrator" / "lines.json") - .read_text(encoding="utf-8")) +NARRATOR_DIR = Path(__file__).resolve().parents[1] / "tegnoggaet" / "narrator" +LINES = json.loads((NARRATOR_DIR / "lines.json").read_text(encoding="utf-8")) +GENERIC = json.loads((NARRATOR_DIR / "generic.json").read_text(encoding="utf-8")) # se party.narrator BEAT_PAD_MS = 700 # luft efter speakerens replik i et trin af afsløringen HURRY_BEFORE = 10 # "ti sekunder tilbage" # Replikker uden navne eller spillertekst, som rummet siger alene: dem kan tts_warm lave på forhånd. @@ -41,7 +42,7 @@ class TegnRoom(rooms.BaseRoom): def __init__(self, code: str, game: Game, host_username: str): super().__init__(code, game, host_username) - self.narrator = Narrator(lines=LINES) + self.narrator = Narrator(lines=LINES, generic=GENERIC) self._last_join_line = 0.0 self._lobby_waiting_said = False self._beat_timers: list[asyncio.TimerHandle] = [] diff --git a/party/tests/test_generic_lines.py b/party/tests/test_generic_lines.py new file mode 100644 index 0000000..820a561 --- /dev/null +++ b/party/tests/test_generic_lines.py @@ -0,0 +1,114 @@ +"""De generiske replikker (uden navne og spillertekst): antal, sprog og at alle navnebegivenheder har dem.""" + +import random +import re + +from django.test import SimpleTestCase + +from fupogfakta import engine as fof_engine +from party import narrator as fof +from party.narrator import Narrator +from party import tegn +from tegnoggaet import engine as tegn_engine + +NUMBERS = ("to tre fire fem seks syv otte ni ti elleve tolv tretten fjorten femten seksten sytten atten nitten " + "tyve tredive fyrre halvtreds hundrede tusind").split() +# Pladsholdere med et endeligt antal værdier: de replikker forvarmes, som de er (temaer, kategorier, antal) +FINITE = {"tema", "kategori", "antal_spillere"} +# Begivenheder, der ikke har en god generisk udgave: tavse, når de ikke er forvarmet (svaret ER replikken) +SILENT = {"present_answer"} +FEST_ONLY = {"award_thirstiest"} # tår findes kun i fest-spil + +GAMES = {"fof": (fof.LINES, {**fof.GENERIC_COMMON, **fof.GENERIC}, fof_engine.MIN_PLAYERS, fof_engine.MAX_PLAYERS), + "tegn": (tegn.LINES, {**fof.GENERIC_COMMON, **tegn.GENERIC}, tegn_engine.MIN_PLAYERS, tegn_engine.MAX_PLAYERS)} + + +def all_generic() -> list[tuple[str, str]]: + files = [fof.GENERIC_COMMON, fof.GENERIC, tegn.GENERIC] + return [(k, line) for f in files for k, v in f.items() if isinstance(v, list) for line in v] + + +def sentences(line: str) -> list[str]: + text = re.sub(r"\[[^\]]*\]\s*", "", line) + return [s for s in re.split(r"(?<=[.!?])\s+", text) if s] + + +class GenericLinesTests(SimpleTestCase): + def test_language(self): + lines = all_generic() + self.assertGreater(len(lines), 250) + for key, line in lines: + with self.subTest(key=key, line=line): + self.assertNotRegex(line, r"[—–]| - ") # ingen tankestreger + self.assertNotRegex(line, r"[{}<>\d]") # ingen pladsholdere eller cifre + for s in sentences(line): + self.assertNotIn(s.split()[0].strip(",!?.").lower(), NUMBERS, "tal først i en sætning") + for tag in re.findall(r"\[([^\]]*)\]", line): + self.assertIn(tag, fof.AUDIO_TAGS) + self.assertRegex(line, r"[.!?]$") + + def test_no_duplicates(self): + texts = [line for _, line in all_generic()] + dupes = sorted({t for t in texts if texts.count(t) > 1}) + self.assertEqual(dupes, []) + specific = {v for lines in (fof.LINES, tegn.LINES) for vs in lines.values() for v in vs} + self.assertEqual(sorted(set(texts) & specific), [], "en generisk replik er en kopi af en almindelig") + + def test_counts(self): + common = fof.GENERIC_COMMON + self.assertTrue(20 <= len(common["lobby_join"]) <= 25) + self.assertGreaterEqual(len(common["lobby_join_fest"]), 5) + for n in range(fof_engine.MIN_PLAYERS, tegn_engine.MAX_PLAYERS + 1): + self.assertIn(len(common[f"game_start_{n}"]), (3, 4), n) + for n in range(fof_engine.MIN_PLAYERS, fof_engine.MAX_PLAYERS + 1): # fest findes kun i Fup og Fakta + self.assertGreaterEqual(len(common[f"game_start_{n}_fest"]), 2, n) + for name, (_, generic, _, _) in GAMES.items(): + for key, v in generic.items(): + if isinstance(v, str) or key.startswith(("lobby_join", "game_start_")): + continue + with self.subTest(game=name, key=key): + self.assertGreaterEqual(len(v), 3 if key.endswith("_fest") else 5) + + def test_every_name_event_has_a_generic(self): + """Alle replikker med navne eller spillertekst har en generisk udgave, i fest-tonen hvor der er en fest-replik.""" + for name, (lines, generic, lo, hi) in GAMES.items(): + n = Narrator(lines=lines, generic=generic, rng=random.Random(1)) + nf = Narrator(lines=lines, generic=generic, fest=True, rng=random.Random(1)) + for key, variants in lines.items(): + base = key.removesuffix("_fest") + holders = {p for v in variants for p in re.findall(r"\{(\w+)\}", v)} + if not holders - FINITE and base != "game_start" or base in SILENT: + continue + with self.subTest(game=name, event=key): + if base == "game_start": + counts = range(lo, (fof_engine.MAX_PLAYERS if key.endswith("_fest") else hi) + 1) + for c in counts: + self.assertTrue((nf if key.endswith("_fest") else n).generic(base, antal_spillere=c)) + continue + self.assertTrue((nf if base in FEST_ONLY else n).generic(base), "mangler generisk replik") + if key.endswith("_fest"): + self.assertTrue(nf._generic_key(base, {}).endswith("_fest"), "mangler fest-udgave") + + def test_aliases_and_keys_are_real_events(self): + for name, (lines, generic, _, _) in GAMES.items(): + for key, v in generic.items(): + with self.subTest(game=name, key=key): + base = re.sub(r"_\d+$", "", key.removesuffix("_fest")) + self.assertTrue(base in lines or base in ("chain_intro_first",), base) + if isinstance(v, str): + self.assertIsInstance(generic.get(v), list) + + def test_generic_follows_the_line(self): + n = Narrator(rng=random.Random(3)) + line = n.line("lobby_join", navn="Bo") + self.assertIn("Bo", line) + self.assertIn(line.generic(), fof.GENERIC_COMMON["lobby_join"]) + only = fof.GENERIC_COMMON["lobby_join"][4] + self.assertEqual(line.generic(lambda t: t == only), only) # kun det, accept godtager + self.assertIsNone(line.generic(lambda t: False)) + self.assertIn(n.generic("game_start", antal_spillere=5), fof.GENERIC_COMMON["game_start_5"]) + self.assertIn(n.generic("reveal_lie_shared"), fof.GENERIC["reveal_lie_fooled"]) # henvisning + f = Narrator(fest=True, rng=random.Random(3)) + self.assertIn(f.generic("lie_progress_fast"), fof.GENERIC["lie_progress_fest"]) + self.assertIn(f.generic("award_thirstiest"), fof.GENERIC["award_thirstiest_fest"]) + self.assertIsNone(n.generic("lie_hurry")) # ingen navne: ingen generisk diff --git a/tegnoggaet/narrator/generic.json b/tegnoggaet/narrator/generic.json new file mode 100644 index 0000000..84540c7 --- /dev/null +++ b/tegnoggaet/narrator/generic.json @@ -0,0 +1,81 @@ +{ + "lobby_waiting": [ + "Vi er klar. VIP'en må gerne trykke start.", + "Alle har fundet blyanten. Nu mangler vi bare startknappen.", + "[sighs] Pennene tørrer ud. VIP, tryk start.", + "Holdet er samlet. VIP'en bestemmer, hvornår vi tegner.", + "Så er vi nok. Startknappen sidder på VIP'ens telefon." + ], + "theme_choose": [ + "VIP'en vælger et tema på telefonen.", + "Hvad skal vi tegne? Det bestemmer VIP'en.", + "Tid til at vælge tema. Vælg med omhu. Eller vælg noget sjovt.", + "[curious] Hvilket tema bliver det? Det afgøres på VIP'ens telefon.", + "Et tema, tak. Resten af jer kan begynde at varme fingrene op." + ], + "chain_intro_first": [ + "Vi starter med den første kæde.", + "Første kæde. Lad os se, hvad der skete med ordet.", + "[curious] Her er den første kæde. Hvor mon den endte?", + "Den første kæde ruller ind. Hold godt fast.", + "Lad os begynde med den første kæde. Den startede så uskyldigt." + ], + "chain_intro": [ + "Her er næste kæde. Navnet på skærmen startede den.", + "Lad os se den her kæde. Den startede med et helt uskyldigt ord.", + "[curious] Næste kæde. Hvad mon der skete her?", + "Så er det en ny kæde. Hold fast.", + "Næste kæde. Kig på det første ord og gæt, hvor det ender.", + "En ny kæde ruller ind. Hvad gik galt denne gang?" + ], + "chain_survived": [ + "[gasps] Den kom hele vejen rundt!", + "Utroligt. Ordet overlevede hele turen.", + "[impressed] Perfekt kæde! Godt tegnet og godt gættet.", + "Hele vejen rundt uden et eneste uheld. Det sker næsten aldrig.", + "Den holdt! Bonus til den, der startede kæden." + ], + "chain_changed": [ + "[laughs] Se lige, hvad det første ord blev til.", + "Det startede ét sted og endte et helt andet sted.", + "[chuckles] Fra det ene til det andet. Det er kunst.", + "Ordet tog en omvej. En lang en.", + "Det første ord og det sidste gæt har ikke meget tilfælles.", + "Klassisk. Et sted på vejen gik det helt galt." + ], + "award_best_drawing": [ + "Prisen for bedste tegning! Flest hjerter til en tegning.", + "Årets kunstner er fundet. Navnet står på skærmen.", + "[impressed] Bedste tegning. Den var næsten genkendelig.", + "Flest hjerter til en tegning. Museet har ringet.", + "Bedste tegning går til en, der bør tegne noget mere." + ], + "award_best_guess": [ + "Bedste gæt! Flest hjerter til et gæt.", + "Prisen for bedste gæt. Fantasi slår facit.", + "[laughs] Det sjoveste gæt i aften fik flest hjerter.", + "Bedste gæt. Nogle gange er det forkerte svar det rigtige.", + "Flest hjerter til et gæt. Hjernen bag det står på skærmen." + ], + "award_survived": [ + "Kæden der overlevede! Bonus til den, der startede den.", + "En kæde kom hele vejen rundt. Det giver bonus.", + "[impressed] Ordet overlevede hele turen. Bonus!", + "Den kæde holdt. Bonuspoint til ejeren.", + "Prisen for kæden, der overlevede. Sjældent og smukt." + ], + "award_transformed": [ + "Største forvandling! Den kæde tog den længste omvej.", + "Prisen for største forvandling. Ordet blev til noget helt andet.", + "[laughs] Ingen kæde ændrede sig mere end den her.", + "Største forvandling. Fra noget til noget helt andet.", + "Den kæde forvandlede sig mest. Det er næsten magi." + ], + "winner": [ + "Vi har en vinder! Aftenens kunstner står øverst.", + "[excited] Tillykke til vinderen!", + "Spillet er slut, og vinderen står øverst på skærmen.", + "Tillykke! Flest point og fortjent ære.", + "Sejren er afgjort. Blyanten går videre til næste gang." + ] +}