"""Spionens motor: roller, stikord, afstemning, spionens gæt, point og at hemmeligheden holder.""" import json import random from unittest import TestCase from fupogfakta.engine import GameError from spionen import engine from spionen.engine import Game, same_word THEMES = [ {"id": "dyr", "name": "Dyr", "icon": "🐘", "words": ["kat", "pingvin", "kænguru", "isbjørn", "pindsvin"]}, {"id": "koekken", "name": "Ting i køkkenet", "icon": "🍳", "words": ["pande", "gaffel", "ovn"]}, ] NAMES = ["Asger", "Sus", "Bo", "Mette", "Kasper", "Line", "Jens", "Ida", "Ole", "Pia"] def game(n=4, seed=1, rounds=4, themes=THEMES): g = Game("TEST", themes, rng=random.Random(seed), rounds=rounds) g.show_rules = False ps = [g.join(NAMES[i]) for i in range(n)] g.start(None, 0) return g, ps def play_clues(g, now=1): """Alle skriver et stikord, der ikke røber ordet (spionerne et tilfældigt).""" for i, pid in enumerate(g.order): g.submit_clue(pid, f"stikord{chr(97 + i)}", now) def to_vote(g, now=2): if g.phase == "clue": play_clues(g, now) while g.phase in ("clues", "talk"): g.skip(now) assert g.phase == "vote" class WordTests(TestCase): def test_inflections_case_spaces_and_hyphens(self): for form in ("KAT", "katten", "katte", "kattene", "kattens", "kat-ten", " Kat "): self.assertTrue(same_word(form, "kat"), form) for form in ("panden", "pander", "panderne", "pandens"): self.assertTrue(same_word(form, "pande"), form) self.assertTrue(same_word("is bjørn", "isbjørn")) self.assertTrue(same_word("isbjørnen", "isbjørn")) self.assertTrue(same_word("kænguruen", "kænguru")) self.assertFalse(same_word("katteskål", "kat")) # sammensatte ord er nye ord self.assertFalse(same_word("hund", "kat")) def test_small_typos_scaled_by_length(self): self.assertTrue(same_word("pingvn", "pingvin")) # en fejl i et ord på syv self.assertTrue(same_word("pindsvn", "pindsvin")) self.assertTrue(same_word("pndsvn", "pindsvin")) # to fejl i et ord på otte self.assertTrue(same_word("kroodile", "krokodille")) self.assertFalse(same_word("pinsvni", "pingvin")) # tre fejl self.assertFalse(same_word("kas", "kat")) # ingen stavefejl i korte ord self.assertFalse(same_word("katten", "kat", typos=False) is False) # bøjning gælder uden stavefejl self.assertFalse(same_word("pingvn", "pingvin", typos=False)) class RoleTests(TestCase): def test_one_spy_and_two_from_eight(self): for n, spies in ((4, 1), (7, 1), (8, 2), (10, 2)): g, _ = game(n) self.assertEqual(len(g.spies), spies, n) self.assertEqual(len(set(g.spies)), spies) def test_nobody_is_spy_twice_in_a_row(self): for seed in range(30): g, _ = game(4, seed=seed, rounds=5) seen = [] while g.phase != "final": if g.phase == "clue": seen.append(list(g.spies)) g.skip(10) for a, b in zip(seen, seen[1:]): self.assertFalse(set(a) & set(b), (seed, seen)) def test_words_and_themes_vary(self): g, _ = game(4, rounds=5) words, themes = [], [] while g.phase != "final": if g.phase == "clue": words.append(g.word) themes.append(g.theme["id"]) g.skip(10) self.assertEqual(len(words), len(set(words))) self.assertTrue(all(a != b for a, b in zip(themes, themes[1:]))) # to temaer: skiftevis class ClueTests(TestCase): def setUp(self): self.g, self.ps = game(4) self.g.word, self.g.theme = "kat", THEMES[0] self.agent = next(pid for pid in self.g.order if pid not in self.g.spies) self.spy = self.g.spies[0] def bad(self, pid, text, code): with self.assertRaises(GameError) as e: self.g.submit_clue(pid, text, 1) self.assertEqual(e.exception.code, code) def test_word_and_inflections_are_refused_but_near_words_are_fine(self): for text in ("kat", "Katten", "kattene", "kattekilling", "missekat"): self.bad(self.agent, text, "clue_is_word") self.g.word = "hund" self.bad(self.agent, "hundehus", "clue_is_word") # sammensat: ordet stod på skærmen self.g.word = "is" self.g.submit_clue(self.agent, "vis", 1) # korte ord: kun ordet selv og bøjninger self.g.word = "hval" self.bad(self.agent, "hvalp", "clue_is_word") # ordet står i det; prisen for at fange "hvalfangst" self.g.word = "slot" self.g.submit_clue(self.agent, "plot", 1) def test_theme_spaces_length_and_duplicates(self): self.bad(self.agent, "dyr", "clue_is_theme") self.bad(self.agent, "dyrene", "clue_is_theme") self.bad(self.agent, "to ord", "clue_invalid") self.bad(self.agent, "x" * 21, "clue_invalid") self.bad(self.agent, "", "clue_invalid") self.g.submit_clue(self.agent, "Mjav", 1) other = next(pid for pid in self.g.order if pid not in (self.agent, self.spy)) self.bad(other, "mjav", "clue_taken") self.g.submit_clue(self.agent, "spinder", 1) # sit eget må man skifte self.g.submit_clue(other, "mjav", 1) def test_the_spy_is_never_told_the_word_or_the_others_clues(self): self.g.submit_clue(self.spy, "kat", 1) # spionen ramte ordet: ingen afvisning, ingen lækage self.assertEqual(self.g.clues[self.spy], "kat") self.g.submit_clue(self.agent, "hav", 1) self.g.submit_clue(self.spy, "hav", 1) # en andens stikord: heller ingen afvisning self.assertEqual(self.g.clues[self.spy], "hav") self.bad(self.spy, "dyr", "clue_is_theme") def test_audience_and_wrong_phase(self): u = self.g.join("Sene") # kom efter start: publikum self.assertEqual(u.role, "audience") self.bad(u.id, "mjav", "not_in_round") play_clues(self.g) self.bad(self.agent, "mjav", "wrong_phase") class VoteAndPointsTests(TestCase): def round_to(self, n=4, seed=1): g, ps = game(n, seed=seed) to_vote(g) return g, ps def test_no_self_vote_and_audience_counts_separately(self): g, ps = self.round_to() a = g.order[0] with self.assertRaises(GameError): g.vote(a, a, 3) u = g.join("Sene") g.vote(u.id, g.spies[0], 3) self.assertEqual(g.audience_votes, {u.id: g.spies[0]}) self.assertNotIn(u.id, g.votes) def test_majority_catches_the_spy_who_then_guesses(self): g, ps = self.round_to() spy = g.spies[0] agents = [pid for pid in g.order if pid != spy] for pid in agents: g.vote(pid, spy, 3) g.vote(spy, agents[0], 3) # alle har stemt: videre med det samme self.assertEqual((g.phase, g.accused), ("verdict", spy)) g.skip(4) self.assertEqual(g.phase, "guess") with self.assertRaises(GameError): g.spy_guess(agents[0], g.word, 5) # kun spionen gætter g.spy_guess(spy, g.word.upper() + "en", 5) self.assertEqual(g.phase, "result") self.assertTrue(g.result["guess"]["correct"] and g.result["spy_won"]) self.assertEqual(g.players[spy].score, engine.POINTS_SPY_GUESS) self.assertTrue(all(g.players[pid].score == engine.POINTS_VOTE for pid in agents)) def test_caught_spy_who_misses_gets_nothing(self): g, ps = self.round_to() spy = g.spies[0] for pid in g.order: g.vote(pid, spy if pid != spy else next(x for x in g.order if x != spy), 3) g.skip(4) g.spy_guess(spy, "noget helt andet", 5) self.assertFalse(g.result["spy_won"]) self.assertEqual(g.players[spy].score, 0) def test_tie_lets_the_spy_go(self): g, ps = self.round_to() spy = g.spies[0] agents = [pid for pid in g.order if pid != spy] g.vote(agents[0], spy, 3) g.vote(agents[1], agents[2], 3) # 1-1: lighed g.skip(3) self.assertIsNone(g.accused) g.skip(4) # ingen anklaget: intet gæt self.assertEqual(g.phase, "result") self.assertEqual(g.players[spy].score, engine.POINTS_SPY_FREE) self.assertEqual(g.players[agents[0]].score, engine.POINTS_VOTE) # stemte stadig rigtigt self.assertEqual(g.players[agents[1]].score, 0) def test_no_votes_at_all(self): g, ps = self.round_to() g.skip(3) g.skip(4) self.assertEqual((g.phase, g.accused), ("result", None)) self.assertEqual(g.players[g.spies[0]].score, engine.POINTS_SPY_FREE) def test_innocent_accused(self): g, ps = self.round_to() spy = g.spies[0] agents = [pid for pid in g.order if pid != spy] for pid in g.order: g.vote(pid, agents[0] if pid != agents[0] else agents[1], 3) g.skip(4) self.assertEqual(g.phase, "result") # ikke spion: intet gæt self.assertEqual(g.players[spy].score, engine.POINTS_SPY_FREE) self.assertEqual(g.result["caught"], None) def test_two_spies(self): """Den anklagede spion er fanget og gætter; den anden slap. En stemme på en af dem er rigtig.""" g, ps = self.round_to(8) s1, s2 = g.spies agents = [pid for pid in g.order if pid not in g.spies] for i, pid in enumerate(agents): g.vote(pid, s1 if i < 4 else s2, 3) g.vote(s1, agents[0], 3) g.vote(s2, agents[0], 3) self.assertEqual(g.accused, s1) g.skip(4) self.assertEqual(g.phase, "guess") g.skip(5) # tiden løb ud self.assertEqual(g.players[s1].score, 0) self.assertEqual(g.players[s2].score, engine.POINTS_SPY_FREE) self.assertTrue(all(g.players[pid].score == engine.POINTS_VOTE for pid in agents)) def test_whole_game_ends_with_podium_and_awards(self): g, ps = game(5, rounds=3) rounds = 0 while g.phase != "final": if g.phase == "clue": rounds += 1 if g.phase == "vote": spy = g.spies[0] for pid in g.order: g.vote(pid, spy if pid != spy else next(x for x in g.order if x != spy), 3) continue g.skip(10) self.assertEqual(rounds, 3) fin = g.final_result self.assertEqual(len(fin["ranking"]), 5) self.assertIn("sharp", [a["kind"] for a in fin["awards"]]) self.assertIn("suspected", [a["kind"] for a in fin["awards"]]) class NeverHangTests(TestCase): def test_missing_clues_and_votes_just_time_out(self): g, _ = game(4) for phase in ("clue", "clues", "talk", "vote", "verdict"): self.assertEqual(g.phase, phase) self.assertTrue(g.tick(g.deadline)) self.assertEqual(g.phase, "result") self.assertIn(None, [c["text"] for c in g._clue_items()]) # ingen stikord: står tomt def test_disconnected_players_do_not_hold_the_round(self): g2, _ = game(4) g2.set_connected(g2.order[3], False, 1) for pid in g2.order[:3]: g2.submit_clue(pid, f"ord{pid}", 1) self.assertEqual(g2.phase, "clues") # de forbundne er færdige def test_accused_spy_leaves_during_guess(self): g, _ = game(4) to_vote(g) spy = g.spies[0] for pid in g.order: g.vote(pid, spy if pid != spy else next(x for x in g.order if x != spy), 3) g.skip(4) self.assertEqual(g.phase, "guess") g.kick(spy, 5) self.assertEqual(g.phase, "result") def test_too_few_players_ends_the_game(self): g, _ = game(4) g.kick(g.order[0], 1) while g.phase not in ("final", "lobby"): g.skip(10) self.assertEqual(g.phase, "final") # runden blev spillet færdig; ingen ny med tre def test_everyone_leaves(self): g, _ = game(4) for pid in list(g.players): g.kick(pid, 1) self.assertEqual(g.phase, "lobby") class SecrecyTests(TestCase): """Ordet kun til dem, der kender det; spionen ingen steder før resultatet. Hvert snapshot i hver fase læses.""" def snaps(self, g): out = {"host": g.snapshot(5, host=True)} for p in list(g.players.values()) + list(g.audience.values()): out[p.id] = g.snapshot(5, viewer=p) return out def test_every_snapshot_in_every_phase(self): g, ps = game(8, seed=4) u = g.join("Sene") checked = set() while g.phase != "final": snaps = self.snaps(g) word, spies = g.word, set(g.spies) for who, st in snaps.items(): text = json.dumps(st, ensure_ascii=False).lower() with self.subTest(phase=g.phase, who=who): if g.phase == "result": self.assertIn(word, text) continue self.assertNotIn("spies\": [", text) # aldrig en liste over spionerne knows = who in g.order and who not in spies if not knows: # spionen, publikum og storskærmen self.assertNotIn(f"\"{word}\"", text) self.assertNotIn(word, json.dumps(st.get("me", {}), ensure_ascii=False).lower()) else: self.assertEqual(st["me"]["card"]["word"], word) card = (st.get("me") or {}).get("card") if card: self.assertEqual(card["spy"], who in spies) # kun ens egen rolle accused_is_spy = (st.get("votes") or {}).get("accused_is_spy") if accused_is_spy: self.assertIn(g.phase, ("verdict", "guess")) self.assertIn(st["votes"]["accused"], spies) if g.phase == "verdict": self.assertEqual(who, "host") # telefonerne først i "guess" checked.add(g.phase) if g.phase == "clue": play_clues(g) continue if g.phase == "vote": g.vote(u.id, g.spies[0], 3) # publikum stemmer for sjov for pid in g.order: g.vote(pid, g.spies[0] if pid != g.spies[0] else g.spies[1], 3) continue g.skip(10) self.assertLessEqual({"clue", "clues", "talk", "vote", "verdict", "guess", "result"}, checked) def test_clue_order_is_shuffled_and_phones_wait_for_the_tv(self): g, ps = game(6, seed=2) play_clues(g) self.assertEqual(g.phase, "clues") orders = set() for seed in range(8): h, _ = game(6, seed=seed) play_clues(h) orders.add(tuple(h.clue_order.index(pid) for pid in h.order)) self.assertGreater(len(orders), 1) self.assertEqual(g.snapshot(5, viewer=ps[0])["clues"]["items"], []) # telefonerne kigger på TV'et self.assertEqual(len(g.snapshot(5, host=True)["clues"]["items"]), 6)