Hjernen: ingen kald til replikker, der alligevel ikke kan siges

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 <noreply@anthropic.com>
This commit is contained in:
agw
2026-10-20 21:59:30 +02:00
co-authored by Claude Opus 5.5
parent 8cba2e8071
commit 4d1a2ad01d
3 changed files with 78 additions and 7 deletions
+10 -4
View File
@@ -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))
+4 -2
View File
@@ -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())
+64 -1
View File
@@ -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))