Ejemplo n.º 1
0
class ScratchManager(BotHelperProcess):
    def __init__(self, agent_metadata_queue, quit_event, options):
        super().__init__(agent_metadata_queue, quit_event, options)
        self.logger = get_logger('scratch_mgr')
        self.game_interface = GameInterface(self.logger)
        self.current_sockets = set()
        self.running_indices = set()
        self.port: int = options['port']
        self.sb3_file = options['sb3-file']
        self.has_received_input = False

    async def data_exchange(self, websocket, path):
        async for message in websocket:
            controller_states = json.loads(message)

            if not self.has_received_input:
                self.has_received_input = True
                self.logger.info(
                    f"Just got first input from Scratch {self.sb3_file} {self.port}"
                )

            for key, scratch_state in controller_states.items():
                self.game_interface.update_player_input_flat(
                    self.convert_to_flatbuffer(scratch_state, int(key)))

            self.current_sockets.add(websocket)

    def try_receive_agent_metadata(self):
        """
        As agents start up, they will dump their configuration into the metadata_queue.
        Read from it to learn about all the bots intending to use this scratch manager.
        """
        while True:  # will exit on queue.Empty
            try:
                single_agent_metadata: AgentMetadata = self.metadata_queue.get(
                    timeout=0.1)
                self.running_indices.add(single_agent_metadata.index)
            except queue.Empty:
                return
            except Exception as ex:
                self.logger.error(ex)

    def start(self):
        self.logger.info("Starting scratch manager")

        self.game_interface.load_interface()

        # Wait a moment for all agents to have a chance to start up and send metadata
        time.sleep(1)
        self.try_receive_agent_metadata()

        self.logger.info(self.running_indices)

        if self.options['spawn_browser']:
            options = Options()
            options.headless = self.options['headless']
            current_folder = os.path.dirname(os.path.realpath(__file__))
            driver_path = os.path.join(current_folder, "chromedriver.exe")
            driver = webdriver.Chrome(driver_path, chrome_options=options)

            players_string = ",".join(
                map(index_to_player_string, self.running_indices))
            driver.get(
                f"http://scratch.rlbot.org?host=localhost:{str(self.port)}&players={players_string}"
            )

            if self.sb3_file is not None:
                element = WebDriverWait(driver, 10).until(
                    EC.presence_of_element_located(
                        (By.ID, "sb3-selenium-uploader")))
                # TODO: This sleep is designed to avoid a race condition. Instead of sleeping,
                # Consider passing a url param to make scratch not load the default project.
                # Hopefully that will make the race go away.
                time.sleep(5)
                element.send_keys(self.sb3_file)

        asyncio.get_event_loop().run_until_complete(
            websockets.serve(self.data_exchange, port=self.port))
        asyncio.get_event_loop().run_until_complete(self.game_loop())

    async def game_loop(self):

        last_tick_game_time = None  # What the tick time of the last observed tick was
        last_call_real_time = datetime.now()  # When we last called the Agent

        # Run until main process tells to stop
        while not self.quit_event.is_set():
            before = datetime.now()

            game_tick_flat_binary = self.game_interface.get_live_data_flat_binary(
            )
            if game_tick_flat_binary is None:
                continue

            game_tick_flat = GameTickPacket.GameTickPacket.GetRootAsGameTickPacket(
                game_tick_flat_binary, 0)

            # Run the Agent only if the gameInfo has updated.
            tick_game_time = self.get_game_time(game_tick_flat)
            worth_communicating = tick_game_time != last_tick_game_time or \
                                  datetime.now() - last_call_real_time >= MAX_AGENT_CALL_PERIOD

            ball = game_tick_flat.Ball()
            if ball is not None and worth_communicating:
                last_tick_game_time = tick_game_time
                last_call_real_time = datetime.now()

                tiny_player_offsets = []
                builder = flatbuffers.Builder(0)

                for i in range(game_tick_flat.PlayersLength()):
                    tiny_player_offsets.append(
                        copy_player(game_tick_flat.Players(i), builder))

                TinyPacket.TinyPacketStartPlayersVector(
                    builder, game_tick_flat.PlayersLength())
                for i in reversed(range(0, len(tiny_player_offsets))):
                    builder.PrependUOffsetTRelative(tiny_player_offsets[i])
                players_offset = builder.EndVector(len(tiny_player_offsets))

                ballOffset = copy_ball(ball, builder)

                TinyPacket.TinyPacketStart(builder)
                TinyPacket.TinyPacketAddPlayers(builder, players_offset)
                TinyPacket.TinyPacketAddBall(builder, ballOffset)
                packet_offset = TinyPacket.TinyPacketEnd(builder)

                builder.Finish(packet_offset)
                buffer = bytes(builder.Output())

                filtered_sockets = {s for s in self.current_sockets if s.open}
                for socket in filtered_sockets:
                    await socket.send(buffer)

                self.current_sockets = filtered_sockets

            after = datetime.now()
            duration = (after - before).total_seconds()

            sleep_secs = 1 / 60 - duration
            if sleep_secs > 0:
                await asyncio.sleep(sleep_secs)

    def get_game_time(self, game_tick_flat):
        try:
            return game_tick_flat.GameInfo().SecondsElapsed()
        except AttributeError:
            return 0.0

    def convert_to_flatbuffer(self, json_state: dict, index: int):
        builder = flatbuffers.Builder(0)

        ControllerState.ControllerStateStart(builder)
        ControllerState.ControllerStateAddSteer(builder, json_state['steer'])
        ControllerState.ControllerStateAddThrottle(builder,
                                                   json_state['throttle'])
        ControllerState.ControllerStateAddPitch(builder, json_state['pitch'])
        ControllerState.ControllerStateAddYaw(builder, json_state['yaw'])
        ControllerState.ControllerStateAddRoll(builder, json_state['roll'])
        ControllerState.ControllerStateAddJump(builder, json_state['jump'])
        ControllerState.ControllerStateAddBoost(builder, json_state['boost'])
        ControllerState.ControllerStateAddHandbrake(builder,
                                                    json_state['handbrake'])
        controller_state = ControllerState.ControllerStateEnd(builder)

        PlayerInput.PlayerInputStart(builder)
        PlayerInput.PlayerInputAddPlayerIndex(builder, index)
        PlayerInput.PlayerInputAddControllerState(builder, controller_state)
        player_input = PlayerInput.PlayerInputEnd(builder)

        builder.Finish(player_input)
        return builder
