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))