Files
weirsoe-party-protocol/spionen/tests/test_engine.py
T
agwandClaude Opus 5.5 ccb925b89a Spionen: et stikord må ikke indeholde ordet
Fra QA: sammensatte ord med ordet i (pingvinunge, hundehus, pizzabager,
togskinne) blev godtaget, og så stod ordet på storskærmen for spionen.
Nu afvises et stikord fra en, der kender ordet, hvis ordet står inde i det
(ord på tre bogstaver eller flere; "is" ville ellers ramme alt for meget).
Prisen er, at et andet ord, der tilfældigvis indeholder ordet, også
afvises ("hvalp" til "hval"); en lækage er værre end en afvisning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 21:59:53 +02:00

360 lines
15 KiB
Python

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