Ejemplo n.º 2
0
class ScratchManager(BotHelperProcess):
    def __init__(self, agent_metadata_queue, quit_event, options):
        super().__init__(agent_metadata_queue, quit_event, options)
        self.logger = get_logger('scratch_mgr')
        self.game_interface = GameInterface(self.logger)
        self.current_sockets = set()
        self.running_indices = set()
        self.metadata_map = dict()
        self.port: int = options['port']
        self.sb3_file = options['sb3-file']
        self.pretend_blue_team = options['pretend_blue_team']
        self.has_received_input = False
        self.scratch_index_to_rlbot = {}
        self.should_flip_field = False

    async def data_exchange(self, websocket, path):
        async for message in websocket:
            try:
                controller_states = json.loads(message)
                if not self.has_received_input:
                    self.has_received_input = True
                    self.logger.info(
                        f"Just got first input from Scratch {self.sb3_file} {self.port}"
                    )

                for key, scratch_state in controller_states.items():
                    scratch_index = int(key)
                    rlbot_index = self.get_rlbot_index(scratch_index)
                    self.game_interface.update_player_input_flat(
                        self.convert_to_flatbuffer(scratch_state, rlbot_index))
            except UnicodeDecodeError:
                backup_location = os.path.join(
                    os.path.dirname(self.sb3_file),
                    f'backup-{str(int(os.path.getmtime(self.sb3_file)))}.sb3')
                print(
                    f"Saving new version of {self.sb3_file} and backing up old version to {backup_location}"
                )
                shutil.move(self.sb3_file, backup_location)
                with open(self.sb3_file, 'wb') as output:
                    output.write(message)

            self.current_sockets.add(websocket)

    def try_receive_agent_metadata(self):
        """
        As agents start up, they will dump their configuration into the metadata_queue.
        Read from it to learn about all the bots intending to use this scratch manager.
        """
        while True:  # will exit on queue.Empty
            try:
                single_agent_metadata: AgentMetadata = self.metadata_queue.get(
                    timeout=0.1)
                self.running_indices.add(single_agent_metadata.index)
                self.metadata_map[
                    single_agent_metadata.index] = single_agent_metadata
            except queue.Empty:
                return
            except Exception as ex:
                self.logger.error(ex)

    def start(self):
        self.logger.info("Starting scratch manager")

        self.game_interface.load_interface()

        # Wait a moment for all agents to have a chance to start up and send metadata
        time.sleep(1)
        self.try_receive_agent_metadata()

        self.logger.info(self.running_indices)

        num_scratch_bots = len(self.running_indices)
        if num_scratch_bots == 0:
            self.logger.error(
                "No scratch bots registered in the scratch manager, exiting!")
            return

        self.setup_index_map()

        all_orange = sum(
            map(lambda meta: meta.team,
                self.metadata_map.values())) == len(self.running_indices)
        self.should_flip_field = self.pretend_blue_team and all_orange

        if self.options['spawn_browser']:
            options = Options()
            options.headless = self.options['headless']

            # This prevents an error message about AudioContext when running in headless mode.
            options.add_argument("--autoplay-policy=no-user-gesture-required")

            players_string = ",".join(
                map(index_to_player_string, range(len(self.running_indices))))

            try:
                driver_path = self.get_driver_path_retryable()
                driver = webdriver.Chrome(driver_path, chrome_options=options)
                driver.get(
                    f"http://scratch.rlbot.org?host=localhost:{str(self.port)}&players={players_string}&awaitBotFile=1"
                )

                if self.sb3_file is not None:
                    element = WebDriverWait(driver, 10).until(
                        EC.presence_of_element_located(
                            (By.ID, "sb3-selenium-uploader")))
                    element.send_keys(self.sb3_file)
                    self.logger.info(
                        f'Loaded sb3 file {self.sb3_file} into chrome window.')

            except SessionNotCreatedException:
                # This can happen if the downloaded chromedriver does not match the version of Chrome that is installed.
                webbrowser.open_new(
                    f"http://scratch.rlbot.org?host=localhost:{str(self.port)}&players={players_string}"
                )
                self.logger.info(
                    f"Could not load the Scratch file automatically! You'll need to upload it yourself "
                    f"from {self.sb3_file}")

        self.logger.info(f'Starting websocket server on port {self.port}')
        asyncio.get_event_loop().run_until_complete(
            websockets.serve(self.data_exchange, port=self.port))
        asyncio.get_event_loop().run_until_complete(self.game_loop())

    def get_driver_path_retryable(self) -> str:
        # Sometimes this code runs concurrently, e.g. if there are many scratch bots which are all
        # using separate_browsers = True. In that case, ChromeDriverManager().install() can run into contention
        # when trying to access the same zip file. Ideally we would use a proper lock here, but that would require
        # a bit of refactoring, and I want to get this experimental fix out to one of our users quickly.
        for _ in range(6):
            try:
                return ChromeDriverManager().install()
            except:
                time.sleep(random())
        return None

    def setup_index_map(self):
        num_scratch_bots = len(self.running_indices)
        sorted_indices = list(self.running_indices)
        sorted_indices.sort()
        self.scratch_index_to_rlbot = {
            i: r
            for i, r in enumerate(sorted_indices)
        }

        leftovers = [
            i for i in range(0, max(self.running_indices))
            if i not in self.running_indices
        ]
        leftovers.sort()
        for i in range(num_scratch_bots, num_scratch_bots + len(leftovers)):
            self.scratch_index_to_rlbot[i] = leftovers[i - num_scratch_bots]

    def get_rlbot_index(self, scratch_index):
        if scratch_index in self.scratch_index_to_rlbot:
            return self.scratch_index_to_rlbot[scratch_index]
        return scratch_index

    async def game_loop(self):

        last_tick_game_time = None  # What the tick time of the last observed tick was
        last_call_real_time = datetime.now()  # When we last called the Agent

        packet = GameTickPacket()

        # Run until main process tells to stop
        while not self.quit_event.is_set():
            before = datetime.now()

            self.game_interface.update_live_data_packet(packet)

            # Run the Agent only if the gameInfo has updated.
            tick_game_time = packet.game_info.seconds_elapsed
            worth_communicating = tick_game_time != last_tick_game_time or \
                                  datetime.now() - last_call_real_time >= MAX_AGENT_CALL_PERIOD

            ball = packet.game_ball
            if ball is not None and worth_communicating and max(
                    self.running_indices) < packet.num_cars:
                last_tick_game_time = tick_game_time
                last_call_real_time = datetime.now()

                tiny_player_offsets = []
                builder = flatbuffers.Builder(0)

                for i in range(packet.num_cars):
                    tiny_player_offsets.append(
                        self.copy_player(packet.game_cars[i], builder))

                TinyPacket.TinyPacketStartPlayersVector(
                    builder, packet.num_cars)
                for i in reversed(range(0, len(tiny_player_offsets))):
                    rlbot_index = self.get_rlbot_index(i)
                    builder.PrependUOffsetTRelative(
                        tiny_player_offsets[rlbot_index])
                players_offset = builder.EndVector(len(tiny_player_offsets))

                ballOffset = self.copy_ball(ball, builder)

                TinyPacket.TinyPacketStart(builder)
                TinyPacket.TinyPacketAddPlayers(builder, players_offset)
                TinyPacket.TinyPacketAddBall(builder, ballOffset)
                packet_offset = TinyPacket.TinyPacketEnd(builder)

                builder.Finish(packet_offset)
                buffer = bytes(builder.Output())

                filtered_sockets = {s for s in self.current_sockets if s.open}
                for socket in filtered_sockets:
                    await socket.send(buffer)

                self.current_sockets = filtered_sockets

            after = datetime.now()
            duration = (after - before).total_seconds()

            sleep_secs = 1 / 60 - duration
            if sleep_secs > 0:
                await asyncio.sleep(sleep_secs)

    def convert_to_flatbuffer(self, json_state: dict, index: int):
        builder = flatbuffers.Builder(0)

        ControllerState.ControllerStateStart(builder)
        ControllerState.ControllerStateAddSteer(builder, json_state['steer'])
        ControllerState.ControllerStateAddThrottle(builder,
                                                   json_state['throttle'])
        ControllerState.ControllerStateAddPitch(builder, json_state['pitch'])
        ControllerState.ControllerStateAddYaw(builder, json_state['yaw'])
        ControllerState.ControllerStateAddRoll(builder, json_state['roll'])
        ControllerState.ControllerStateAddJump(builder, json_state['jump'])
        ControllerState.ControllerStateAddBoost(builder, json_state['boost'])
        ControllerState.ControllerStateAddHandbrake(builder,
                                                    json_state['handbrake'])

        # This may throw a KeyError for anyone using old cached javascript. You should hard-refresh scratch.rlbot.org.
        ControllerState.ControllerStateAddUseItem(builder,
                                                  json_state['useItem'])
        controller_state = ControllerState.ControllerStateEnd(builder)

        PlayerInput.PlayerInputStart(builder)
        PlayerInput.PlayerInputAddPlayerIndex(builder, index)
        PlayerInput.PlayerInputAddControllerState(builder, controller_state)
        player_input = PlayerInput.PlayerInputEnd(builder)

        builder.Finish(player_input)
        return builder

    def copy_v3(self, v3, builder):
        if self.should_flip_field:
            return Vector3.CreateVector3(builder, -v3.x, -v3.y, v3.z)
        return Vector3.CreateVector3(builder, v3.x, v3.y, v3.z)

    def copy_rot(self, rot, builder):
        yaw = rot.yaw
        if self.should_flip_field:
            yaw = yaw + math.pi if yaw < 0 else yaw - math.pi
        return Rotator.CreateRotator(builder, rot.pitch, yaw, rot.roll)

    def copy_player(self, player, builder):
        TinyPlayer.TinyPlayerStart(builder)
        TinyPlayer.TinyPlayerAddLocation(
            builder, self.copy_v3(player.physics.location, builder))
        TinyPlayer.TinyPlayerAddVelocity(
            builder, self.copy_v3(player.physics.velocity, builder))
        TinyPlayer.TinyPlayerAddRotation(
            builder, self.copy_rot(player.physics.rotation, builder))
        TinyPlayer.TinyPlayerAddTeam(
            builder,
            invert_team(player.team)
            if self.should_flip_field else player.team)
        TinyPlayer.TinyPlayerAddBoost(builder, player.boost)
        return TinyPlayer.TinyPlayerEnd(builder)

    def copy_ball(self, ball, builder):
        phys = ball.physics
        TinyBall.TinyBallStart(builder)
        TinyBall.TinyBallAddLocation(builder,
                                     self.copy_v3(phys.location, builder))
        TinyBall.TinyBallAddVelocity(builder,
                                     self.copy_v3(phys.velocity, builder))
        return TinyBall.TinyBallEnd(builder)
