From 4d1a2ad01d3c44c364b0a0dc97a5e8a905ecd6c0 Mon Sep 17 00:00:00 2001 From: Asger Geel Weirsoee Date: Tue, 20 Oct 2026 21:59:30 +0200 Subject: [PATCH] Hjernen: ingen kald til replikker, der alligevel ikke kan siges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kan stemmen ikke laves live (WPP_TTS_LIVE=off, ingen nøgle, pause efter en fejl eller lav saldo), siges en fast replik i stedet for hjernens, så kaldene var spildt. Nu springes de over, når tts.live_ok() er falsk: fremskridt under løgnene, drillerier, afsløringens og oplæsningens kommentarer og slutanmeldelsen. Dommeren (judge_lie) kaldes som før, for den afgør, om en løgn er sandheden. Lederens beslutning efter review. Test: i off kaldes dommeren én gang og ingen af de andre; med live-stemme kaldes de alle som før. test_brain's rum-tests siger nu selv, at stemmen er live. Co-Authored-By: Claude Opus 5.5 --- party/rooms.py | 14 +++++-- party/tests/test_brain.py | 6 ++- party/tests/test_tts_fallback.py | 65 +++++++++++++++++++++++++++++++- 3 files changed, 78 insertions(+), 7 deletions(-) diff --git a/party/rooms.py b/party/rooms.py index e78a6a6..6bcaed3 100644 --- a/party/rooms.py +++ b/party/rooms.py @@ -391,6 +391,12 @@ class Room(BaseRoom): self._schedule_timer() asyncio.get_running_loop().create_task(run()) + def _brain_speaks(self) -> bool: + """Hjernens replikker, der kun bliver sagt (fremskridt, drillerier, kommentarer, slutanmeldelse), caches + aldrig. Kan stemmen ikke laves live, siges en fast replik i stedet, så kaldet springes over. Dommeren + (judge_lie) påvirker spillet og kaldes altid, når der er en hjerne.""" + return bool(self.brain) and tts.live_ok() + def prepare_judging(self): """Kaldes før en løgn afleveres: hold skærmen et øjeblik, så dommeren når at se den sidste løgn.""" if self.brain: @@ -432,7 +438,7 @@ class Room(BaseRoom): f"{names.get(author, 'huset')} {n} gange i aften.") elif t == "scores": self.remember(f"Efter runden fører {ev['leader']}, og {ev['last']} ligger sidst.") - elif t == "final" and self.brain: + elif t == "final" and self._brain_speaks(): players = [{"player_id": p.id, "navn": p.name, "plads": i + 1, "point": p.score, "narrede_andre": p.fooled, "fandt_sandheden": p.correct, "hjerter": p.likes} for i, p in enumerate(sorted(g.players.values(), key=lambda p: -p.score))] @@ -472,7 +478,7 @@ class Room(BaseRoom): # faste replikker med navnet: lyden caches, efterhånden som navnene går igen key = ("lie_progress_retry" if info["forsøg"] > 1 else "lie_progress_fast" if info["sekunder_om_det"] < 15 else "lie_progress") - if self.brain and settings.BRAIN_PROGRESS: + if self._brain_speaks() and settings.BRAIN_PROGRESS: line = await self.brain.lie_progress(info, list(self.memory)) line = instead_of(self.narrator.clean_ai(line), self.narrator.line(key, navn=p.name)) if line else line else: @@ -588,7 +594,7 @@ class Room(BaseRoom): g.hold_reveal(time.time(), max_wait=COMMENT_WAIT + REVEAL_AUDIO_WAIT + 2 + max(0.0, self.speech_end - time.time())) self.changed() names = {p.id: p.name for p in g.players.values()} - if self.brain: + if self._brain_speaks(): # drillerierne er som standard faste (cachet lyd); hjernens personlige udgave er et tilvalg tease_job = (asyncio.get_running_loop().create_task(self._brain_teases(names)) if settings.BRAIN_TEASES else None) @@ -662,7 +668,7 @@ class Room(BaseRoom): pitch = {engine.normalize(k): v for k, v in ((g.question or {}).get("pitch") or {}).items()} comments = {a.id: pitch[engine.normalize(a.text)] for a in g.answers if engine.normalize(a.text) in pitch} missing = [{"id": a.id, "text": a.text} for a in g.answers if a.id not in comments] - if missing and self.brain: + if missing and self._brain_speaks(): try: comments.update(await asyncio.wait_for( self.brain.present_lines(dict(g.question or {}), missing), timeout=COMMENT_WAIT)) diff --git a/party/tests/test_brain.py b/party/tests/test_brain.py index 8b940a6..d26c1b9 100644 --- a/party/tests/test_brain.py +++ b/party/tests/test_brain.py @@ -89,7 +89,8 @@ class BrainInGameTests(TransactionTestCase): async def test_judge_rejects_lie_and_comments_reach_reveal(self): room = await self._room() - with mock.patch.object(tts, "enabled", return_value=True), mock.patch.object(tts, "synth", fake_synth): + with mock.patch.object(tts, "enabled", return_value=True), mock.patch.object(tts, "synth", fake_synth), \ + mock.patch.object(tts, "live_ok", return_value=True): # en live-stemme: hjernens replikker siges host = WebsocketCommunicator(application, f"/ws/host/{room.code}/", headers=HOST_HEADERS) host.scope["client"] = ("127.0.0.1", 5000) await host.connect() @@ -155,7 +156,8 @@ class PresentAndSummaryTests(TransactionTestCase): with mock.patch.object(rooms, "get_brain", return_value=GameBrain(fake)): room = rooms.create_room(await sync_to_async(load_game_questions)(), "agw", "http://testserver") g = room.game - with mock.patch.object(tts, "enabled", return_value=True), mock.patch.object(tts, "synth", fake_synth): + with mock.patch.object(tts, "enabled", return_value=True), mock.patch.object(tts, "synth", fake_synth), \ + mock.patch.object(tts, "live_ok", return_value=True): # en live-stemme: hjernens replikker siges import time as _t a, b = g.join("Asger"), g.join("Sus") g.start(None, _t.time()) diff --git a/party/tests/test_tts_fallback.py b/party/tests/test_tts_fallback.py index 1992141..295bf26 100644 --- a/party/tests/test_tts_fallback.py +++ b/party/tests/test_tts_fallback.py @@ -19,7 +19,8 @@ from party.tegn import TegnRoom from tegnoggaet.engine import Game as TegnGame from tegnoggaet.themes import load_themes -QUESTIONS = [{"id": i, "category": "Kat", "prompt": f"Hvad _____ {i}?", "answer": f"svar{i}"} for i in range(9)] +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)] def http_error(code): @@ -190,3 +191,65 @@ class FallbackSeamTests(CacheMixin, SimpleTestCase): combos = first_chain_generics(r.narrator, r.narrator.variants("reveal_intro")[0]) self.assertEqual(len(combos), len(r.narrator.generic_variants("chain_intro_first"))) self.assertTrue(all(c.startswith(r.narrator.variants("reveal_intro")[0] + " ") for c in combos)) + + +class BrainOnlyWhenSpokenTests(CacheMixin, SimpleTestCase): + """Lederens beslutning: uden live-stemme kaldes hjernen ikke til det, der kun bliver sagt; dommeren kaldes altid.""" + + def brain(self): + b = mock.Mock() + b.judge_lie = mock.AsyncMock(return_value={"is_truth": False, "reason": ""}) + b.lie_progress = mock.AsyncMock(return_value="Qxa skriver hurtigt.") + b.final_review = mock.AsyncMock(return_value=[]) + b.reveal_lines = mock.AsyncMock(return_value={}) + b.teases = mock.AsyncMock(return_value={}) + b.present_lines = mock.AsyncMock(return_value={}) + return b + + def play(self): + """Et rum med hjerne, ført gennem ét spørgsmål til afsløringen; hjernens kald tælles.""" + calls = {} + + async def run(): + g = FofGame("TEST", QUESTIONS, rng=random.Random(1)) + g.show_rules = False + r = Room("TEST", g, "agw") + r.brain = self.brain() + now = time.time() + ps = [g.join(n) for n in ("Qxa", "Zyb", "Wvc")] + g.start(None, now) + g.choose_category(g.chooser_id, g.category_options[0]["id"], now) + g.tick(now + 60) + await r._judge(ps[0].id, "en løgn", dict(g.question)) + await r._progress_remark(ps[1].id) # mens der løjes + for i, p in enumerate(ps): + g.submit_lie(p.id, f"løgn {i}", now) + truth = next(a for a in g.answers if a.is_truth) + for p in ps: + g.guess(p.id, truth.id, now) + g.drain_events() + await r._narrate_reveal() + g.answers = g.answers[:2] + await r._narrate_present() + r._think({"type": "final"}) + await asyncio.sleep(0.01) + r.close() + for name in ("judge_lie", "lie_progress", "final_review", "reveal_lines", "present_lines"): + calls[name] = getattr(r.brain, name).await_count + with override_settings(BRAIN_PROGRESS=True, BRAIN_TEASES=True), mock.patch.object(rooms, "MAX_HOLD", 0), \ + mock.patch.object(rooms, "COMMENT_WAIT", 0.2), mock.patch.object(rooms, "REVEAL_AUDIO_WAIT", 0.2), \ + self.fake_fetch(): + asyncio.run(run()) + return calls + + def test_off_skips_speech_only_calls_but_still_judges(self): + with override_settings(TTS_LIVE="off"): + calls = self.play() + self.assertEqual(calls, {"judge_lie": 1, "lie_progress": 0, "final_review": 0, "reveal_lines": 0, + "present_lines": 0}) + + def test_live_calls_the_brain_as_before(self): + calls = self.play() + self.assertEqual(calls["judge_lie"], 1) + self.assertEqual((calls["lie_progress"], calls["final_review"], calls["reveal_lines"], calls["present_lines"]), + (1, 1, 1, 1))