研究 · 休闲、经营 · 第 1 赛季 · shoal/overcooked

Overcooked

要打败的Starter (GreedyHumanModel) 97.7778
加入
What shoal-games changes in this game
由 @shoal shoal 从 main 的 28c377d 分出 · 领先 1 个提交,比 main 落后 0 个
接续
贡献回去
144 行 · 6.6 KB 原始文件 永久链接 历史
1"""A room's half of the room protocol (docs/games.md §8.3), for any game's referee. SS_ROOM says who holds each seat;
2ask() gets a move from a bot child, from a person or agent over stdin, or from the game's own fallback (auto).
3
4 room = Room(start) start(folder) -> host.Child, the game's way to run a bot (a route's or field/<name>)
5 move = room.ask(seat, obs, legal, auto=lambda: ..., bot=<the bot child's message, in the game's own interface>)
6 move = room.ask(seat, obs, legal, auto, accept=lambda a: ...) # legal holds templates (free text): accept says
7 # what an answer means, or None to keep waiting
8 room.tell(seat, obs); room.event(data); room.end(result, scores); room.close()
9
10Lines to the platform start with '@shoal ' and are flushed one by one; the referee's own prints go to stderr, so a
11library printing as it loads cannot break a line. A person or agent who does not answer within the limit (SS_ROOM
12"limit", 30 s by default; ask(..., limit=s) sets one request's, e.g. a speech's) gets auto's move. {"control": ...}
13re-asks the seat with a new version, so a late answer from the old controller is dropped; {"stop": true} makes ask()
14raise Stop, and the referee writes what it has and ends.
15"""
16import json, os, queue, sys, threading, time
17
18
19class Stop(Exception):
20 pass
21
22
23def log(*a):
24 print('room:', *a, file=sys.stderr, flush=True)
25
26
27class Room:
28 def __init__(self, start, field='field'):
29 conf = json.loads(os.environ.get('SS_ROOM') or '{}')
30 self.seats = {str(k): v for k, v in conf.get('seats', {}).items()}
31 self.limit = float(conf.get('limit', 30))
32 self.start, self.field, self.bots = start, field, {}
33 self.version, self.stopped = 0, False
34 self.inbox, self.lock = queue.Queue(), threading.Lock()
35 self.out, sys.stdout = sys.stdout, sys.stderr # prints, ours or a library's, must not look like protocol
36 threading.Thread(target=self._read, daemon=True).start()
37
38 def say(self, msg):
39 with self.lock: # the reader thread speaks too (control events)
40 self.out.write('@shoal ' + json.dumps(msg, separators=(',', ':')) + '\n')
41 self.out.flush()
42
43 def tell(self, seat, obs):
44 self.say({'to': seat, 'obs': obs})
45
46 def event(self, data):
47 self.say({'event': data})
48
49 def end(self, result, scores):
50 self.say({'end': {'result': result, 'scores': scores}})
51
52 def label(self, seat):
53 s = self.seats.get(seat) or {}
54 return s.get('name') or s.get('route') or s.get('field') or os.path.basename((s.get('dir') or '').rstrip('/')) or s.get('kind') or 'auto'
55
56 def kind(self, seat):
57 return (self.seats.get(seat) or {}).get('kind') or 'auto'
58
59 def ask(self, seat, obs, legal, auto, bot=None, accept=None, limit=None):
60 ok = accept or (lambda a: a if a in legal else None)
61 while True:
62 if self.stopped:
63 raise Stop
64 spec = self.seats.get(seat) or {}
65 kind = spec.get('kind')
66 if kind == 'bot':
67 child = self._bot(spec, seat=seat)
68 move = child.ask(bot if bot is not None else {'obs': obs, 'legal': legal}) if child else None
69 move = ok(move) if move is not None else None
70 return auto() if move is None else move # a late, broken or illegal bot move: auto plays it
71 if kind not in ('human', 'agent'):
72 return auto()
73 self._drain() # answers and notices from before this request are stale
74 if self.stopped: # a stop drained with them still counts
75 raise Stop
76 self.version += 1
77 v = self.version
78 self.say({'to': seat, 'version': v, 'obs': obs, 'legal': legal})
79 deadline = time.monotonic() + (limit or self.limit)
80 while True:
81 try:
82 msg = self.inbox.get(timeout=max(0.0, deadline - time.monotonic()))
83 except queue.Empty:
84 log(f'seat {seat} out of time at version {v}: auto plays')
85 return auto()
86 if msg.get('stop'):
87 raise Stop
88 c = msg.get('control')
89 if isinstance(c, dict) and str(c.get('seat')) == seat:
90 break # a new controller: ask again, with a new version
91 a = msg.get('act')
92 if isinstance(a, dict) and str(a.get('seat')) == seat and a.get('version') == v:
93 move = ok(a.get('action'))
94 if move is not None:
95 return move
96 if a is not None:
97 log(f'ignored act {json.dumps(a)[:120]}: asking seat {seat} at version {v}')
98
99 def close(self):
100 for c in self.bots.values():
101 if c:
102 c.close()
103
104 def _read(self):
105 for line in sys.stdin:
106 try:
107 msg = json.loads(line)
108 except ValueError:
109 log('not JSON:', line.strip()[:120])
110 continue
111 if not isinstance(msg, dict):
112 continue
113 if msg.get('stop'):
114 self.stopped = True
115 c = msg.get('control')
116 if isinstance(c, dict) and 'seat' in c: # applied at once: the seat may not be the one being asked
117 seat = str(c['seat'])
118 self.seats[seat] = {k: v for k, v in c.items() if k != 'seat'}
119 self.event({'type': 'control', 'seat': seat, 'kind': self.kind(seat), 'name': self.label(seat)})
120 self.inbox.put(msg)
121
122 def _drain(self):
123 while True:
124 try:
125 msg = self.inbox.get_nowait()
126 except queue.Empty:
127 return
128 if msg.get('act') is not None:
129 log(f'ignored act {json.dumps(msg["act"])[:120]}: stale')
130
131 def _bot(self, spec, seat=None):
132 """One process per seat and folder: a bot must never accumulate another seat's hidden observations."""
133 if spec.get('field'):
134 folder = os.path.join(self.field, spec['field'])
135 else:
136 folder = os.path.join(os.environ.get('SS_INPUT', '.'), spec.get('dir') or spec.get('route') or '')
137 key = (seat, folder)
138 if key not in self.bots:
139 try:
140 self.bots[key] = self.start(folder)
141 except (Exception, SystemExit) as e: # a missing bot is played by auto rather than ending the room
142 log(f'no bot at {folder} ({e}): auto plays that seat')
143 self.bots[key] = None
144 return self.bots[key]