Spionen: rettelser fra review og QA, og bots

Replikker:
- To spioner siges i flertal ("Spionerne var ..."), også i den generiske
  udgave; fanges den ene, og den anden slipper, er det en halv sejr
  ("Den ene spion er fanget, men den anden slap væk"), ikke "Agenterne
  vinder". Storskærmens overskrift følger med.
- Er spionen gået i løbet af runden, siges "Spionen forlod missionen", og
  navnet står stadig i afsløringen (motoren husker spionernes navne fra
  rundens start; storskærmen viser dem uden farve).
- Fire små replikker skrevet om (vedkommende, "Måske er det ansigtet",
  skarpeste øje = flest rigtige stemmer, vinderen gentog ikke den faste).

Ord: Spionen bruger ikke ord under tre bogstaver ("is", "fe"); de står inde
i for mange andre ord (isvaffel), til at stikordene kan holdes fri af dem.
Mad har 17 ord, Eventyr 15.

Test: speaker-testen kører uden rigtige ventetider (37 s til 4 s for hele
filen); skift-testen venter på det nye spils lobby i stedet for at gætte
på tiden.

deploy/bots.py --game spion: tilfældige stikord (prøver et andet, hvis det
afvises), tilfældige stemmer og gæt; værten forkorter snakken. En stemme,
der kommer et øjeblik efter, at fasen sluttede, er ikke en fejl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
agw
2026-10-20 21:59:54 +02:00
co-authored by Claude Opus 5.5
parent d5d60ad75d
commit 5b24404eac
9 changed files with 175 additions and 21 deletions
+72 -5
View File
@@ -11,6 +11,10 @@ faser blev nået og slutstillingen blev vist.
Tegn og Gæt (mindst 3 spillere):
python deploy/bots.py --base http://127.0.0.1:8000 --game tegn --players 5
Bots tegner tilfældige streger og gætter tilfældige ord.
Spionen (mindst 4 spillere):
python deploy/bots.py --base http://127.0.0.1:8000 --game spion --players 10
Bots giver tilfældige stikord, stemmer tilfældigt, og en anklaget spion gætter tilfældigt. Værten forkorter snakken.
"""
import argparse
@@ -60,6 +64,8 @@ async def host(ws_base, code, seen: list, done: asyncio.Event):
st = msg["state"]
if not seen or seen[-1] != st["phase"]:
seen.append(st["phase"])
if st.get("game") == "spion" and st["phase"] == "talk": # værten forkorter snakken (bots snakker ikke)
await ws.send(json.dumps({"action": "skip"}))
extra = st.get("question", {}).get("prompt", "") if st["phase"] == "lie" else ""
print(f"[vært] {st['phase']:8} {extra}")
if st["phase"] == "final":
@@ -74,7 +80,8 @@ async def host(ws_base, code, seen: list, done: asyncio.Event):
async def player(ws_base, code, name, n_players, done: asyncio.Event, rng: random.Random, stop_at_final=False, game="fof"):
try:
await (_tegn_player if game == "tegn" else _player)(ws_base, code, name, n_players, done, rng, stop_at_final)
play = {"tegn": _tegn_player, "spion": _spion_player}.get(game, _player)
await play(ws_base, code, name, n_players, done, rng, stop_at_final)
except websockets.ConnectionClosed:
print(f"[{name}] forbindelsen blev lukket (findes rummet?)")
done.set()
@@ -211,6 +218,64 @@ async def _tegn_player(ws_base, code, name, n_players, done, rng, stop_at_final)
await ws.send(json.dumps({"action": "react", "emoji": rng.choice(st.get("reactions") or ["😂"])}))
SPION_CLUES = ["rund", "lille", "stor", "blød", "hård", "rød", "grøn", "kold", "varm", "hurtig", "langsom", "farlig",
"sjov", "gammel", "ny", "tung", "let", "lang", "kort", "våd", "tør", "glat", "skarp", "larm", "stille",
"dyr", "billig", "udenfor", "indenfor", "sommer", "vinter", "morgen", "aften", "hjem", "skole"]
async def _spion_player(ws_base, code, name, n_players, done, rng, stop_at_final):
async with websockets.connect(f"{ws_base}/ws/play/{code}/") as ws:
await ws.send(json.dumps({"action": "join", "name": name}))
acted, state = set(), None
while not done.is_set():
try:
raw = await asyncio.wait_for(ws.recv(), timeout=1)
except asyncio.TimeoutError:
continue
msg = json.loads(raw)
if msg["type"] == "error":
if msg["code"] in ("clue_is_word", "clue_is_theme", "clue_taken", "clue_invalid") and state:
# afvist stikord: prøv et andet (det er en del af spillet, ikke en fejl)
await ws.send(json.dumps({"action": "submit_clue", "text": rng.choice(SPION_CLUES)}))
continue
if msg["code"] == "wrong_phase":
continue # stemte et øjeblik efter, at alle andre havde stemt: helt normalt
print(f"[{name}] fejl: {msg['code']} {msg['message']}")
if msg["code"] == "unknown_room":
done.set()
return
continue
if msg["type"] != "state":
continue
state = st = msg["state"]
me = st["me"]
if st["phase"] == "final" and stop_at_final:
done.set()
return
key = (st["phase"], (st.get("round") or {}).get("number"))
pause = lambda: asyncio.sleep(rng.uniform(0.05, 0.4)) # noqa: E731
if st["phase"] == "lobby" and me["vip"] and len(st["players"]) >= n_players and key not in acted:
acted.add(key)
await pause()
await ws.send(json.dumps({"action": "start"}))
elif st["phase"] == "clue" and me.get("card") and not me.get("clue") and key not in acted:
acted.add(key)
await pause()
await ws.send(json.dumps({"action": "submit_clue", "text": rng.choice(SPION_CLUES)}))
elif st["phase"] == "vote" and not me.get("vote") and key not in acted:
options = [p["id"] for p in st["players"] if p["in_round"] and p["id"] != me["id"]]
if options:
acted.add(key)
await pause()
await ws.send(json.dumps({"action": "vote", "player_id": rng.choice(options)}))
elif st["phase"] == "guess" and me.get("can_guess") and key not in acted:
acted.add(key)
await pause()
await ws.send(json.dumps({"action": "spy_guess", "text": rng.choice(SPION_CLUES)}))
if st["phase"] in ("talk", "result") and rng.random() < 0.2:
await ws.send(json.dumps({"action": "react", "emoji": rng.choice(st.get("reactions") or ["😂"])}))
async def main():
ap = argparse.ArgumentParser()
ap.add_argument("--base", default="http://127.0.0.1:8000")
@@ -219,7 +284,8 @@ async def main():
ap.add_argument("--seed", type=int, default=None)
ap.add_argument("--pack", default="alle", help="spørgsmålspakke (alle, familie, nørd, arbejde, fest)")
ap.add_argument("--code", help="join et eksisterende rum i stedet for at oprette et (du er selv vært/VIP)")
ap.add_argument("--game", default="fof", choices=["fof", "tegn"], help="spillet (fof = Fup og Fakta, tegn = Tegn og Gæt)")
ap.add_argument("--game", default="fof", choices=["fof", "tegn", "spion"],
help="spillet (fof = Fup og Fakta, tegn = Tegn og Gæt, spion = Spionen)")
args = ap.parse_args()
rng = random.Random(args.seed)
if args.code:
@@ -236,7 +302,7 @@ async def main():
await asyncio.sleep(0.1)
# Publikum joiner, når spillet er i gang (i lobbyen ville de blive spillere).
for _ in range(200):
if "category" in seen or "theme" in seen:
if "category" in seen or "theme" in seen or "clue" in seen:
break
await asyncio.sleep(0.1)
for i in range(args.audience):
@@ -248,8 +314,9 @@ async def main():
return 1
for t in tasks:
t.cancel()
needed = ({"lobby", "rules", "theme", "word", "draw", "guess", "reveal", "final"} if args.game == "tegn" else
{"lobby", "rules", "category", "lie", "present", "guess", "reveal", "scores", "final"})
needed = {"tegn": {"lobby", "rules", "theme", "word", "draw", "guess", "reveal", "final"},
"spion": {"lobby", "rules", "clue", "clues", "talk", "vote", "verdict", "result", "final"},
}.get(args.game, {"lobby", "rules", "category", "lie", "present", "guess", "reveal", "scores", "final"})
missing = needed - set(seen)
if missing:
print(f"Faser der aldrig blev nået: {missing}", file=sys.stderr)