Research · Strategy · season 1 · shoal/werewolf

Werewolf

Play liveChoose your opponent
Join
games
What shoal-games changes in this game
Branched from main at b64da5f by @shoal shoal · 1 commit ahead, 0 behind main
Continue
Contribute back
206 lines · 11.2 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 offline evidence-based computer, with a private journal per seat. 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}], day and night votes also {type: vote|kill, 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_seat import seat as bot_seat # noqa: E402
36from play import N, Story, transcript # noqa: E402
37from rule_agent import RuleAgent, public_config # 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 # SequentialVoting's potential targets include the voter. The upstream
75 # example agent avoids self-votes, but that is a policy, not a rule.
76 return 'vote', list(raw['alive_players'])
77 if phase == 'NIGHT_AWAIT_ACTIONS':
78 if role == 'Werewolf':
79 return 'kill', listed('vote_request', 'valid_targets') or others
80 if role == 'Doctor':
81 return 'heal', listed('heal_request', 'valid_candidates') or others
82 if role == 'Seer':
83 return 'inspect', listed('inspect_request', 'valid_candidates') or others
84 return None, []
85
86
87def main():
88 conf = json.loads(os.environ.get('SS_ROOM') or '{}')
89 seed = int(conf['seed']) if conf.get('seed') is not None else random.SystemRandom().randrange(1 << 31)
90 random.seed(seed) # the engine's own draws: ties, who speaks and votes first
91 room = Room(Bots, field=os.path.join(ROOT, 'field'))
92 env = make('werewolf', configuration={'seed': seed, 'randomize_roles': True})
93 env.reset(N)
94 config = public_config(json.loads(json.dumps(env.configuration, default=str)))
95 stand_ins = [RuleAgent() for _ in range(N)]
96 seats = [str(i) for i in range(N)]
97 ids = [p.id for p in env.game_state.players]
98 role = [str(p.role.name) for p in env.game_state.players]
99 team = [str(p.role.team) for p in env.game_state.players]
100 wolves = [i for i in range(N) if role[i] == 'Werewolf']
101 names = [room.label(s) for s in seats]
102 story, public, known, seen = Story(ids), [], {s: [] for s in seats}, 0
103
104 def tell(i, e):
105 known[seats[i]].append(e)
106 room.tell(seats[i], e)
107
108 def publish():
109 nonlocal seen
110 log = env.info['MODERATOR_OBSERVATION']
111 for t in range(seen, len(log)):
112 for e in story.step(t, log[t]):
113 e = {k: v for k, v in e.items() if k not in ('t', 'why')} # reasoning stays private until the replay
114 if e['kind'] in SECRET:
115 for i in range(N):
116 if role[i] == SECRET[e['kind']]:
117 tell(i, e)
118 else:
119 public.append(e)
120 room.event(e)
121 seen = len(log)
122
123 def move(i):
124 s = seats[i]
125 obs = json.loads(json.dumps({**env.state[i].observation, 'step': env.state[0].observation.step}, default=str))
126 raw = obs['raw_observation']
127 what, targets = request(raw)
128 # Observe every turn, including turns a human handles, so a later
129 # timeout does not forget that seat's earlier private knowledge.
130 stand_ins[i].observe(obs)
131 stand_in = lambda: stand_ins[i](obs) # noqa: E731
132 if what is None:
133 return stand_in() # nothing asked: the engine's no-op
134 seat_of = {p: str(ids.index(p)) for p in ids}
135 aim = [seat_of[p] for p in targets if p in seat_of]
136 legal = [{'type': 'chat', 'message': ''}] if what == 'chat' else \
137 [{'type': what, 'target': t} for t in aim] + ([{'type': what, 'target': None}] if what in ('vote', 'kill') else [])
138 base = {'day': raw['day'], 'phase': raw['game_state_phase'], 'actor_id': raw['player_id']}
139
140 def accept(a):
141 if isinstance(a, int) and not isinstance(a, bool) and 0 <= a < len(legal):
142 a = legal[a]
143 if isinstance(a, dict) and 'action_type' in a: # a Kaggle agent's own action
144 if a['action_type'] != ACTION[what].__name__:
145 return None
146 k = a.get('kwargs') or {}
147 t = k.get('target_id')
148 a = {'type': what, 'message': k.get('message'), 'reasoning': k.get('reasoning'),
149 'target': None if t in (None, '-1') else seat_of.get(t, '?')}
150 if not isinstance(a, dict) or a.get('type') != what:
151 return None
152 why = a.get('reasoning')
153 kw = {**base, 'reasoning': str(why)[:SAY] if why else None}
154 if what == 'chat':
155 text = a.get('message')
156 return ChatAction(**kw, message=text.strip()[:SAY] or PASS).serialize() if isinstance(text, str) else None
157 t = a.get('target')
158 t = None if t is None else str(t)
159 if (t is None and what not in ('vote', 'kill')) or (t is not None and t not in aim):
160 return None
161 return ACTION[what](**kw, target_id='-1' if t is None else ids[int(t)]).serialize()
162
163 limit = room.limit * 3 if what == 'chat' else None # a speech takes longer to write
164 seconds = limit or room.limit
165 if what in ('chat', 'vote') and room.kind(s) in ('human', 'agent'):
166 room.event({'kind': 'turn', 'seat': i, 'what': what, 'seconds': seconds})
167 view = {'seat': s, 'player': ids[i], 'role': role[i], 'team': team[i],
168 'wolves': [seats[w] for w in wolves] if role[i] == 'Werewolf' else [],
169 'day': raw['day'], 'phase': raw['game_state_phase'].lower(), 'request': what,
170 'alive': [seat_of[p] for p in raw['alive_players']], 'known': known[s], 'events': public[-40:],
171 'seconds': seconds, 'raw': raw}
172 return room.ask(s, view, legal, auto=stand_in, bot={'obs': obs, 'config': config}, accept=accept, limit=limit)
173
174 room.event({'kind': 'start', 'seats': {s: {'name': names[i], 'kind': room.kind(s)} for i, s in enumerate(seats)},
175 'roles': {r: role.count(r) for r in ('Werewolf', 'Seer', 'Doctor', 'Villager')}, 'seconds': room.limit})
176 for i in range(N):
177 tell(i, {'kind': 'role', 'seat': i, 'role': role[i], 'team': team[i], 'wolves': wolves if role[i] == 'Werewolf' else []})
178 publish()
179 stopped = False
180 try:
181 while not env.done:
182 acts = [None] * N
183 for i in sorted((i for i in range(N) if env.state[i].status == 'ACTIVE'), key=lambda i: WAKE.get(role[i], 2)):
184 acts[i] = move(i)
185 env.step(acts)
186 publish()
187 except Stop:
188 stopped = True
189 e = {'kind': 'over', **story.over(None, 'stopped')}
190 public.append(e)
191 room.event(e)
192
193 end = env.info.get('GAME_END') or {}
194 winner = None if stopped else end.get('winner_team')
195 result = 'stopped' if winner is None else f'{winner.lower()} win, {N - len(story.gone)} left alive'
196 replay = transcript(env.toJSON(), names, {i: room.kind(s) for i, s in enumerate(seats)})
197 if stopped:
198 replay['events'].append(public[-1])
199 league.Replays().add(f'room-{seed}.json', replay, 'Room: ' + ', '.join(f'{i + 1} {n}' for i, n in enumerate(names)),
200 names, result)
201 room.end(result, {s: float(winner is not None and team[i] == winner) for i, s in enumerate(seats)})
202 room.close()
203
204
205if __name__ == '__main__':
206 main()