"""Play a room on a laptop, without the site: run a room command with SS_ROOM, print what it says (docs/games.md ยง8.3), and answer its requests to human and agent seats, from the terminal or by rule. python3 kit/roomcli.py --room '{"seats": {...}}' [--auto] [--seat 0 --pick random] [--control 0=auto@6] [--stop 12] -- --auto answers every request with the first legal action; --seat S (repeatable) answers seat S by --pick (first or random) and leaves the other seats to the terminal. --control S=KIND@N: at the first request to seat S after N public events, hand the seat to KIND (auto, agent, human), then send the old controller's late answer to that request, which the room must ignore (it re-asks with a new version, or auto plays). --stop N sends {"stop": true} after N events. SS_INPUT (a route's dir is under it) defaults to the current folder; --full prints whole messages. """ import argparse, json, os, random, subprocess, sys def main(): ap = argparse.ArgumentParser(description=__doc__.split('\n')[0]) ap.add_argument('--room', default=os.environ.get('SS_ROOM', '{"seats": {}}'), help='SS_ROOM for the room') ap.add_argument('--auto', action='store_true', help='answer every request with the first legal action') ap.add_argument('--seat', action='append', default=[], help='a seat answered by --pick') ap.add_argument('--pick', choices=['first', 'random'], default='first') ap.add_argument('--control', action='append', default=[], help='S=KIND@N: hand seat S to KIND after N events') ap.add_argument('--stop', type=int, help='send stop after N events') ap.add_argument('--seed', type=int, default=0, help='for --pick random') ap.add_argument('--full', action='store_true') ap.add_argument('cmd', nargs=argparse.REMAINDER) a = ap.parse_args() cmd = a.cmd[1:] if a.cmd[:1] == ['--'] else a.cmd if not cmd: ap.error('give the room command after --') controls = [] for c in a.control: seat, rest = c.split('=', 1) kind, at = rest.split('@', 1) controls.append({'seat': seat, 'kind': kind, 'at': int(at)}) rnd = random.Random(a.seed) env = {**os.environ, 'SS_ROOM': a.room, 'SS_INPUT': os.environ.get('SS_INPUT', os.getcwd())} p = subprocess.Popen(cmd, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, bufsize=1) events = asked = answered = late = 0 versions = [] def send(msg): p.stdin.write(json.dumps(msg) + '\n') p.stdin.flush() print(' >>', json.dumps(msg)[:200]) for line in p.stdout: if not line.startswith('@shoal '): print(' log', line.rstrip()) continue msg = json.loads(line[7:]) text = json.dumps(msg) print(f'@shoal {text if a.full or len(text) <= 200 else text[:197] + "..."}') if 'event' in msg: events += 1 if a.stop is not None and events == a.stop: send({'stop': True}) if 'end' in msg: break if 'version' not in msg: continue seat, v, legal = msg['to'], msg['version'], msg['legal'] asked += 1 assert not versions or v > versions[-1], 'versions must grow' versions.append(v) due = next((c for c in controls if c['seat'] == seat and events >= c['at']), None) if due: # the handover, then the old controller's answer arriving late controls.remove(due) send({'control': {'seat': seat, 'kind': due['kind'], 'name': f'{due["kind"]}-{seat}'}}) send({'act': {'seat': seat, 'version': v, 'action': legal[-1]}}) late += 1 continue if a.auto or seat in a.seat: action = legal[0] if a.pick == 'first' or a.auto else rnd.choice(legal) else: print(f'seat {seat}, version {v}: obs {json.dumps(msg["obs"])[:300]}\nlegal: {" ".join(map(str, legal[:60]))}') got = input('your move, or its index (blank: the first)> ').strip() action = got if got in legal else legal[int(got)] if got.isdigit() and int(got) < len(legal) else got or legal[0] send({'act': {'seat': seat, 'version': v, 'action': action}}) answered += 1 p.stdin.close() code = p.wait() print(f'roomcli: {events} events, {asked} requests (versions {versions[:1]}..{versions[-1:]}), {answered} answered, ' f'{late} late answers sent, exit {code}') sys.exit(code) if __name__ == '__main__': main()