diff --git a/party/tegn.py b/party/tegn.py index f07678b..0705f1c 100644 --- a/party/tegn.py +++ b/party/tegn.py @@ -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: diff --git a/tegnoggaet/engine.py b/tegnoggaet/engine.py index 9376197..504abd0 100644 --- a/tegnoggaet/engine.py +++ b/tegnoggaet/engine.py @@ -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"} diff --git a/tegnoggaet/tests/test_engine.py b/tegnoggaet/tests/test_engine.py index dc3c381..3581094 100644 --- a/tegnoggaet/tests/test_engine.py +++ b/tegnoggaet/tests/test_engine.py @@ -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