Research · Strategy · season 1 · shoal/werewolf

Werewolf

Play liveChoose your opponent
Join
9cd2307
203 lines · 10.9 KB Raw Permalink History
1"""The project's referee, on kaggle_environments 1.33.0's seven-player Werewolf preset: two
2werewolves against a seer, a doctor and three villagers, nights of killing, healing and inspecting, days of talk and
3votes. Version 2 privately shuffles each deal. check: in three balanced rounds, the route's bot takes every seat and role and the opponent
4fills the other six; the score is the share of games the route's team wins, each role a subtask (over the field).
5practice: one seed against each opponent, the route as a werewolf and as the seer, a replay of each in replays/.
6 python3 -s referee/play.py check|practice <route folder>
7"""
8import json, os, random, sys
9
10ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
11sys.path.insert(0, os.path.join(ROOT, 'kit'))
12sys.path.insert(0, os.path.join(ROOT, 'referee'))
13import league # noqa: E402
14from kaggle_environments import make # noqa: E402
15from kaggle_seat import seat # noqa: E402
16
17N, SEEDS = 7, [3, 5, 8]
18ROLES = ['Werewolf', 'Werewolf', 'Seer', 'Doctor', 'Villager', 'Villager', 'Villager']
19
20
21def configuration(seed, mine=None, rng=None):
22 """Independent private deal; candidate covers each role once per seven games.
23
24 Gameplay seeds alone must not reconstruct the hidden assignment. The test
25 helper may inject a local RNG for repeatable audits, never a player input.
26 """
27 rng = rng or random.SystemRandom()
28 roles = list(ROLES)
29 if mine is None:
30 rng.shuffle(roles)
31 else:
32 fixed = roles.pop(mine)
33 rng.shuffle(roles)
34 roles.insert(mine, fixed)
35 return {'seed': seed, 'agents': [{'id': f'Player {i + 1}', 'role': r, 'agent_id': 'seat'}
36 for i, r in enumerate(roles)]}
37
38
39GOD = 'MODERATOR'
40NIGHT_ORDER = {'heal': 0, 'inspect': 1} # as a judge calls them: the doctor, the seer, then the wolves
41
42
43class Story:
44 """The game as the viewer draws it, from the engine's god-view records (env.info MODERATOR_OBSERVATION, one list
45 per step), so nothing downstream reads English. Seats are 0-6 (Player 1-7); a public moderator line rides on its
46 event as en. Events:
47 phase {phase: night|day, day} the engine counts night 0, day 1, night 1, day 2 ...
48 heal {actor, target} / inspect {actor, target, role, team} / wolf_vote {actor, target} / wolf_pick {target,
49 votes, counts, tie} / saved {target} the night, private: the doctor, the seer, the wolves
50 dawn {killed} after "day N begins": who died in the night, or None
51 out {who, cause: night|exile, role, team, day} every elimination, its role as the judge reveals it
52 talk {order} / speech {actor, text} the day's speaking order, then each speech
53 vote_start {order} / vote {actor, target} target None is an abstention
54 tally {votes, counts, exiled, tie} the day vote: who voted whom, the counts, who goes (tie:
55 the seats level at the top, one drawn at random)
56 over {winner, reason, roles, out} the end: every role and every elimination
57 actions carry the actor's private reasoning as why."""
58
59 def __init__(self, ids):
60 self.seat = {p: i for i, p in enumerate(ids)}
61 self.roles, self.teams = [None] * len(ids), [None] * len(ids)
62 self.gone, self.votes, self.order, self.dawn, self.en, self.peek = [], {}, None, [], [], {}
63
64 def at(self, pid):
65 return self.seat.get(pid)
66
67 def step(self, t, records):
68 out = []
69
70 def put(kind, rec=None, **kv):
71 e = {'t': t, 'kind': kind, **kv}
72 if rec is not None and rec.get('public') and rec.get('source') == GOD:
73 e['en'] = '\n'.join(self.en + [rec['description']])
74 self.en = []
75 out.append(e)
76 return e
77
78 for r in records:
79 r = json.loads(r['json_str'])
80 name, d, desc = r['event_name'], r.get('data') or {}, r['description']
81 if name == 'game_start' and 'role' in d:
82 s = self.at(d['player_id'])
83 self.roles[s], self.teams[s] = d['role'], d['team']
84 elif name == 'night_start':
85 self.votes = {}
86 put('phase', r, phase='night', day=r['day'])
87 elif name == 'heal_action':
88 put('heal', actor=self.at(d['actor_id']), target=self.at(d['target_id']), why=d.get('reasoning'))
89 elif name == 'inspect_action':
90 self.peek[d['actor_id']] = d.get('reasoning')
91 elif name == 'inspect_result':
92 put('inspect', actor=self.at(d['actor_id']), target=self.at(d['target_id']), role=d.get('role'),
93 team=d.get('team'), why=self.peek.pop(d['actor_id'], None))
94 elif name == 'vote_action' and r.get('source') != GOD: # a ballot (no data: a no-op, an abstention)
95 voter = self.at(r['source'])
96 target = self.at(d.get('target_id')) if 'target_id' in d else None
97 self.votes[voter] = target
98 put('wolf_vote' if r['phase'] == 'Night' else 'vote', actor=voter, target=target, why=d.get('reasoning'))
99 elif name == 'vote_result' and 'elected_target_player_id' in d:
100 put('wolf_pick', target=self.at(d['elected_target_player_id']), **self.count())
101 self.votes = {}
102 elif name == 'heal_result':
103 put('saved', target=self.at(d['saved_player_id']))
104 elif name == 'elimination' and 'eliminated_player_id' in d:
105 s = self.at(d['eliminated_player_id'])
106 self.dawn = [('dawn', r, {'killed': s}), ('out', None, self.fall(s, 'night', r['day']))]
107 elif name == 'moderator_announcement' and r['public'] and r['phase'] == 'Night' and not r.get('data'):
108 self.dawn = [('dawn', r, {'killed': None})] # "Last night, no one was eliminated."
109 elif name == 'day_start':
110 put('phase', r, phase='day', day=r['day'])
111 for k, rec, kv in self.dawn:
112 put(k, rec, **kv)
113 self.dawn = []
114 elif name == 'discussion_order':
115 put('talk', r, order=[self.at(p) for p in d.get('chat_order_of_player_ids') or []])
116 elif name == 'discussion' and 'message' in d and r.get('source') != GOD:
117 put('speech', actor=self.at(r['source']), text=d['message'], why=d.get('reasoning'))
118 elif name == 'vote_order' and r['phase'] == 'Day':
119 self.order = [self.at(p) for p in d.get('vote_order_of_player_ids') or []]
120 elif name == 'moderator_announcement' and 'voting_rule' in d:
121 self.votes = {}
122 put('vote_start', r, order=self.order)
123 elif name == 'elimination' and 'elected_player_id' in d:
124 s = self.at(d['elected_player_id'])
125 put('tally', **self.count(), exiled=s)
126 self.votes = {}
127 put('out', r, **self.fall(s, 'exile', r['day']))
128 elif name == 'vote_result' and r['public']: # no exile (a tie under a no-exile rule)
129 put('tally', r, **self.count(), exiled=None)
130 self.votes = {}
131 elif name == 'game_end':
132 for k, rec, kv in self.dawn: # the wolves can win in the night
133 put(k, rec, **kv)
134 self.dawn = []
135 put('over', r, **self.over(d.get('winner_team'), d.get('reason')))
136 elif r.get('public') and r.get('source') == GOD:
137 self.en.append(desc) # the rule lines: told with the next event
138 out.sort(key=lambda e: NIGHT_ORDER.get(e['kind'], 2))
139 return out
140
141 def count(self):
142 counts = {}
143 for v in self.votes.values():
144 if v is not None:
145 counts[v] = counts.get(v, 0) + 1
146 top = [s for s, c in counts.items() if c == max(counts.values())] if counts else []
147 return {'votes': dict(self.votes), 'counts': counts, 'tie': top if len(top) > 1 else None,
148 'all_abstained': bool(self.votes) and not counts}
149
150 def fall(self, s, cause, day):
151 e = {'who': s, 'cause': cause, 'role': self.roles[s], 'team': self.teams[s], 'day': day}
152 self.gone.append(e)
153 return e
154
155 def over(self, winner, reason):
156 return {'winner': winner, 'reason': (reason or '').replace('Reason: ', ''), 'roles': self.roles, 'out': self.gone}
157
158
159def transcript(j, names, kinds=None):
160 """The replay: the players with their roles and fates, and the game as Story's events (see Story)."""
161 ids = [a['id'] for a in j['configuration']['agents']]
162 story, events = Story(ids), []
163 for t, records in enumerate(j['info'].get('MODERATOR_OBSERVATION') or []):
164 events += story.step(t, records)
165 over = next((e for e in events if e['kind'] == 'over'), None)
166 fate = {e['who']: e for e in story.gone}
167 return {'game': 'werewolf', 'version': 2,
168 'players': [{'seat': i, 'id': ids[i], 'name': names[i], 'kind': (kinds or {}).get(i, 'bot'), 'role': story.roles[i],
169 'team': story.teams[i], 'out': fate.get(i)} for i in range(len(ids))],
170 'events': events, 'winner': over and over['winner'], 'reason': over and over['reason']}
171
172
173def main(mode, route):
174 name = league.route_name()
175 reps = league.Replays() if mode == 'practice' else None
176 me = seat(league.bot_dir(route), limit=3.0, idle={'action_type': 'NoOpAction', 'kwargs': {}})
177 wins, by_role = {}, {}
178 for opp in league.field(os.path.join(ROOT, 'field')):
179 others = [seat(os.path.join(ROOT, 'field', opp), limit=3.0, idle={'action_type': 'NoOpAction', 'kwargs': {}}) for _ in range(N - 1)]
180 for k, sd in enumerate(SEEDS if mode == 'check' else SEEDS[:1]):
181 for mine in range(N) if mode == 'check' else (0, 2):
182 agents = others[:mine] + [me] + others[mine:]
183 random.seed(sd)
184 env = make('werewolf', configuration=configuration(sd, mine))
185 env.run(agents)
186 j = env.toJSON()
187 role = j['configuration']['agents'][mine]['role']
188 won = 1.0 if (env.steps[-1][mine].get('reward') or 0) > 0 else 0.0
189 by_role.setdefault(role.lower(), []).append(won)
190 wins.setdefault(opp, []).append(won)
191 if reps:
192 names = [opp] * N
193 names[mine] = name
194 r = transcript(j, names)
195 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: ", "")}')
196 for o in others:
197 o.child.close()
198 me.child.close()
199 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())})
200
201
202if __name__ == '__main__':
203 main(sys.argv[1], sys.argv[2])