CI / test (push) Successful in 39s
Ny spørgsmålspakke "fest" (aldrig med i "Alle"): 43 faktatjekkede spørgsmål om dansk pop i to kategorier, Fest: retro og Fest: nu. Et fest-spil tæller tår ved siden af point: narret giver en tår, ingen fandt sandheden giver alle en tår, gange rundens faktor (finalen er en shot). Afsløringen, opsummeringen, stillingen og telefonen viser tårene, og der er en pris til aftenens tørstigste. Speakeren bruger zoomer-replikker (<begivenhed>_fest), egne regler og FEST_VOICE_ID til alt. Claude-Session: https://claude.ai/code/session_01JmhJG6JnhXfzNiKv8f2vHu
364 lines
16 KiB
Python
364 lines
16 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"])
|