Research · Strategy · season 1 · shoal/werewolf

Werewolf

Play liveChoose your opponent
Join
b64da5f
200 lines · 10.7 KB Raw Permalink History
1"""A live room (docs/games.md §8): one game of kaggle-environments' Werewolf with people, agents and bots at the seven
2seats, speaking the room protocol through kit/room.py. Seat "0"-"6" is Player 1-7 and the engine deals the roles
3(seeded by SS_ROOM's seed, else at random). A bot runs a route's or a field folder's main.py in a process of its own per
4seat, as in the checks, 3 s a move; a seat on auto, a person out of time and a bot that fails or answers out of turn
5are played by the engine's deterministic agent. Public things are events; what one seat alone may know (its role, the
6wolves' picks, the seer's results, the doctor's heals) is told to that seat. At the end the game is left in replays/ in
7the practice replays' format with a matches.json entry, so it lists as a match.
8
9 SS_ROOM='{"seats": {"0": {"kind": "human", "name": "ada"}, "1": {"kind": "bot", "field": "deterministic"}}}' \
10 python3 -s referee/room.py
11
12Messages (events are play.py's Story events, seats 0-6, without the private reasoning):
13 event {kind: start, seats: {seat: {name, kind}}, roles: {role: count}, seconds}
14 to each seat {kind: role, seat, role, team, wolves}: its role card (wolves: the werewolves' seats, for a werewolf)
15 event {kind: phase|dawn|out|talk|speech|vote_start|vote|tally, ...}, and {kind: turn, seat, what: chat|vote,
16 seconds} before a person's or an agent's speech or day vote
17 to a seat {kind: heal|saved|inspect|wolf_vote|wolf_pick, ...}: the doctor's, the seer's and the wolves' own night
18 request {to, version, obs: {seat, player, role, team, wolves, day, phase, request, alive, known, events, seconds,
19 raw}, legal}: request is chat, vote, kill, heal or inspect; known is what was told to the seat, events the public
20 ones so far (the last 40), raw Kaggle's own raw_observation, so a Kaggle agent can play from it
21 chat: legal [{type: chat, message: ""}], answered with the message filled in (trimmed, at most 600 characters;
22 left empty, the seat passes)
23 kill|heal|inspect|vote: legal [{type, target: seat}], a day vote also {type: vote, target: null} to abstain
24 an answer may add "reasoning" (kept for the replay), or be the Kaggle agent's own action, as main.py returns it
25 event {kind: over, winner, reason, roles, out}, then end {result, scores}: 1 for each seat of the winning team
26"""
27import json, os, random, sys
28
29ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
30sys.path[:0] = [os.path.join(ROOT, 'kit'), os.path.join(ROOT, 'referee')] # kit's room.py, not this file
31from room import Room, Stop # noqa: E402 (first: play.py puts referee/ ahead of kit/ as it loads)
32import league # noqa: E402
33from kaggle_environments import make # noqa: E402
34from kaggle_environments.envs.werewolf.game.actions import ChatAction, HealAction, InspectAction, VoteAction # noqa: E402
35from kaggle_environments.envs.werewolf.werewolf import deterministic_agent # noqa: E402
36from kaggle_seat import seat as bot_seat # noqa: E402
37from play import N, Story, transcript # noqa: E402
38
39SAY, PASS = 600, 'I pass.'
40ACTION = {'chat': ChatAction, 'vote': VoteAction, 'kill': VoteAction, 'heal': HealAction, 'inspect': InspectAction}
41SECRET = {'heal': 'Doctor', 'saved': 'Doctor', 'inspect': 'Seer', 'wolf_vote': 'Werewolf', 'wolf_pick': 'Werewolf'}
42WAKE = {'Doctor': 0, 'Seer': 1} # the judge wakes the doctor, then the seer, then the wolves
43
44
45class Bots:
46 """A folder's bot, one process per seat as in the checks: a bot may keep what it learns about its own game."""
47
48 def __init__(self, folder):
49 self.new = lambda: bot_seat(folder, limit=3.0).child
50 self.kids = {None: self.new()} # fails here, as Room expects, when the folder has no bot
51
52 def ask(self, msg):
53 pid = msg['obs']['raw_observation']['player_id']
54 if pid not in self.kids:
55 self.kids[pid] = self.kids.pop(None, None) or self.new()
56 return self.kids[pid].ask(msg)
57
58 def close(self):
59 for c in self.kids.values():
60 c.close()
61
62
63def request(raw):
64 """What the engine asks of this player now and the players it may name: (chat|vote|kill|heal|inspect, ids)."""
65 phase, role, me = raw['detailed_phase'], raw['role'], raw['player_id']
66 others = [p for p in raw['alive_players'] if p != me]
67
68 def listed(name, key): # the request's own list of valid targets
69 return next((v['data'][key] for v in raw['new_player_event_views'] if v['event_name'] == name and (v.get('data') or {}).get(key)), None)
70
71 if phase == 'DAY_CHAT_AWAIT':
72 return 'chat', []
73 if phase == 'DAY_VOTING_AWAIT':
74 return 'vote', others
75 if phase == 'NIGHT_AWAIT_ACTIONS':
76 if role == 'Werewolf':
77 return 'kill', listed('vote_request', 'valid_targets') or others
78 if role == 'Doctor':
79 return 'heal', listed('heal_request', 'valid_candidates') or others
80 if role == 'Seer':
81 return 'inspect', listed('inspect_request', 'valid_candidates') or others
82 return None, []
83
84
85def main():
86 conf = json.loads(os.environ.get('SS_ROOM') or '{}')
87 seed = int(conf['seed']) if conf.get('seed') is not None else random.SystemRandom().randrange(1 << 31)
88 random.seed(seed) # the engine's own draws: ties, who speaks and votes first
89 room = Room(Bots, field=os.path.join(ROOT, 'field'))
90 env = make('werewolf', configuration={'seed': seed, 'randomize_roles': True})
91 env.reset(N)
92 config = json.loads(json.dumps(env.configuration, default=str))
93 seats = [str(i) for i in range(N)]
94 ids = [p.id for p in env.game_state.players]
95 role = [str(p.role.name) for p in env.game_state.players]
96 team = [str(p.role.team) for p in env.game_state.players]
97 wolves = [i for i in range(N) if role[i] == 'Werewolf']
98 names = [room.label(s) for s in seats]
99 story, public, known, seen = Story(ids), [], {s: [] for s in seats}, 0
100
101 def tell(i, e):
102 known[seats[i]].append(e)
103 room.tell(seats[i], e)
104
105 def publish():
106 nonlocal seen
107 log = env.info['MODERATOR_OBSERVATION']
108 for t in range(seen, len(log)):
109 for e in story.step(t, log[t]):
110 e = {k: v for k, v in e.items() if k not in ('t', 'why')} # reasoning stays private until the replay
111 if e['kind'] in SECRET:
112 for i in range(N):
113 if role[i] == SECRET[e['kind']]:
114 tell(i, e)
115 else:
116 public.append(e)
117 room.event(e)
118 seen = len(log)
119
120 def move(i):
121 s = seats[i]
122 obs = json.loads(json.dumps({**env.state[i].observation, 'step': env.state[0].observation.step}, default=str))
123 raw = obs['raw_observation']
124 what, targets = request(raw)
125 stand_in = lambda: deterministic_agent(obs) # noqa: E731
126 if what is None:
127 return stand_in() # nothing asked: the engine's no-op
128 seat_of = {p: str(ids.index(p)) for p in ids}
129 aim = [seat_of[p] for p in targets if p in seat_of]
130 legal = [{'type': 'chat', 'message': ''}] if what == 'chat' else \
131 [{'type': what, 'target': t} for t in aim] + ([{'type': 'vote', 'target': None}] if what == 'vote' else [])
132 base = {'day': raw['day'], 'phase': raw['game_state_phase'], 'actor_id': raw['player_id']}
133
134 def accept(a):
135 if isinstance(a, int) and not isinstance(a, bool) and 0 <= a < len(legal):
136 a = legal[a]
137 if isinstance(a, dict) and 'action_type' in a: # a Kaggle agent's own action
138 if a['action_type'] != ACTION[what].__name__:
139 return None
140 k = a.get('kwargs') or {}
141 t = k.get('target_id')
142 a = {'type': what, 'message': k.get('message'), 'reasoning': k.get('reasoning'),
143 'target': None if t in (None, '-1') else seat_of.get(t, '?')}
144 if not isinstance(a, dict) or a.get('type') != what:
145 return None
146 why = a.get('reasoning')
147 kw = {**base, 'reasoning': str(why)[:SAY] if why else None}
148 if what == 'chat':
149 text = a.get('message')
150 return ChatAction(**kw, message=text.strip()[:SAY] or PASS).serialize() if isinstance(text, str) else None
151 t = a.get('target')
152 t = None if t is None else str(t)
153 if (t is None and what != 'vote') or (t is not None and t not in aim):
154 return None
155 return ACTION[what](**kw, target_id='-1' if t is None else ids[int(t)]).serialize()
156
157 limit = room.limit * 3 if what == 'chat' else None # a speech takes longer to write
158 seconds = limit or room.limit
159 if what in ('chat', 'vote') and room.kind(s) in ('human', 'agent'):
160 room.event({'kind': 'turn', 'seat': i, 'what': what, 'seconds': seconds})
161 view = {'seat': s, 'player': ids[i], 'role': role[i], 'team': team[i],
162 'wolves': [seats[w] for w in wolves] if role[i] == 'Werewolf' else [],
163 'day': raw['day'], 'phase': raw['game_state_phase'].lower(), 'request': what,
164 'alive': [seat_of[p] for p in raw['alive_players']], 'known': known[s], 'events': public[-40:],
165 'seconds': seconds, 'raw': raw}
166 return room.ask(s, view, legal, auto=stand_in, bot={'obs': obs, 'config': config}, accept=accept, limit=limit)
167
168 room.event({'kind': 'start', 'seats': {s: {'name': names[i], 'kind': room.kind(s)} for i, s in enumerate(seats)},
169 'roles': {r: role.count(r) for r in ('Werewolf', 'Seer', 'Doctor', 'Villager')}, 'seconds': room.limit})
170 for i in range(N):
171 tell(i, {'kind': 'role', 'seat': i, 'role': role[i], 'team': team[i], 'wolves': wolves if role[i] == 'Werewolf' else []})
172 publish()
173 stopped = False
174 try:
175 while not env.done:
176 acts = [None] * N
177 for i in sorted((i for i in range(N) if env.state[i].status == 'ACTIVE'), key=lambda i: WAKE.get(role[i], 2)):
178 acts[i] = move(i)
179 env.step(acts)
180 publish()
181 except Stop:
182 stopped = True
183 e = {'kind': 'over', **story.over(None, 'stopped')}
184 public.append(e)
185 room.event(e)
186
187 end = env.info.get('GAME_END') or {}
188 winner = None if stopped else end.get('winner_team')
189 result = 'stopped' if winner is None else f'{winner.lower()} win, {N - len(story.gone)} left alive'
190 replay = transcript(env.toJSON(), names, {i: room.kind(s) for i, s in enumerate(seats)})
191 if stopped:
192 replay['events'].append(public[-1])
193 league.Replays().add(f'room-{seed}.json', replay, 'Room: ' + ', '.join(f'{i + 1} {n}' for i, n in enumerate(names)),
194 names, result)
195 room.end(result, {s: float(winner is not None and team[i] == winner) for i, s in enumerate(seats)})
196 room.close()
197
198
199if __name__ == '__main__':
200 main()