| 1 | """The project's referee, on kaggle_environments' own Werewolf (the game Kaggle Game Arena plays): seven players, two |
| 2 | werewolves against a seer, a doctor and three villagers, nights of killing, healing and inspecting, days of talk and |
| 3 | votes. Seats take roles in order. check: on three seeds, the route's bot takes every seat in turn and the opponent |
| 4 | fills the other six; the score is the share of games the route's team wins, each role a subtask (over the field). |
| 5 | practice: 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 | """ |
| 8 | import json, os, random, sys |
| 9 | |
| 10 | ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| 11 | sys.path.insert(0, os.path.join(ROOT, 'kit')) |
| 12 | sys.path.insert(0, os.path.join(ROOT, 'referee')) |
| 13 | import league |
| 14 | from kaggle_environments import make |
| 15 | from kaggle_seat import seat |
| 16 | |
| 17 | N, SEEDS = 7, [3, 5, 8] |
| 18 | |
| 19 | |
| 20 | GOD = 'MODERATOR' |
| 21 | NIGHT_ORDER = {'heal': 0, 'inspect': 1} |
| 22 | |
| 23 | |
| 24 | class 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: |
| 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})] |
| 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']: |
| 110 | put('tally', r, **self.count(), exiled=None) |
| 111 | self.votes = {} |
| 112 | elif name == 'game_end': |
| 113 | for k, rec, kv in self.dawn: |
| 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) |
| 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 | |
| 139 | def 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 | |
| 153 | def 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 | |
| 182 | if __name__ == '__main__': |
| 183 | main(sys.argv[1], sys.argv[2]) |