CI / test (push) Successful in 1m32s
Fest-spillet hedder nu Fest-Z (kategorierne "Fest-Z: …"), og telefonerne får zoomer-emojis (💀 😭 🧢 🗿 💅 🍻) i stedet for familie-sættet. Serveren sender sættet i tilstanden, så klienten altid viser det, spillet tager imod. Reglerne og speakeren taler om slang og memes frem for pop-sladder. Claude-Session: https://claude.ai/code/session_01JmhJG6JnhXfzNiKv8f2vHu
374 lines
17 KiB
Python
374 lines
17 KiB
Python
import random
|
||
|
||
from django.test import SimpleTestCase
|
||
|
||
from fupogfakta import engine
|
||
from fupogfakta.engine import Game, GameError
|
||
|
||
|
||
def bank(n_cats=5, per_cat=6):
|
||
qs = []
|
||
for c in range(1, n_cats + 1):
|
||
for i in range(per_cat):
|
||
qs.append({"id": c * 100 + i, "category_id": c, "category": f"Kategori {c}",
|
||
"prompt": f"Spørgsmål {c}-{i} har svaret _____.", "answer": f"sandhed{c}{i}",
|
||
"alternatives": [], "decoys": [f"lok{c}{i}a", f"lok{c}{i}b", f"lok{c}{i}c"], "note": "Fordi."})
|
||
return qs
|
||
|
||
|
||
|
||
def setUpModule():
|
||
from fupogfakta.engine import Game
|
||
assert Game.show_rules is True # standarden i drift
|
||
Game.show_rules = False # reglerne testes for sig (RulesTests)
|
||
|
||
|
||
def tearDownModule():
|
||
from fupogfakta.engine import Game
|
||
Game.show_rules = True
|
||
|
||
class GameTestCase(SimpleTestCase):
|
||
def setUp(self):
|
||
self.now = 1000.0
|
||
self.g = Game("TEST", bank(), join_url="http://x/?code=TEST", rng=random.Random(4))
|
||
|
||
def join(self, *names):
|
||
return [self.g.join(n) for n in names]
|
||
|
||
def through_category(self):
|
||
if self.g.phase == "category":
|
||
self.g.choose_category(self.g.chooser_id, self.g.category_options[0]["id"], self.now)
|
||
self.assertEqual(self.g.phase, "intro")
|
||
self.now += engine.INTRO_SECONDS
|
||
self.g.tick(self.now)
|
||
self.assertEqual(self.g.phase, "lie")
|
||
|
||
def truth_id(self):
|
||
return next(a.id for a in self.g.answers if a.is_truth)
|
||
|
||
|
||
class LobbyTests(GameTestCase):
|
||
def test_vip_is_first_player_and_only_vip_starts(self):
|
||
a, b = self.join("Asger", "Sus")
|
||
with self.assertRaises(GameError) as e:
|
||
self.g.start(b.id, self.now)
|
||
self.assertEqual(e.exception.code, "not_allowed")
|
||
self.g.start(a.id, self.now)
|
||
self.assertEqual(self.g.phase, "category")
|
||
|
||
def test_too_few_players(self):
|
||
self.join("Asger")
|
||
with self.assertRaises(GameError) as e:
|
||
self.g.start(None, self.now)
|
||
self.assertEqual(e.exception.code, "too_few_players")
|
||
|
||
def test_name_rules(self):
|
||
self.join("Asger")
|
||
for bad, code in [("asger", "name_taken"), ("", "name_invalid"), ("x" * 13, "name_invalid")]:
|
||
with self.assertRaises(GameError) as e:
|
||
self.g.join(bad)
|
||
self.assertEqual(e.exception.code, code)
|
||
|
||
def test_ninth_and_late_joiners_become_audience(self):
|
||
players = self.join(*[f"P{i}" for i in range(8)])
|
||
self.assertTrue(all(p.role == "player" for p in players))
|
||
self.assertEqual(self.g.join("Ni").role, "audience")
|
||
self.g.start(None, self.now)
|
||
self.assertEqual(self.g.join("Sen").role, "audience")
|
||
|
||
def test_resume_by_token(self):
|
||
a, = self.join("Asger")
|
||
self.assertIs(self.g.resume(a.token), a)
|
||
with self.assertRaises(GameError):
|
||
self.g.resume("nope")
|
||
|
||
|
||
class RoundTests(GameTestCase):
|
||
def setUp(self):
|
||
super().setUp()
|
||
self.a, self.b, self.c = self.join("A", "B", "C")
|
||
self.g.start(None, self.now)
|
||
self.through_category()
|
||
|
||
def test_only_chooser_may_choose(self):
|
||
self.g._begin_question(self.now)
|
||
other = next(p for p in self.g.players if p != self.g.chooser_id)
|
||
with self.assertRaises(GameError):
|
||
self.g.choose_category(other, self.g.category_options[0]["id"], self.now)
|
||
|
||
def test_lie_equal_or_close_to_truth_is_rejected(self):
|
||
truth = self.g.question["answer"]
|
||
for attempt in [truth, truth.upper() + "!", " " + truth + " ", "en " + truth, truth[:-1]]:
|
||
with self.assertRaises(GameError) as e:
|
||
self.g.submit_lie(self.a.id, attempt, self.now)
|
||
self.assertEqual(e.exception.code, "lie_is_truth", attempt)
|
||
|
||
def test_all_lies_moves_to_guess_and_hides_own_lie(self):
|
||
self.g.submit_lie(self.a.id, "katte", self.now)
|
||
self.g.submit_lie(self.b.id, "hunde", self.now)
|
||
self.assertEqual(self.g.phase, "lie")
|
||
self.g.submit_lie(self.c.id, "KATTE", self.now) # samme løgn som A
|
||
self.assertEqual(self.g.phase, "guess")
|
||
texts = [a.text for a in self.g.answers]
|
||
self.assertEqual(len(self.g.answers), 4) # katte, hunde, sandhed + husets løgn
|
||
shared = next(a for a in self.g.answers if a.text == "katte")
|
||
self.assertEqual(sorted(shared.authors), sorted([self.a.id, self.c.id]))
|
||
snap = self.g.snapshot(self.now, viewer=self.a)
|
||
self.assertNotIn(shared.id, [x["id"] for x in snap["answers"]])
|
||
self.assertIn(self.g.question["answer"], texts)
|
||
|
||
def test_cannot_guess_own_lie_and_scoring(self):
|
||
for p, lie in [(self.a, "katte"), (self.b, "hunde"), (self.c, "heste")]:
|
||
self.g.submit_lie(p.id, lie, self.now)
|
||
by_text = {a.text: a.id for a in self.g.answers}
|
||
with self.assertRaises(GameError):
|
||
self.g.guess(self.a.id, by_text["katte"], self.now)
|
||
self.g.guess(self.a.id, self.truth_id(), self.now)
|
||
self.g.guess(self.b.id, by_text["katte"], self.now)
|
||
self.g.guess(self.c.id, by_text["katte"], self.now)
|
||
self.assertEqual(self.g.phase, "reveal")
|
||
# A: rigtigt (1000) + narrede B og C (2 × 500)
|
||
self.assertEqual(self.a.score, 2000)
|
||
self.assertEqual(self.b.score, 0)
|
||
steps = self.g.reveal_steps
|
||
self.assertTrue(steps[-1]["is_truth"])
|
||
self.assertEqual(steps[0]["text"], "katte")
|
||
|
||
def test_like_only_in_reveal_gives_points_and_toggles(self):
|
||
for p, lie in [(self.a, "katte"), (self.b, "hunde"), (self.c, "heste")]:
|
||
self.g.submit_lie(p.id, lie, self.now)
|
||
katte = next(a.id for a in self.g.answers if a.text == "katte")
|
||
with self.assertRaises(GameError): # ikke i gættefasen
|
||
self.g.like(self.b.id, katte)
|
||
for p in (self.a, self.b, self.c):
|
||
self.g.guess(p.id, self.truth_id(), self.now)
|
||
self.assertEqual(self.g.phase, "reveal")
|
||
base = self.a.score
|
||
self.g.like(self.b.id, katte)
|
||
self.assertEqual(self.a.score, base + engine.POINTS_LIKE)
|
||
with self.assertRaises(GameError): # ikke sandheden
|
||
self.g.like(self.b.id, self.truth_id())
|
||
snap = self.g.snapshot(self.now, host=True)
|
||
self.assertEqual(next(x for x in snap["reveal"]["all"] if x["id"] == katte)["likes"], 1)
|
||
self.g.like(self.b.id, katte)
|
||
self.assertEqual(self.a.score, base)
|
||
|
||
def test_lie_for_me_gives_half_points(self):
|
||
suggestion = self.g.lie_for_me(self.a.id)
|
||
self.g.submit_lie(self.a.id, suggestion, self.now)
|
||
self.g.submit_lie(self.b.id, "hunde", self.now)
|
||
self.g.submit_lie(self.c.id, "heste", self.now)
|
||
mine = next(x.id for x in self.g.answers if self.a.id in x.authors)
|
||
hunde = next(x.id for x in self.g.answers if x.text == "hunde")
|
||
self.g.guess(self.b.id, mine, self.now)
|
||
self.g.guess(self.c.id, mine, self.now)
|
||
self.g.guess(self.a.id, hunde, self.now)
|
||
self.assertEqual(self.a.score, engine.POINTS_FOOLED * 2 // 2) # to narret, halve point
|
||
self.assertEqual(self.b.score, engine.POINTS_FOOLED) # egen løgn, fulde point
|
||
all_ = self.g.snapshot(self.now, host=True)["reveal"]["all"]
|
||
self.assertEqual(next(x for x in all_ if x["id"] == mine)["assisted"], [self.a.id])
|
||
|
||
def test_edited_suggestion_counts_as_own_lie(self):
|
||
suggestion = self.g.lie_for_me(self.a.id)
|
||
self.g.submit_lie(self.a.id, suggestion + " måske", self.now)
|
||
self.assertNotIn(self.a.id, self.g.assisted)
|
||
|
||
def test_timeouts_advance_and_fill_with_house_lies(self):
|
||
self.g.submit_lie(self.a.id, "katte", self.now)
|
||
self.assertTrue(self.g.tick(self.now + engine.LIE_SECONDS + 1))
|
||
self.assertEqual(self.g.phase, "guess")
|
||
self.assertGreaterEqual(len(self.g.answers), 4)
|
||
self.assertTrue(any(a.house for a in self.g.answers))
|
||
self.g.tick(self.now + 200)
|
||
self.assertEqual(self.g.phase, "reveal")
|
||
|
||
def test_disconnected_player_does_not_block(self):
|
||
self.g.set_connected(self.c.id, False, self.now)
|
||
self.g.submit_lie(self.a.id, "katte", self.now)
|
||
self.g.submit_lie(self.b.id, "hunde", self.now)
|
||
self.assertEqual(self.g.phase, "guess")
|
||
|
||
def test_audience_votes_give_bonus(self):
|
||
u = self.g.join("Publikum")
|
||
for p, lie in [(self.a, "katte"), (self.b, "hunde"), (self.c, "heste")]:
|
||
self.g.submit_lie(p.id, lie, self.now)
|
||
katte = next(a.id for a in self.g.answers if a.text == "katte")
|
||
self.g.guess(u.id, katte, self.now)
|
||
for p in (self.a, self.b, self.c):
|
||
self.g.guess(p.id, self.truth_id(), self.now)
|
||
self.assertEqual(self.a.score, 1000 + engine.POINTS_AUDIENCE)
|
||
|
||
|
||
class FullGameTests(GameTestCase):
|
||
def test_full_game_reaches_final_with_awards(self):
|
||
players = self.join("A", "B", "C")
|
||
self.g.start(players[0].id, self.now)
|
||
seen_phases = []
|
||
for _ in range(200):
|
||
if self.g.phase == "final":
|
||
break
|
||
seen_phases.append(self.g.phase)
|
||
if self.g.phase == "lie":
|
||
for i, p in enumerate(players):
|
||
self.g.submit_lie(p.id, f"løgn {p.name} {self.g.q_idx} {self.g.round_idx}", self.now)
|
||
elif self.g.phase == "guess":
|
||
for p in players:
|
||
if p.id not in self.g.guesses:
|
||
opts = [a for a in self.g.answers if p.id not in a.authors]
|
||
self.g.guess(p.id, opts[0].id, self.now)
|
||
else:
|
||
self.now += 100
|
||
self.g.tick(self.now)
|
||
self.assertEqual(self.g.phase, "final")
|
||
self.assertEqual(seen_phases.count("lie"), 7)
|
||
self.assertEqual(seen_phases.count("intro"), 7)
|
||
self.assertEqual(seen_phases.count("scores"), 2)
|
||
self.assertEqual(seen_phases.count("category"), 6) # finalen vælger selv
|
||
final = self.g.snapshot(self.now, host=True)["final"]
|
||
self.assertEqual(len(final["ranking"]), 3)
|
||
self.assertTrue(final["awards"])
|
||
self.assertEqual(len(self.g.used_questions), 7)
|
||
self.g.play_again(None, self.now)
|
||
self.assertEqual(self.g.phase, "category")
|
||
self.assertEqual(engine.slugify("Mad og drikke"), "mad_og_drikke")
|
||
self.assertEqual(engine.slugify("Rummet og naturen"), "rummet_og_naturen")
|
||
self.assertTrue(all(p.score == 0 for p in players))
|
||
|
||
def test_kick_chooser_picks_random_category(self):
|
||
a, b, c = self.join("A", "B", "C")
|
||
self.g.start(None, self.now)
|
||
self.g.kick(self.g.chooser_id, self.now)
|
||
self.assertEqual(self.g.phase, "intro")
|
||
self.assertEqual(len(self.g.players), 2)
|
||
|
||
|
||
class PresentAndReactionTests(GameTestCase):
|
||
def _to_round2_lie(self):
|
||
a, b, c = self.join("A", "B", "C")
|
||
self.g.start(None, self.now)
|
||
self.g.round_idx, self.g.q_idx = 1, 0 # spring direkte til runde 2 (oplæsning)
|
||
self.g._begin_question(self.now)
|
||
self.through_category()
|
||
return a, b, c
|
||
|
||
def test_round2_reads_answers_before_guessing(self):
|
||
a, b, c = self._to_round2_lie()
|
||
for p, lie in [(a, "katte"), (b, "hunde"), (c, "heste")]:
|
||
self.g.submit_lie(p.id, lie, self.now)
|
||
self.assertEqual(self.g.phase, "present")
|
||
with self.assertRaises(GameError): # ingen gæt under oplæsningen
|
||
self.g.guess(a.id, self.g.answers[0].id, self.now)
|
||
snap = self.g.snapshot(self.now, viewer=a)
|
||
self.assertEqual(len(snap["present"]["steps_ms"]), len(self.g.answers))
|
||
self.g.tick(self.g.deadline)
|
||
self.assertEqual(self.g.phase, "guess")
|
||
|
||
def test_reactions_are_rate_limited_and_follow_the_featured_answer(self):
|
||
a, b, c = self._to_round2_lie()
|
||
for p, lie in [(a, "katte"), (b, "hunde"), (c, "heste")]:
|
||
self.g.submit_lie(p.id, lie, self.now)
|
||
t = self.g.present_started_at + (self.g.present_intro_ms + 100) / 1000 # første svar i fokus
|
||
first = self.g.answers[0].id
|
||
sent = [self.g.react(b.id, "😂", t) for _ in range(10)]
|
||
self.assertEqual(sum(1 for r in sent if r), engine.REACT_PER_SECOND)
|
||
self.assertEqual(self.g.reactions[first], engine.REACT_PER_SECOND)
|
||
self.assertIsNone(self.g.react(b.id, "💩", t + 2)) # kun de faste emojis
|
||
self.assertIsNotNone(self.g.react(b.id, "🔥", t + 2)) # spanden er tom igen efter et sekund
|
||
|
||
|
||
class RulesTests(GameTestCase):
|
||
def setUp(self):
|
||
super().setUp()
|
||
self.g.show_rules = True
|
||
|
||
def test_rules_before_first_game_only(self):
|
||
a, b = self.join("Asger", "Sus")
|
||
self.g.start(a.id, self.now)
|
||
self.assertEqual(self.g.phase, "rules")
|
||
snap = self.g.snapshot(self.now, viewer=a)
|
||
self.assertEqual(len(snap["rules"]["items"]), len(engine.RULES))
|
||
self.assertNotIn("say", snap["rules"]["items"][0]) # replikken er til speakeren, kortet har sin egen tekst
|
||
self.g.hold_rules(self.now, max_wait=3) # speakeren laver lyd: ingen tidslinje endnu
|
||
self.g.tick(self.now + 3) # lyden blev aldrig klar: kortene vises uden stemme
|
||
self.assertEqual(self.g.phase, "rules")
|
||
self.g.tick(self.g.deadline)
|
||
self.assertEqual(self.g.phase, "category")
|
||
# spil igen: ingen regler
|
||
self.g.phase = "final"
|
||
self.g.play_again(a.id, self.now)
|
||
self.assertEqual(self.g.phase, "category")
|
||
|
||
|
||
class SanitizeTests(GameTestCase):
|
||
def test_lies_are_cleaned_and_lowercased(self):
|
||
a, b = self.join("Asger", "Sus")
|
||
self.g.start(a.id, self.now)
|
||
self.through_category()
|
||
with self.assertRaises(GameError) as e:
|
||
self.g.submit_lie(a.id, "😂🔥", self.now) # kun emojis: intet tilbage
|
||
self.assertEqual(e.exception.code, "lie_invalid")
|
||
with self.assertRaises(GameError):
|
||
self.g.submit_lie(a.id, "x" * (engine.MAX_LIE + 1), self.now)
|
||
self.g.submit_lie(a.id, " Røde Krager 🐦!! ", self.now)
|
||
self.assertEqual(self.g.lies[a.id], "røde krager")
|
||
|
||
def test_sanitize_and_names(self):
|
||
self.assertEqual(engine.sanitize("Rock'n'roll & co."), "Rock'n'roll & co")
|
||
self.assertEqual(engine.sanitize("full width 12"), "full width 12")
|
||
self.assertEqual(engine.sanitize("a\tb\nc"), "a b c")
|
||
p = self.g.join("Bo 👑")
|
||
self.assertEqual(p.name, "Bo")
|
||
with self.assertRaises(GameError) as e:
|
||
self.g.join("🎉🎉")
|
||
self.assertEqual(e.exception.code, "name_invalid")
|
||
|
||
|
||
class FestTests(GameTestCase):
|
||
def setUp(self):
|
||
super().setUp()
|
||
self.g = Game("TEST", bank(), join_url="http://x/?code=TEST", rng=random.Random(4), fest=True)
|
||
self.a, self.b, self.c = self.join("A", "B", "C")
|
||
self.g.start(None, self.now)
|
||
self.through_category()
|
||
|
||
def test_fooled_players_drink(self):
|
||
for p, lie in [(self.a, "katte"), (self.b, "hunde"), (self.c, "heste")]:
|
||
self.g.submit_lie(p.id, lie, self.now)
|
||
by_text = {a.text: a.id for a in self.g.answers}
|
||
self.g.guess(self.a.id, self.truth_id(), self.now)
|
||
self.g.guess(self.b.id, by_text["katte"], self.now)
|
||
self.g.guess(self.c.id, by_text["katte"], self.now)
|
||
self.assertEqual((self.a.sips, self.b.sips, self.c.sips), (0, 1, 1))
|
||
self.assertEqual(self.a.score, 2000) # pointene findes stadig
|
||
snap = self.g.snapshot(self.now, viewer=self.b)
|
||
self.assertTrue(snap["fest"])
|
||
self.assertEqual(snap["me"]["last_sips"], 1)
|
||
self.assertEqual({d["player_id"]: d["n"] for d in snap["reveal"]["sip_deltas"]}[self.b.id], 1)
|
||
|
||
def test_nobody_found_truth_everyone_drinks(self):
|
||
for p, lie in [(self.a, "katte"), (self.b, "hunde"), (self.c, "heste")]:
|
||
self.g.submit_lie(p.id, lie, self.now)
|
||
by_text = {a.text: a.id for a in self.g.answers}
|
||
self.g.guess(self.a.id, by_text["hunde"], self.now)
|
||
self.g.guess(self.b.id, by_text["katte"], self.now)
|
||
self.g.guess(self.c.id, by_text["katte"], self.now)
|
||
truth = next(s for s in self.g.reveal_steps if s["is_truth"])
|
||
self.assertTrue(truth["everyone_drinks"])
|
||
self.assertEqual((self.a.sips, self.b.sips, self.c.sips), (2, 2, 2))
|
||
|
||
def test_fest_rules_and_no_sips_in_normal_games(self):
|
||
self.assertIs(self.g.rules, engine.RULES_FEST)
|
||
normal = Game("N", bank())
|
||
self.assertIs(normal.rules, engine.RULES)
|
||
self.assertFalse(normal.snapshot(self.now)["fest"])
|
||
|
||
def test_fest_has_its_own_emojis(self):
|
||
for p, lie in [(self.a, "katte"), (self.b, "hunde"), (self.c, "heste")]:
|
||
self.g.submit_lie(p.id, lie, self.now)
|
||
self.assertEqual(self.g.phase, "guess")
|
||
self.assertEqual(self.g.snapshot(self.now)["reactions"], list(engine.REACTIONS_FEST))
|
||
self.assertIsNotNone(self.g.react(self.a.id, "💀", self.now))
|
||
self.assertIsNone(self.g.react(self.a.id, "😂", self.now + 2)) # familie-sættet gælder ikke her
|
||
normal = Game("N", bank())
|
||
self.assertEqual(normal.snapshot(self.now)["reactions"], list(engine.REACTIONS))
|