Tegn og Gæt: kæden slutter altid på et gæt
Ved et lige antal spillere får kæden et trin mere: i trin 1 tegner ejeren sit eget ord, og rotationen starter et trin senere. Antal trin hedder nu steps og bruges i step-eventet, snapshot og afsløringen; self.n er antal spillere. Snapshot får step.own til trin 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+6
-5
@@ -224,9 +224,10 @@ Oprettes med `game=tegn` på `/host/new` (se [Spillene og skift af spil](#spille
|
||||
Samme endpoints, samme `join`/`resume`/`ping`/`react`, og samme `joined`/`state`/`error`. Snapshot har `"game": "tegn"`.
|
||||
3-10 spillere; dem der joiner efter start, bliver publikum (de kan give hjerter).
|
||||
|
||||
Med N spillere er der N kæder, én pr. spiller. I trin `t` (0..N-1) arbejder spiller `i` (rækkefølgen ved start)
|
||||
på kæde `(i - t) mod N`: trin 0 er et ord, ulige trin tegnes, lige trin gættes. Ingen får sin egen kæde igen, og
|
||||
hver kæde ender med N bidrag. Med et lige antal spillere ender kæden på en tegning; så er "sidste gæt" gættet før den.
|
||||
Med N spillere er der N kæder, én pr. spiller: trin 0 er et ord, ulige trin tegnes, lige trin gættes, og kæden
|
||||
slutter altid på et gæt. Ulige N: N trin; i trin `t` arbejder spiller `i` (rækkefølgen ved start) på kæde
|
||||
`(i - t) mod N`. Lige N: N+1 trin; i trin 1 tegner ejeren sit eget ord (`step.own: true`), og i trin `t >= 1`
|
||||
arbejder spiller `i` på kæde `(i - (t-1)) mod N`. Ingen gætter på sin egen kæde.
|
||||
|
||||
Faser: `lobby → rules` (kun rummets første spil) `→ theme` (20 s) `→ word` (30 s) `→ draw` (75 s) / `guess` (30 s) skiftevis
|
||||
`→ reveal → final`. Et trin slutter, når alle forbundne spillere har afleveret, eller når tiden er gået. Så fyldes det
|
||||
@@ -273,11 +274,11 @@ Fejlkoder ud over Fup og Fakta: `word_invalid`, `guess_invalid`, `drawing_invali
|
||||
"rules": {...}, // fase rules, som Fup og Fakta
|
||||
"theme": {"options": [{"id": "dyr", "name": "Dyr", "icon": "🐘"}], "chosen_id": null}, // fase theme
|
||||
"theme": {"name": "Dyr", "icon": "🐘"}, // word/draw/guess/reveal
|
||||
"step": {"index": 1, "number": 2, "of": 5, "kind": "draw"}, // word/draw/guess
|
||||
"step": {"index": 1, "number": 2, "of": 5, "kind": "draw", "own": false}, // word/draw/guess; own = tegn dit eget ord
|
||||
"reveal": {
|
||||
"id": 1,
|
||||
"started_at": 1790000000000, // ms epoch; null = serveren laver stadig lyd
|
||||
"plan": [ // tidslinjen: pr. kæde en intro (entry -1), hvert bidrag, og slutningen (entry N);
|
||||
"plan": [ // tidslinjen: pr. kæde en intro (entry -1), hvert bidrag, og slutningen (entry = antal trin);
|
||||
// audio (speakerens replik) kun til storskærmen, da slutningen nævner sidste gæt
|
||||
{"chain": 0, "entry": -1, "ms": 2500, "audio": null},
|
||||
{"chain": 0, "entry": 0, "ms": 3000, "audio": null},
|
||||
|
||||
+25
-9
@@ -5,9 +5,11 @@ handlinger tager `now` (sekunder siden epoch) som parameter, rummet i
|
||||
`party.tegn` kalder `tick()` når en deadline udløber og sender `snapshot()` til
|
||||
hver skærm.
|
||||
|
||||
Med N spillere er der N kæder, én pr. spiller. I trin t (0..N-1) arbejder
|
||||
spiller i på kæde (i - t) mod N: trin 0 er et ord, ulige trin tegnes, lige trin
|
||||
gættes. Ingen får sin egen kæde igen, og hver kæde ender med N bidrag.
|
||||
Med N spillere er der N kæder, én pr. spiller: trin 0 er et ord, ulige trin
|
||||
tegnes, lige trin gættes, og kæden slutter altid på et gæt. Ulige N: N trin, og i
|
||||
trin t arbejder spiller i på kæde (i - t) mod N. Lige N: N+1 trin; i trin 1
|
||||
tegner ejeren sit eget ord, og i trin t >= 1 arbejder spiller i på kæde
|
||||
(i - (t-1)) mod N. Ingen gætter på sin egen kæde.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -204,8 +206,19 @@ class Game:
|
||||
|
||||
@property
|
||||
def n(self) -> int:
|
||||
"""Antal spillere i kæderne."""
|
||||
return len(self.order)
|
||||
|
||||
@property
|
||||
def steps(self) -> int:
|
||||
"""Antal trin. Kæden skal slutte på et gæt: ved et lige antal spillere tegner ejeren selv sit ord
|
||||
i trin 1, og så bliver der et trin mere."""
|
||||
return self.n if self.n % 2 else self.n + 1
|
||||
|
||||
def own_step(self, t: int | None = None) -> bool:
|
||||
"""Trin 1 ved et lige antal spillere: ejeren tegner sit eget ord."""
|
||||
return (self.step if t is None else t) == 1 and self.n % 2 == 0
|
||||
|
||||
@property
|
||||
def rules(self) -> list[dict]:
|
||||
return RULES
|
||||
@@ -219,6 +232,8 @@ class Game:
|
||||
if member_id not in self.order:
|
||||
return None
|
||||
t = self.step if t is None else t
|
||||
if self.n % 2 == 0 and t > 0:
|
||||
t -= 1 # trin 1 bliver hos ejeren, rotationen starter et trin senere
|
||||
return (self.order.index(member_id) - t) % self.n
|
||||
|
||||
def _entry(self, entry_id: str) -> dict | None:
|
||||
@@ -371,7 +386,8 @@ class Game:
|
||||
self.drafts = {}
|
||||
self.phase = step_kind(t)
|
||||
self.deadline = now + {"word": WORD_SECONDS, "draw": DRAW_SECONDS, "guess": GUESS_SECONDS}[self.phase]
|
||||
self._emit("step", step=self.phase, number=t + 1, of=self.n, theme=self.theme["name"])
|
||||
self._emit("step", step=self.phase, number=t + 1, of=self.steps, theme=self.theme["name"],
|
||||
own=self.own_step(t))
|
||||
self._changed()
|
||||
|
||||
def task(self, member_id: str) -> dict | None:
|
||||
@@ -481,7 +497,7 @@ class Game:
|
||||
else:
|
||||
value = draft or EMPTY_GUESS
|
||||
self._put(c, pid, value, auto=True)
|
||||
if self.step + 1 < self.n:
|
||||
if self.step + 1 < self.steps:
|
||||
self._begin_step(self.step + 1, now)
|
||||
else:
|
||||
self._start_reveal(now)
|
||||
@@ -492,8 +508,7 @@ class Game:
|
||||
self.likes = {}
|
||||
self.survived = []
|
||||
for chain in self.chains:
|
||||
# Sidste gæt mod første ord. Med et lige antal spillere ender kæden på en tegning,
|
||||
# og så er det gættet før den, der tæller.
|
||||
# Sidste gæt mod første ord
|
||||
last = self._last_guess(chain)
|
||||
self.survived.append(last is not None and last != EMPTY_GUESS
|
||||
and normalize(last) == normalize(chain[0]["text"]))
|
||||
@@ -505,7 +520,7 @@ class Game:
|
||||
self.plan.append({"chain": k, "entry": i, "ms": ms, "audio": None})
|
||||
self.plan.append({"chain": k, "entry": len(chain), "ms": CHAIN_END_MS, "audio": None})
|
||||
self.phase = "reveal"
|
||||
self.step = self.n
|
||||
self.step = self.steps
|
||||
self.start_reveal_timeline(now)
|
||||
self._emit("reveal")
|
||||
self._changed()
|
||||
@@ -728,7 +743,8 @@ class Game:
|
||||
if stepping or self.phase == "reveal":
|
||||
state["theme"] = {"name": self.theme["name"], "icon": self.theme.get("icon", "")}
|
||||
if stepping:
|
||||
state["step"] = {"index": self.step, "number": self.step + 1, "of": self.n, "kind": self.phase}
|
||||
state["step"] = {"index": self.step, "number": self.step + 1, "of": self.steps, "kind": self.phase,
|
||||
"own": self.own_step()}
|
||||
if self.phase == "reveal":
|
||||
state["reveal"] = {
|
||||
"id": self.reveal_id,
|
||||
|
||||
@@ -54,24 +54,54 @@ class TegnTestCase(SimpleTestCase):
|
||||
|
||||
|
||||
class RotationTests(TegnTestCase):
|
||||
def test_rotation_never_gives_own_chain_and_all_chains_get_n_entries(self):
|
||||
for n in range(3, 11):
|
||||
def test_every_chain_ends_on_a_guess(self):
|
||||
"""Ulige N: N trin. Lige N: N+1 trin, og i trin 1 tegner ejeren sit eget ord."""
|
||||
for n in (3, 4, 5, 6, 7, 8, 9, 10):
|
||||
with self.subTest(n=n):
|
||||
self.g = Game("TEST", THEMES, rng=random.Random(n))
|
||||
ps = self.start(n)
|
||||
steps = n if n % 2 else n + 1
|
||||
self.assertEqual(self.g.steps, steps)
|
||||
owner = {k: pid for k, pid in enumerate(self.g.order)}
|
||||
for t in range(n):
|
||||
own = {self.g.order.index(p.id): p.id for p in ps}
|
||||
for t in range(steps):
|
||||
chains = [self.g.chain_for(p.id, t) for p in ps]
|
||||
self.assertEqual(sorted(chains), list(range(n)), "hver kæde har præcis én spiller pr. trin")
|
||||
at_home = t == 0 or (t == 1 and n % 2 == 0)
|
||||
for p, c in zip(ps, chains):
|
||||
self.assertEqual(c == self.g.order.index(p.id), t == 0, "egen kæde kun i trin 0")
|
||||
self.play_all(ps)
|
||||
self.assertEqual(own[c] == p.id, at_home, "egen kæde kun i trin 0 (og 1 ved lige N)")
|
||||
t = 0
|
||||
while self.g.phase in ("word", "draw", "guess"):
|
||||
self.assertEqual(self.g.step, t)
|
||||
snap = self.g.snapshot(self.now, host=True)
|
||||
self.assertEqual(snap["step"], {"index": t, "number": t + 1, "of": steps, "kind": self.g.phase,
|
||||
"own": t == 1 and n % 2 == 0})
|
||||
for p in ps:
|
||||
task = self.g.task(p.id)
|
||||
if t == 1 and n % 2 == 0: # ejeren får sit eget ord
|
||||
self.assertEqual((task["kind"], task["from"], task["text"]), ("draw", p.id, f"ord {p.name}".lower()))
|
||||
elif t > 0:
|
||||
self.assertNotEqual(task["from"], p.id)
|
||||
self.play_step(ps)
|
||||
t += 1
|
||||
self.assertEqual(t, steps)
|
||||
self.assertEqual(self.g.phase, "reveal")
|
||||
for k, chain in enumerate(self.g.chains):
|
||||
self.assertEqual(len(chain), n)
|
||||
self.assertEqual(len({e["by"] for e in chain}), n, "alle bidrager én gang til hver kæde")
|
||||
self.assertEqual(len(chain), steps)
|
||||
self.assertEqual(chain[-1]["kind"], "guess", "kæden slutter på et gæt")
|
||||
self.assertEqual(chain[0]["by"], owner[k])
|
||||
self.assertEqual([e["kind"] for e in chain],
|
||||
["word"] + ["drawing" if t % 2 else "guess" for t in range(1, n)])
|
||||
["word"] + ["drawing" if t % 2 else "guess" for t in range(1, steps)])
|
||||
self.assertNotIn(owner[k], [e["by"] for e in chain if e["kind"] == "guess"],
|
||||
"ingen gætter på sin egen kæde")
|
||||
self.assertEqual(len({e["by"] for e in chain}), n, "alle bidrager til hver kæde")
|
||||
if n % 2 == 0:
|
||||
self.assertEqual(chain[1]["by"], owner[k], "ejeren tegner selv i trin 1")
|
||||
self.assertEqual(len({e["by"] for e in chain[1 if n % 2 == 0 else 0:]}), n,
|
||||
"ud over ejerens to første trin ser ingen kæden to gange")
|
||||
for t in range(steps):
|
||||
by = sorted(chain[t]["by"] for chain in self.g.chains)
|
||||
self.assertEqual(by, sorted(p.id for p in ps), "hver spiller ét bidrag pr. trin")
|
||||
|
||||
def test_kicks_before_the_chains_start_go_back_to_lobby(self):
|
||||
a, b, c = self.join(3)
|
||||
@@ -193,7 +223,7 @@ class TimeoutTests(TegnTestCase):
|
||||
a, b, c, d = self.start(4)
|
||||
self.g.kick(d.id, self.now)
|
||||
self.play_all([a, b, c])
|
||||
self.assertTrue(all(len(chain) == 4 for chain in self.g.chains))
|
||||
self.assertTrue(all(len(chain) == 5 for chain in self.g.chains))
|
||||
self.assertTrue(all(e["auto"] for chain in self.g.chains for e in chain if e["by"] == d.id))
|
||||
|
||||
def test_held_phase_waits_for_the_speaker(self):
|
||||
@@ -249,8 +279,8 @@ class RevealTests(TegnTestCase):
|
||||
def test_plan_covers_every_chain_and_entry(self):
|
||||
self.play_game(4)
|
||||
plan = self.g.plan
|
||||
self.assertEqual(len(plan), 4 * (4 + 2))
|
||||
self.assertEqual([b["entry"] for b in plan[:6]], [-1, 0, 1, 2, 3, 4])
|
||||
self.assertEqual(len(plan), 4 * (5 + 2)) # 4 spillere: 5 trin, så kæden slutter på et gæt
|
||||
self.assertEqual([b["entry"] for b in plan[:7]], [-1, 0, 1, 2, 3, 4, 5])
|
||||
snap = self.g.snapshot(self.now, host=True)
|
||||
self.assertNotIn("strokes", str(snap["reveal"])) # tegningerne kommer i beskeden "drawings"
|
||||
self.assertEqual(len(self.g.drawings()), 4 * 2)
|
||||
|
||||
Reference in New Issue
Block a user