Tegn og Gæt: telefonerne ser kun det, storskærmen har vist

Under afsløringen får telefonerne kun de bidrag, der allerede er vist
(og om kæden overlevede, når slutningen er vist). Rummet sender et
nyt snapshot, hver gang et trin begynder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
agw
2026-10-20 17:11:58 +02:00
co-authored by Claude Opus 5.5
parent ab7ab1e8f1
commit d991279196
3 changed files with 73 additions and 12 deletions
+23
View File
@@ -39,6 +39,29 @@ class TegnRoom(rooms.BaseRoom):
self.narrator = Narrator(lines=LINES)
self._last_join_line = 0.0
self._lobby_waiting_said = False
self._beat_timers: list[asyncio.TimerHandle] = []
def changed(self):
super().changed()
self._schedule_beats()
def _schedule_beats(self):
"""Telefonerne får kun de trin, storskærmen har vist: et nyt snapshot hver gang et trin begynder."""
for h in self._beat_timers:
h.cancel()
self._beat_timers = []
g = self.game
if g.phase != "reveal":
return
loop, now = asyncio.get_running_loop(), time.time()
for t in g.beat_starts():
if t > now:
self._beat_timers.append(loop.call_later(t - now + 0.02, self.broadcast))
def close(self):
super().close()
for h in self._beat_timers:
h.cancel()
# ── handlinger ───────────────────────────────────────────────────────────
async def host_action(self, conn, action: str, content: dict, now: float) -> bool:
+32 -12
View File
@@ -540,15 +540,14 @@ class Game:
el -= b["ms"]
return len(self.plan) - 1
def _revealed(self, entry_id: str, now: float) -> bool:
def shown_beats(self, now: float) -> list[dict]:
"""Trinene, storskærmen har vist indtil nu (inklusive det aktuelle)."""
i = self.beat_at(now)
if i is None:
return False
for b in self.plan[: i + 1]:
chain = self.chains[b["chain"]]
if 0 <= b["entry"] < len(chain) and chain[b["entry"]]["id"] == entry_id:
return True
return False
return [] if i is None else self.plan[: i + 1]
def _revealed(self, entry_id: str, now: float) -> bool:
return any(0 <= b["entry"] < len(self.chains[b["chain"]]) and self.chains[b["chain"]][b["entry"]]["id"] == entry_id
for b in self.shown_beats(now))
def like(self, member_id: str, entry_id: str, now: float) -> None:
self._require_phase("reveal")
@@ -736,10 +735,7 @@ class Game:
"started_at": int(self.reveal_started_at * 1000) if self.reveal_started_at else None,
"plan": [{"chain": b["chain"], "entry": b["entry"], "ms": b["ms"], "audio": b["audio"]} for b in self.plan],
# Tegningerne følger ikke med her (de er store); storskærmen får dem i beskeden "drawings".
"chains": [{"owner": self.order[k], "survived": self.survived[k],
"entries": [{"id": e["id"], "kind": e["kind"], "by": e["by"], "text": e["text"],
"auto": e["auto"], "likes": len(self.likes.get(e["id"], ()))} for e in chain]}
for k, chain in enumerate(self.chains)],
"chains": self._reveal_chains(now, everything=host),
}
if self.phase == "final":
state["final"] = self.final_result
@@ -756,6 +752,30 @@ class Game:
}
return state
def _reveal_chains(self, now: float, everything: bool) -> list[dict]:
"""Kæderne i afsløringen. Telefonerne får kun det, storskærmen har vist (ellers kunne man læse
sidste gæt før tid); storskærmen får det hele og styrer selv tempoet. Rummet sender et nyt
snapshot ved hvert trin."""
shown = None if everything else {(b["chain"], b["entry"]) for b in self.shown_beats(now)}
out = []
for k, chain in enumerate(self.chains):
entries = [{"id": e["id"], "kind": e["kind"], "by": e["by"], "text": e["text"], "auto": e["auto"],
"likes": len(self.likes.get(e["id"], ()))}
for i, e in enumerate(chain) if shown is None or (k, i) in shown]
ended = shown is None or (k, len(chain)) in shown
out.append({"owner": self.order[k], "survived": self.survived[k] if ended else None, "entries": entries})
return out
def beat_starts(self) -> list[float]:
"""Hvornår hvert trin i afsløringen begynder (sekunder siden epoch)."""
if self.reveal_started_at is None:
return []
t, out = self.reveal_started_at, []
for b in self.plan:
out.append(t)
t += b["ms"] / 1000
return out
def drawings(self) -> dict[str, list]:
"""Alle tegninger i den aktuelle afsløring (til storskærmen)."""
return {e["id"]: e["strokes"] for chain in self.chains for e in chain if e["kind"] == "drawing"}
+18
View File
@@ -256,6 +256,24 @@ class RevealTests(TegnTestCase):
self.assertEqual(len(self.g.drawings()), 4 * 2)
self.assertEqual(snap["reveal"]["chains"][0]["entries"][0]["kind"], "word")
def test_phones_only_get_what_the_tv_has_shown(self):
a, b, c = self.play_game(3, survive=True)
def chains(t):
return self.g.snapshot(t, viewer=a)["reveal"]["chains"]
self.assertEqual([len(ch["entries"]) for ch in chains(self.now)], [0, 0, 0]) # kædens intro
t = self.now + (engine.CHAIN_INTRO_MS + 10) / 1000
self.assertEqual([e["id"] for e in chains(t)[0]["entries"]], ["c0e0"])
self.assertIsNone(chains(t)[0]["survived"]) # afgøres først på storskærmen
t = self.now + (engine.CHAIN_INTRO_MS + engine.WORD_MS + engine.DRAWING_MS + engine.GUESS_MS + 10) / 1000
ch = chains(t)
self.assertEqual((len(ch[0]["entries"]), ch[0]["survived"], len(ch[1]["entries"])), (3, True, 0))
self.assertNotIn("kat", str(ch[1:]))
host = self.g.snapshot(self.now, host=True)["reveal"]["chains"]
self.assertEqual([len(x["entries"]) for x in host], [3, 3, 3]) # storskærmen får alt
starts = self.g.beat_starts()
self.assertEqual(len(starts), len(self.g.plan))
self.assertEqual(starts[1], self.now + engine.CHAIN_INTRO_MS / 1000)
def test_likes_only_on_revealed_drawings_and_guesses_by_others(self):
a, b, c = self.play_game(3)
word, drawing, guess = self.g.chains[0] # a's kæde: ord af a, tegning af b, gæt af c