Ejemplo n.º 3
0
class ScratchManager(BotHelperProcess):
    def __init__(self, agent_metadata_queue, quit_event):
        super().__init__(agent_metadata_queue, quit_event)
        self.logger = get_logger('scratch_mgr')
        self.game_interface = GameInterface(self.logger)
        self.current_sockets = set()

    async def data_exchange(self, websocket, path):
        async for message in websocket:
            controller_states = json.loads(message)

            for key, scratch_state in controller_states.items():
                self.game_interface.update_player_input_flat(
                    self.convert_to_flatbuffer(scratch_state, int(key)))

            self.current_sockets.add(websocket)

    def start(self):
        self.logger.info("Starting scratch manager")

        self.game_interface.load_interface()

        asyncio.get_event_loop().run_until_complete(
            websockets.serve(self.data_exchange, port=PORT))
        asyncio.get_event_loop().run_until_complete(self.game_loop())

    async def game_loop(self):

        last_tick_game_time = None  # What the tick time of the last observed tick was
        last_call_real_time = datetime.now()  # When we last called the Agent

        # Run until main process tells to stop
        while not self.quit_event.is_set():
            before = datetime.now()

            game_tick_flat_binary = self.game_interface.get_live_data_flat_binary(
            )
            if game_tick_flat_binary is None:
                continue

            game_tick_flat = GameTickPacket.GameTickPacket.GetRootAsGameTickPacket(
                game_tick_flat_binary, 0)

            # Run the Agent only if the gameInfo has updated.
            tick_game_time = self.get_game_time(game_tick_flat)
            worth_communicating = tick_game_time != last_tick_game_time or \
                                  datetime.now() - last_call_real_time >= MAX_AGENT_CALL_PERIOD

            ball = game_tick_flat.Ball()
            if ball is not None and worth_communicating:
                last_tick_game_time = tick_game_time
                last_call_real_time = datetime.now()

                tiny_player_offsets = []
                builder = flatbuffers.Builder(0)

                for i in range(game_tick_flat.PlayersLength()):
                    tiny_player_offsets.append(
                        copy_player(game_tick_flat.Players(i), builder))

                TinyPacket.TinyPacketStartPlayersVector(
                    builder, game_tick_flat.PlayersLength())
                for i in reversed(range(0, len(tiny_player_offsets))):
                    builder.PrependUOffsetTRelative(tiny_player_offsets[i])
                players_offset = builder.EndVector(len(tiny_player_offsets))

                ballOffset = copy_ball(ball, builder)

                TinyPacket.TinyPacketStart(builder)
                TinyPacket.TinyPacketAddPlayers(builder, players_offset)
                TinyPacket.TinyPacketAddBall(builder, ballOffset)
                packet_offset = TinyPacket.TinyPacketEnd(builder)

                builder.Finish(packet_offset)
                buffer = bytes(builder.Output())

                filtered_sockets = {s for s in self.current_sockets if s.open}
                for socket in filtered_sockets:
                    await socket.send(buffer)

                self.current_sockets = filtered_sockets

            after = datetime.now()
            duration = (after - before).total_seconds()

            sleep_secs = 1 / 60 - duration
            if sleep_secs > 0:
                await asyncio.sleep(sleep_secs)

    def get_game_time(self, game_tick_flat):
        try:
            return game_tick_flat.GameInfo().SecondsElapsed()
        except AttributeError:
            return 0.0

    def convert_to_flatbuffer(self, json_state: dict, index: int):
        builder = flatbuffers.Builder(0)

        ControllerState.ControllerStateStart(builder)
        ControllerState.ControllerStateAddSteer(builder, json_state['steer'])
        ControllerState.ControllerStateAddThrottle(builder,
                                                   json_state['throttle'])
        ControllerState.ControllerStateAddPitch(builder, json_state['pitch'])
        ControllerState.ControllerStateAddYaw(builder, json_state['yaw'])
        ControllerState.ControllerStateAddRoll(builder, json_state['roll'])
        ControllerState.ControllerStateAddJump(builder, json_state['jump'])
        ControllerState.ControllerStateAddBoost(builder, json_state['boost'])
        ControllerState.ControllerStateAddHandbrake(builder,
                                                    json_state['handbrake'])
        controller_state = ControllerState.ControllerStateEnd(builder)

        PlayerInput.PlayerInputStart(builder)
        PlayerInput.PlayerInputAddPlayerIndex(builder, index)
        PlayerInput.PlayerInputAddControllerState(builder, controller_state)
        player_input = PlayerInput.PlayerInputEnd(builder)

        builder.Finish(player_input)
        return builder