"""The project's referee, on kaggle_environments 1.33.0's seven-player Werewolf preset: two werewolves against a seer, a doctor and three villagers, nights of killing, healing and inspecting, days of talk and votes. Version 2 privately shuffles each deal. check: in three balanced rounds, the route's bot takes every seat and role and the opponent fills the other six; the score is the share of games the route's team wins, each role a subtask (over the field). practice: one seed against each opponent, the route as a werewolf and as the seer, a replay of each in replays/. python3 -s referee/play.py check|practice """ import json, os, random, sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.join(ROOT, 'kit')) sys.path.insert(0, os.path.join(ROOT, 'referee')) import league # noqa: E402 from kaggle_environments import make # noqa: E402 from kaggle_seat import seat # noqa: E402 N, SEEDS = 7, [3, 5, 8] ROLES = ['Werewolf', 'Werewolf', 'Seer', 'Doctor', 'Villager', 'Villager', 'Villager'] def configuration(seed, mine=None, rng=None): """Independent private deal; candidate covers each role once per seven games. Gameplay seeds alone must not reconstruct the hidden assignment. The test helper may inject a local RNG for repeatable audits, never a player input. """ rng = rng or random.SystemRandom() roles = list(ROLES) if mine is None: rng.shuffle(roles) else: fixed = roles.pop(mine) rng.shuffle(roles) roles.insert(mine, fixed) return {'seed': seed, 'agents': [{'id': f'Player {i + 1}', 'role': r, 'agent_id': 'seat'} for i, r in enumerate(roles)]} GOD = 'MODERATOR' NIGHT_ORDER = {'heal': 0, 'inspect': 1} # as a judge calls them: the doctor, the seer, then the wolves class Story: """The game as the viewer draws it, from the engine's god-view records (env.info MODERATOR_OBSERVATION, one list per step), so nothing downstream reads English. Seats are 0-6 (Player 1-7); a public moderator line rides on its event as en. Events: phase {phase: night|day, day} the engine counts night 0, day 1, night 1, day 2 ... heal {actor, target} / inspect {actor, target, role, team} / wolf_vote {actor, target} / wolf_pick {target, votes, counts, tie} / saved {target} the night, private: the doctor, the seer, the wolves dawn {killed} after "day N begins": who died in the night, or None out {who, cause: night|exile, role, team, day} every elimination, its role as the judge reveals it talk {order} / speech {actor, text} the day's speaking order, then each speech vote_start {order} / vote {actor, target} target None is an abstention tally {votes, counts, exiled, tie} the day vote: who voted whom, the counts, who goes (tie: the seats level at the top, one drawn at random) over {winner, reason, roles, out} the end: every role and every elimination actions carry the actor's private reasoning as why.""" def __init__(self, ids): self.seat = {p: i for i, p in enumerate(ids)} self.roles, self.teams = [None] * len(ids), [None] * len(ids) self.gone, self.votes, self.order, self.dawn, self.en, self.peek = [], {}, None, [], [], {} def at(self, pid): return self.seat.get(pid) def step(self, t, records): out = [] def put(kind, rec=None, **kv): e = {'t': t, 'kind': kind, **kv} if rec is not None and rec.get('public') and rec.get('source') == GOD: e['en'] = '\n'.join(self.en + [rec['description']]) self.en = [] out.append(e) return e for r in records: r = json.loads(r['json_str']) name, d, desc = r['event_name'], r.get('data') or {}, r['description'] if name == 'game_start' and 'role' in d: s = self.at(d['player_id']) self.roles[s], self.teams[s] = d['role'], d['team'] elif name == 'night_start': self.votes = {} put('phase', r, phase='night', day=r['day']) elif name == 'heal_action': put('heal', actor=self.at(d['actor_id']), target=self.at(d['target_id']), why=d.get('reasoning')) elif name == 'inspect_action': self.peek[d['actor_id']] = d.get('reasoning') elif name == 'inspect_result': put('inspect', actor=self.at(d['actor_id']), target=self.at(d['target_id']), role=d.get('role'), team=d.get('team'), why=self.peek.pop(d['actor_id'], None)) elif name == 'vote_action' and r.get('source') != GOD: # a ballot (no data: a no-op, an abstention) voter = self.at(r['source']) target = self.at(d.get('target_id')) if 'target_id' in d else None self.votes[voter] = target put('wolf_vote' if r['phase'] == 'Night' else 'vote', actor=voter, target=target, why=d.get('reasoning')) elif name == 'vote_result' and 'elected_target_player_id' in d: put('wolf_pick', target=self.at(d['elected_target_player_id']), **self.count()) self.votes = {} elif name == 'heal_result': put('saved', target=self.at(d['saved_player_id'])) elif name == 'elimination' and 'eliminated_player_id' in d: s = self.at(d['eliminated_player_id']) self.dawn = [('dawn', r, {'killed': s}), ('out', None, self.fall(s, 'night', r['day']))] elif name == 'moderator_announcement' and r['public'] and r['phase'] == 'Night' and not r.get('data'): self.dawn = [('dawn', r, {'killed': None})] # "Last night, no one was eliminated." elif name == 'day_start': put('phase', r, phase='day', day=r['day']) for k, rec, kv in self.dawn: put(k, rec, **kv) self.dawn = [] elif name == 'discussion_order': put('talk', r, order=[self.at(p) for p in d.get('chat_order_of_player_ids') or []]) elif name == 'discussion' and 'message' in d and r.get('source') != GOD: put('speech', actor=self.at(r['source']), text=d['message'], why=d.get('reasoning')) elif name == 'vote_order' and r['phase'] == 'Day': self.order = [self.at(p) for p in d.get('vote_order_of_player_ids') or []] elif name == 'moderator_announcement' and 'voting_rule' in d: self.votes = {} put('vote_start', r, order=self.order) elif name == 'elimination' and 'elected_player_id' in d: s = self.at(d['elected_player_id']) put('tally', **self.count(), exiled=s) self.votes = {} put('out', r, **self.fall(s, 'exile', r['day'])) elif name == 'vote_result' and r['public']: # no exile (a tie under a no-exile rule) put('tally', r, **self.count(), exiled=None) self.votes = {} elif name == 'game_end': for k, rec, kv in self.dawn: # the wolves can win in the night put(k, rec, **kv) self.dawn = [] put('over', r, **self.over(d.get('winner_team'), d.get('reason'))) elif r.get('public') and r.get('source') == GOD: self.en.append(desc) # the rule lines: told with the next event out.sort(key=lambda e: NIGHT_ORDER.get(e['kind'], 2)) return out def count(self): counts = {} for v in self.votes.values(): if v is not None: counts[v] = counts.get(v, 0) + 1 top = [s for s, c in counts.items() if c == max(counts.values())] if counts else [] return {'votes': dict(self.votes), 'counts': counts, 'tie': top if len(top) > 1 else None, 'all_abstained': bool(self.votes) and not counts} def fall(self, s, cause, day): e = {'who': s, 'cause': cause, 'role': self.roles[s], 'team': self.teams[s], 'day': day} self.gone.append(e) return e def over(self, winner, reason): return {'winner': winner, 'reason': (reason or '').replace('Reason: ', ''), 'roles': self.roles, 'out': self.gone} def transcript(j, names, kinds=None): """The replay: the players with their roles and fates, and the game as Story's events (see Story).""" ids = [a['id'] for a in j['configuration']['agents']] story, events = Story(ids), [] for t, records in enumerate(j['info'].get('MODERATOR_OBSERVATION') or []): events += story.step(t, records) over = next((e for e in events if e['kind'] == 'over'), None) fate = {e['who']: e for e in story.gone} return {'game': 'werewolf', 'version': 2, 'players': [{'seat': i, 'id': ids[i], 'name': names[i], 'kind': (kinds or {}).get(i, 'bot'), 'role': story.roles[i], 'team': story.teams[i], 'out': fate.get(i)} for i in range(len(ids))], 'events': events, 'winner': over and over['winner'], 'reason': over and over['reason']} def main(mode, route): name = league.route_name() reps = league.Replays() if mode == 'practice' else None me = seat(league.bot_dir(route), limit=3.0, idle={'action_type': 'NoOpAction', 'kwargs': {}}) wins, by_role = {}, {} for opp in league.field(os.path.join(ROOT, 'field')): others = [seat(os.path.join(ROOT, 'field', opp), limit=3.0, idle={'action_type': 'NoOpAction', 'kwargs': {}}) for _ in range(N - 1)] for k, sd in enumerate(SEEDS if mode == 'check' else SEEDS[:1]): for mine in range(N) if mode == 'check' else (0, 2): agents = others[:mine] + [me] + others[mine:] random.seed(sd) env = make('werewolf', configuration=configuration(sd, mine)) env.run(agents) j = env.toJSON() role = j['configuration']['agents'][mine]['role'] won = 1.0 if (env.steps[-1][mine].get('reward') or 0) > 0 else 0.0 by_role.setdefault(role.lower(), []).append(won) wins.setdefault(opp, []).append(won) if reps: names = [opp] * N names[mine] = name r = transcript(j, names) reps.add(f'{opp}-{sd}-{role.lower()}.json', r, f'{name} as the {role.lower()}, six {opp} bots, seed {sd}', [name, opp], f'{r["winner"]} win: {(r["reason"] or "").replace("Reason: ", "")}') for o in others: o.child.close() me.child.close() league.write_metrics(sum(sum(v) for v in by_role.values()) / sum(len(v) for v in by_role.values()), {k: sum(v) / len(v) for k, v in sorted(by_role.items())}) if __name__ == '__main__': main(sys.argv[1], sys.argv[2])