import argparse import os import random from string import ascii_lowercase as lowercase import zmq import logbook import urwid from network.client import Discoverer from robots.cursesviewer import CursesViewer from robots.game import Game from robots.utils import read_map log = logbook.Logger(__name__) def button_width(btn): return len(btn.label) + 4 def focusable(widget): return urwid.AttrMap(widget, None, focus_map='reversed') class Browser(Discoverer): SERVER_KIND = 'robots' def __init__(self, avail_walker, added_walker, ctx=None): super(Browser, self).__init__(self.SERVER_KIND) if ctx is None: ctx = zmq.Context.instance() self.ctx = ctx self.targets = {} self.added = [] self.avail_walker = avail_walker self.added_walker = added_walker def make_server_widget(self, target, info): return urwid.AttrMap( urwid.Text(info['name']), 'bright', ) def make_bot_widget(self, key, server_info, name): button = urwid.Button('Add') def on_button_pressed(_): self.added.append((key, server_info, name)) self.update_added() urwid.connect_signal(button, 'click', on_button_pressed) return focusable(urwid.Columns([ urwid.Text(' ' * 4 + name), (button_width(button), button), ])) def make_added_widget(self, key, server_info, name): button = urwid.Button('Remove') widget = focusable(urwid.Columns([ urwid.Text(server_info['name'] + ' / ' + name), (button_width(button), button), ])) def on_button_pressed(_): self.added.remove((key, server_info, name)) self.update_added() urwid.connect_signal(button, 'click', on_button_pressed) return widget def on_services_changed(self): current_targets = set() for service in self.services.values(): if service is None: continue target = service['target'] current_targets.add(target) if target in self.targets: continue sock = self.ctx.socket(zmq.REQ) sock.connect(target) sock.send_json({'action': 'list'}) response = sock.recv_json() if response['status'] == 200: self.targets[target] = { 'name': str(service['name']), 'bots': response['bots'], } for target in list(self.targets.keys()): if target not in current_targets: del self.targets[target] self.update_avail() def update_avail(self): things = [] for target, info in sorted( self.targets.items(), key=lambda item: item[1]['name'], ): things.append(self.make_server_widget(target, info)) for name in sorted(info['bots']): key = (target, name) things.append(self.make_bot_widget(key, info, name)) things.append(urwid.Text('')) self.avail_walker[:] = things def update_added(self): things = [] for key, server_info, name in self.added: things.append(self.make_added_widget(key, server_info, name)) self.added_walker[:] = things def make_ui(avail_walker, added_walker): start_game = urwid.Button('Start Game') def on_start_game(_): raise urwid.ExitMainLoop() urwid.connect_signal(start_game, 'click', on_start_game) main = urwid.Columns([ urwid.Frame( urwid.LineBox(urwid.ListBox(avail_walker)), header=urwid.Text('Available bots:'), ), urwid.Frame( urwid.Pile([ urwid.LineBox(urwid.ListBox(added_walker)), ('pack', urwid.Padding( focusable(start_game), align='right', width=button_width(start_game), )), ]), header=urwid.Text('Bots in game:'), ), ]) stdout = urwid.Text('') def update_stdout(data): content = stdout.text + data.decode('utf-8') lines = content.split('\n') stdout.set_text('\n'.join(lines[-5:])) frame = urwid.Frame(main, footer=urwid.LineBox(stdout)) return frame, update_stdout def choose_bots(): avail_walker = urwid.SimpleFocusListWalker([]) added_walker = urwid.SimpleFocusListWalker([]) frame, update_stdout = make_ui(avail_walker, added_walker) mainloop = urwid.MainLoop( frame, palette=[('reversed', 'standout', '')], event_loop=urwid.GLibEventLoop(), ) pipe = mainloop.watch_pipe(update_stdout) handler = logbook.StreamHandler(os.fdopen(pipe, 'w')) with handler.applicationbound(): browser = Browser(avail_walker, added_walker) mainloop.run() return browser.added class RemoteBot(object): def __init__(self, target, name, ctx=None): if ctx is None: ctx = zmq.Context.instance() self.sock = ctx.socket(zmq.REQ) self.sock.connect(target) self.debug_name = target + '/' + name self.sock.send_json({ 'action': 'create', 'name': name, 'options': {}, }) resp = self.sock.recv_json() if resp['status'] != 200: raise RuntimeError( 'Failed to create remote bot %s/%s: %s' % ( target, name, resp.get('message'), ) ) self.instance_id = resp['instance_id'] def __call__(self, whoami, state): self.sock.send_json({ 'action': 'process', 'instance_id': self.instance_id, 'whoami': whoami, 'state': state.to_json(), }) result = self.sock.recv_json() if result['status'] != 200: raise RuntimeError( 'Failed to run %s: %s' % ( self.debug_name, result.get('message'), ) ) return result['result'] def close(self): self.sock.send_json({ 'action': 'destroy', 'instance_id': self.instance_id, }) self.sock.recv_json() def __del__(self): self.close() def main(): parser = argparse.ArgumentParser() parser.add_argument('map', type=argparse.FileType('r')) args = parser.parse_args() with args.map: map_ = read_map(args.map) bots = choose_bots() game = Game( map_, victory_by_combat=len(bots) != 1, ) for key, server_info, name in bots: suffix = ''.join(random.sample(lowercase, 3)) fullname = '%s.%s.%s' % ( server_info['name'], name, suffix, ) game.add_bot( RemoteBot(*key), fullname, ) viewer = CursesViewer(game) viewer.run() if __name__ == '__main__': main()