CI / test (push) Successful in 28s
Faseskift venter, til speakeren er færdig med den aktuelle skærm, og replikker til en skærm, der er væk, afspilles ikke. Ny kategori-intro med en lyd pr. kategori, musik pr. fase med bløde overgange og lydeffekter til alt, der flyver over skærmen. Efter afsløringen eksploderer løgne, ingen valgte, og pointene tæller op. Hjerter gives kun under afsløringen og ses live på storskærmen. Lyv for mig giver halve point. Narrede spilleres telefon kan blive drillet med et fjollet billede og et buh. Alle lyd- og billedfiler serveres med versionerede navne og caches af browseren. Claude-Session: https://claude.ai/code/session_01JmhJG6JnhXfzNiKv8f2vHu
232 lines
10 KiB
Python
232 lines
10 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
|
||
|
||
|
||
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)
|