Research · Strategy · season 1 · shoal/werewolf

Werewolf

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