Esempio n. 1
0
class PythonHivemind(BotHelperProcess):

    # Terminology:
    # hivemind - main process controlling the drones.
    # drone - a bot under the hivemind's control.

    # Path to the executable. NOT USED BY PYTHON HIVEMINDS!
    exec_path = None

    def __init__(self, agent_metadata_queue, quit_event, options):
        super().__init__(agent_metadata_queue, quit_event, options)

        # Sets up the logger. The string is the name of your hivemind.
        # This is configured inside your config file under hivemind_name.
        self.logger = get_logger(options['name'])

        # Give a warning if exec_path was given.
        if self.exec_path is not None:
            self.logger.warning(
                "An exec_path was given, but this is a PythonHivemind, and so it is ignored."
            )
            self.logger.warning(
                "  Try subclassing SubprocessHivemind if you want to use an executable."
            )

        # The game interface is how you get access to things
        # like ball prediction, the game tick packet, or rendering.
        self.game_interface = GameInterface(self.logger)

        # drone_indices is a set of bot indices
        # which requested this hivemind with the same key.
        self.drone_indices = set()

    def try_receive_agent_metadata(self):
        """Adds all drones with the correct key to our set of running indices."""
        while not self.metadata_queue.empty():
            # Tries to get the next agent from the queue.
            single_agent_metadata: AgentMetadata = self.metadata_queue.get(
                timeout=1.0)
            # Adds the index to the drone_indices.
            self.drone_indices.add(single_agent_metadata.index)

    def start(self):
        """Starts the BotHelperProcess - Hivemind."""
        # Prints an activation message into the console.
        # This lets you know that the process is up and running.
        self.logger.debug("Hello, world!")

        # Loads game interface.
        self.game_interface.load_interface()

        # Collect drone indices that requested a helper process with our key.
        self.logger.info("Collecting drones; give me a moment.")
        self.try_receive_agent_metadata()
        self.logger.info("Ready to go!")

        # Runs the game loop where the hivemind will spend the rest of its time.
        self.__game_loop()

    def __game_loop(self):
        """
        The bot hivemind will stay in this loop for the whole game. 
        This is where the initialize_hive and get_outputs functions are called.
        """

        # Creating ball prediction and field info objects to later update in wrapper methods.
        self._ball_prediction = BallPrediction()
        self._field_info = FieldInfoPacket()
        self.game_interface.update_field_info_packet(self._field_info)
        # Wrapper for renderer.
        self.renderer: RenderingManager = self.game_interface.renderer

        # Create packet object.
        packet = GameTickPacket()
        # Uses one of the drone indices as a key.
        key = next(iter(self.drone_indices))
        self.game_interface.fresh_live_data_packet(packet, 20, key)

        # Initialization step for your hivemind.
        self.initialize_hive(packet)

        while not self.quit_event.is_set():
            try:
                # Updating the packet.
                self.game_interface.fresh_live_data_packet(packet, 20, key)

                # Get outputs from hivemind for each bot.
                # Outputs are expected to be a Dict[int, PlayerInput]
                outputs = self.get_outputs(packet)

                if outputs is None:
                    self.logger.error("No outputs were returned.")
                    self.logger.error(
                        "  Try putting `return {i: PlayerInput() for i in self.drone_indices}`"
                    )
                    self.logger.error("  in `get_outputs()` to troubleshoot.")
                    continue

                if len(outputs) < len(self.drone_indices):
                    self.logger.error("Not enough outputs were given.")

                elif len(outputs) > len(self.drone_indices):
                    self.logger.error("Too many outputs were given.")

                # Send the drone inputs to the drones.
                for index in outputs:
                    if index not in self.drone_indices:
                        self.logger.error(
                            "Tried to send output to bot index not in drone_indices."
                        )
                        continue
                    output = outputs[index]
                    self.game_interface.update_player_input(output, index)

            except Exception:
                traceback.print_exc()

    # Override these methods:

    def initialize_hive(self, packet: GameTickPacket) -> None:
        """
        Override this method if you want to have an initialization step for your hivemind.
        """
        pass

    def get_outputs(self, packet: GameTickPacket) -> Dict[int, PlayerInput]:
        """Where all the logic of your hivemind gets its input and returns its outputs for each drone.

        Use self.drone_indices to access the set of bot indices your hivemind controls.

        Arguments:
            packet {GameTickPacket} -- see https://github.com/RLBot/RLBot/wiki/Input-and-Output-Data-(current)

        Returns:
            Dict[int, PlayerInput] -- A dictionary with drone indices as keys and corresponding PlayerInputs as values.
        """
        return {index: PlayerInput() for index in self.drone_indices}

    # Wrapper methods to make them the same as if you were making a normal python bot:

    def get_ball_prediction_struct(self) -> BallPrediction:
        self.game_interface.update_ball_prediction(self._ball_prediction)
        return self._ball_prediction

    def get_field_info(self) -> FieldInfoPacket:
        # Field info does not need to be updated.
        return self._field_info

    def get_match_settings(self) -> MatchSettings:
        return self.game_interface.get_match_settings()

    def set_game_state(self, game_state: GameState) -> None:
        self.game_interface.set_game_state(game_state)
Esempio n. 2
0
class Hivemind:
    """
    Sends and receives data from Rocket League, and maintains the list of drones.
    """

    # Some terminology:
    # hivemind = the process which controls the drones.
    # drone = a bot under the hivemind's control.

    def __init__(self, queue, choreo_obj):
        # Sets up the logger. The string is the name of your hivemind.
        # Call this something unique so people can differentiate between hiveminds.
        self.logger = get_logger('Choreography Hivemind')

        # The game interface is how you get access to things
        # like ball prediction, the game tick packet, or rendering.
        self.game_interface = GameInterface(self.logger)

        self.drones = []

        self.choreo = choreo_obj(self.game_interface)
        self.choreo.generate_sequence(self.drones)

        # Set up queue to know when to stop and reload.
        self.queue = queue

    def start(self):
        """Runs once, sets up the hivemind and its agents."""
        # Prints an activation message into the console.
        # This let's you know that the process is up and running.
        self.logger.info("Hello World!")

        # Loads game interface.
        self.game_interface.load_interface()

        # This is how you access field info.
        # First create the initialise the object...
        field_info = FieldInfoPacket()
        # Then update it.
        self.game_interface.update_field_info_packet(field_info)

        # Same goes for the packet, but that is
        # also updated in the main loop every tick.
        packet = GameTickPacket()
        self.game_interface.update_live_data_packet(packet)

        # Initialise drones list. Will be filled with Drone objects for every drone.
        self.drones = []

        # Runs the game loop where the hivemind will spend the rest of its time.
        self.game_loop()

    def game_loop(self):

        # Creating packet which will be updated every tick.
        packet = GameTickPacket()

        # MAIN LOOP:
        while self.loop_check():
            #print('test')

            prev_time = packet.game_info.seconds_elapsed
            # Updating the game tick packet.
            self.game_interface.update_live_data_packet(packet)

            # Checking if packet is new, otherwise sleep.
            if prev_time == packet.game_info.seconds_elapsed:
                time.sleep(0.001)
                continue

            # Create a Drone object for every drone that holds its information.
            if packet.num_cars > len(self.drones):
                # Clears the list if there are more cars than drones.
                self.drones.clear()
                for index in range(packet.num_cars):
                    self.drones.append(
                        Drone(index, packet.game_cars[index].team))

            # Processing drone data.
            for drone in self.drones:
                drone.update(packet.game_cars[drone.index],
                             packet.game_info.seconds_elapsed)

            # Steps through the choreography.
            self.choreo.step(packet, self.drones)

            # Resets choreography once it has finished.
            if self.choreo.finished:
                # Re-instantiates the choreography.
                self.choreo = self.choreo.__class__(self.game_interface)
                self.choreo.generate_sequence(self.drones)

            # Sends the drone inputs to the drones.
            for drone in self.drones:
                self.game_interface.update_player_input(
                    convert_player_input(drone.ctrl), drone.index)

    def loop_check(self):
        """
        Checks whether the hivemind should keep looping or should die.
        """
        if self.queue.empty():
            return True

        else:
            message = self.queue.get()
            return message != QCommand.STOP
Esempio n. 3
0
class BotManager:
    def __init__(self, terminate_request_event, termination_complete_event,
                 reload_request_event, bot_configuration, name, team, index,
                 agent_class_wrapper, agent_metadata_queue,
                 match_config: MatchConfig, matchcomms_root: URL,
                 spawn_id: int):
        """
        :param terminate_request_event: an Event (multiprocessing) which will be set from the outside when the program is trying to terminate
        :param termination_complete_event: an Event (multiprocessing) which should be set from inside this class when termination has completed successfully
        :param reload_request_event: an Event (multiprocessing) which will be set from the outside to force a reload of the agent
        :param reload_complete_event: an Event (multiprocessing) which should be set from inside this class when reloading has completed successfully
        :param bot_configuration: parameters which will be passed to the bot's constructor
        :param name: name which will be passed to the bot's constructor. Will probably be displayed in-game.
        :param team: 0 for blue team or 1 for orange team. Will be passed to the bot's constructor.
        :param index: The player index, i.e. "this is player number <index>". Will be passed to the bot's constructor.
            Can be used to pull the correct data corresponding to the bot's car out of the game tick packet.
        :param agent_class_wrapper: The ExternalClassWrapper object that can be used to load and reload the bot
        :param agent_metadata_queue: a Queue (multiprocessing) which expects to receive AgentMetadata once available.
        :param match_config: Describes the match that is being played.
        :param matchcomms_root: The server to connect to if you want to communicate to other participants in the match.
        :param spawn_id: The identifier we expect to see in the game tick packet at our player index. If it does not
            match, then we will force the agent to retire. Pass None to opt out of this behavior.
        """
        self.terminate_request_event = terminate_request_event
        self.termination_complete_event = termination_complete_event
        self.reload_request_event = reload_request_event
        self.bot_configuration = bot_configuration
        self.name = name
        self.team = team
        self.index = index
        self.agent_class_wrapper = agent_class_wrapper
        self.agent_metadata_queue = agent_metadata_queue
        self.logger = get_logger('bot' + str(self.index))
        self.game_interface = GameInterface(self.logger)
        self.last_chat_time = time.time()
        self.chat_counter = 0
        self.reset_chat_time = True
        self.game_tick_packet = None
        self.bot_input = None
        self.ball_prediction = None
        self.rigid_body_tick = None
        self.match_config = match_config
        self.matchcomms_root = matchcomms_root
        self.last_message_index = 0
        self.agent = None
        self.agent_class_file = None
        self.last_module_modification_time = 0
        self.scan_last = 0
        self.scan_temp = 0
        self.file_iterator = None
        self.maximum_tick_rate_preference = bot_configuration.get(
            BOT_CONFIG_MODULE_HEADER, MAXIMUM_TICK_RATE_PREFERENCE_KEY)
        self.spawn_id = spawn_id
        self.spawn_id_seen = False
        self.counter = 0

    def send_quick_chat_from_agent(self, team_only, quick_chat):
        """
        Passes the agents quick chats to the game, and also to other python bots.
        This does perform limiting.
        You are limited to 5 quick chats in a 2 second period starting from the first chat.
        This means you can spread your chats out to be even within that 2 second period.
        You could spam them in the first little bit but then will be throttled.
        """

        # Send the quick chat to the game
        rlbot_status = send_quick_chat_flat(self.game_interface, self.index,
                                            self.team, team_only, quick_chat)

        if rlbot_status == RLBotCoreStatus.QuickChatRateExceeded:
            self.logger.debug('quick chat disabled')

    def load_agent(self) -> Tuple[BaseAgent, Path]:
        """
        Loads and initializes an agent using instance variables, registers for quick chat and sets render functions.
        :return: An instance of an agent, and the agent class file.
        """
        agent_class = self.agent_class_wrapper.get_loaded_class()
        self.agent = agent_class(self.name, self.team, self.index)
        self.agent._set_spawn_id(self.spawn_id)
        self.agent.matchcomms_root = self.matchcomms_root
        self.agent.init_match_config(self.match_config)

        self.agent.load_config(
            self.bot_configuration.get_header("Bot Parameters"))

        self.update_metadata_queue()
        self.set_render_manager()

        self.agent_class_file = self.agent_class_wrapper.python_file
        self.agent._register_quick_chat(self.send_quick_chat_from_agent)
        self.agent._register_field_info(self.get_field_info)
        self.agent._register_set_game_state(self.set_game_state)
        self.agent._register_ball_prediction(self.get_ball_prediction)
        self.agent._register_ball_prediction_struct(
            self.get_ball_prediction_struct)
        self.agent._register_get_rigid_body_tick(self.get_rigid_body_tick)
        self.agent._register_match_settings_func(self.get_match_settings)

        # Once all engine setup is done, do the agent-specific initialization, if any:
        self.agent.initialize_agent()
        return self.agent, self.agent_class_file

    def set_render_manager(self):
        """
        Sets the render manager for the agent.
        :param agent: An instance of an agent.
        """
        rendering_manager = self.game_interface.renderer.get_rendering_manager(
            self.index, self.team)
        self.agent._set_renderer(rendering_manager)

    def update_metadata_queue(self):
        """
        Adds a new instance of AgentMetadata into the `agent_metadata_queue` using `agent` data.
        :param agent: An instance of an agent.
        """
        pids = {os.getpid(), *self.agent.get_extra_pids()}

        helper_process_request = self.agent.get_helper_process_request()

        self.agent_metadata_queue.put(
            AgentMetadata(self.index, self.name, self.team, pids,
                          helper_process_request))

    def reload_agent(self):
        """
        Reloads the agent. Can throw exceptions. External classes should use reload_event.set() instead.
        """
        self.logger.info('Reloading Agent: ' + self.agent.name)
        self.agent_class_wrapper.reload()
        old_agent = self.agent
        self.load_agent()
        self.retire_agent(
            old_agent)  # We do this after load_agent as load_agent might fail.

    def wait_for_full_data(self):
        for i in range(10):
            match_settings = self.get_match_settings()
            if match_settings is not None and self.get_field_info(
            ).num_goals > 0:
                return
            time.sleep(0.1)
        self.logger.error(
            "WARNING: failed to get full match data before starting bot!")

    def run(self):
        """
        Loads interface for RLBot, prepares environment and agent, and calls the update for the agent.
        """
        self.logger.debug('initializing agent')
        self.game_interface.load_interface()
        self.wait_for_full_data()

        self.prepare_for_run()

        if self.match_config is None:
            match_settings = self.game_interface.get_match_settings()
            self.match_config = MatchConfig.from_match_settings_flatbuffer(
                match_settings)

        last_tick_game_time = 0  # What the tick time of the last observed tick was
        last_call_real_time = datetime.now()  # When we last called the Agent
        frame_urgency = 0  # If the bot is getting called more than its preferred max rate, urgency will go negative.

        # Get bot module
        self.load_agent()

        self.last_module_modification_time = self.check_modification_time(
            os.path.dirname(self.agent_class_file))

        # Run until main process tells to stop, or we detect Ctrl+C
        try:
            while not self.terminate_request_event.is_set():
                self.pull_data_from_game()

                # Run the Agent only if the game_info has updated.
                tick_game_time = self.get_game_time()
                now = datetime.now()
                should_call_while_paused = now - last_call_real_time >= MAX_AGENT_CALL_PERIOD or self.match_config.enable_lockstep

                if frame_urgency < 1 / self.maximum_tick_rate_preference:
                    # Urgency increases every frame, but don't let it build up a large backlog
                    frame_urgency += tick_game_time - last_tick_game_time

                if tick_game_time != last_tick_game_time and frame_urgency >= 0 or should_call_while_paused:
                    last_call_real_time = now
                    # Urgency decreases when a tick is processed.
                    if frame_urgency > 0:
                        frame_urgency -= 1 / self.maximum_tick_rate_preference

                    self.perform_tick()
                    self.counter += 1

                last_tick_game_time = tick_game_time
                if self.spawn_id is not None:
                    packet_spawn_id = self.get_spawn_id()
                    if self.spawn_id_seen:
                        if packet_spawn_id != self.spawn_id:
                            self.logger.warn(
                                f"The bot's spawn id {self.spawn_id} does not match the one in the packet "
                                f"{packet_spawn_id}, retiring!")
                            break
                    elif packet_spawn_id == self.spawn_id and self.game_tick_packet.game_info.is_round_active:
                        self.spawn_id_seen = True

        except KeyboardInterrupt:
            self.terminate_request_event.set()

        self.retire_agent(self.agent)

        # If terminated, send callback
        self.termination_complete_event.set()

    def perform_tick(self):
        # Reload the Agent if it has been modified or if reload is requested from outside.
        # But only do that every 20th tick.
        if self.agent.is_hot_reload_enabled() and self.counter % 20 == 1:
            self.hot_reload_if_necessary()
        try:
            chat_messages = self.game_interface.receive_chat(
                self.index, self.team, self.last_message_index)
            for i in range(0, chat_messages.MessagesLength()):
                message = chat_messages.Messages(i)
                if len(self.match_config.player_configs) > message.PlayerIndex(
                ):
                    self.agent.handle_quick_chat(
                        index=message.PlayerIndex(),
                        team=self.match_config.player_configs[
                            message.PlayerIndex()].team,
                        quick_chat=message.QuickChatSelection())
                else:
                    self.logger.debug(
                        f"Skipping quick chat delivery for {message.MessageIndex()} because "
                        "we don't recognize the player index. Probably stale.")
                self.last_message_index = message.MessageIndex()
        except EmptyDllResponse:
            self.logger.debug("Empty response when reading chat!")
        # Call agent
        try:
            self.call_agent(self.agent,
                            self.agent_class_wrapper.get_loaded_class())
        except Exception as e:
            self.logger.error("Call to agent failed:\n" +
                              traceback.format_exc())

    def hot_reload_if_necessary(self):
        try:
            new_module_modification_time = self.check_modification_time(
                os.path.dirname(self.agent_class_file))
            if new_module_modification_time != self.last_module_modification_time or self.reload_request_event.is_set(
            ):
                self.reload_request_event.clear()
                self.last_module_modification_time = new_module_modification_time
                # Clear the render queue on reload.
                if hasattr(self.agent, 'renderer') and isinstance(
                        self.agent.renderer, RenderingManager):
                    self.agent.renderer.clear_all_touched_render_groups()
                self.reload_agent()
        except FileNotFoundError:
            self.logger.error(
                f"Agent file {self.agent_class_file} was not found. Will try again."
            )
            time.sleep(0.5)
        except Exception:
            self.logger.error("Reloading the agent failed:\n" +
                              traceback.format_exc())
            time.sleep(
                5
            )  # Avoid burning CPU, and give the user a moment to read the log

    def retire_agent(self, agent):
        # Shut down the bot by calling cleanup functions.
        if hasattr(agent, 'retire'):
            try:
                agent.retire()
            except Exception as e:
                self.logger.error("Retiring the agent failed:\n" +
                                  traceback.format_exc())
        if hasattr(agent, 'renderer') and isinstance(agent.renderer,
                                                     RenderingManager):
            agent.renderer.clear_all_touched_render_groups()
        # Zero out the inputs, so it's more obvious that the bot has stopped.
        self.game_interface.update_player_input(PlayerInput(), self.index)

        # Don't trust the agent to shut down its own client in retire().
        if agent._matchcomms is not None:
            agent._matchcomms.close()

    def check_modification_time(self, directory, timeout_ms=1):
        if self.scan_last > 0 and timeout_ms is not None:
            stop_time = time.perf_counter_ns() + timeout_ms * 10**6
        else:
            stop_time = None
        if self.file_iterator is None:
            self.file_iterator = glob.iglob(f"{directory}/**/*.py",
                                            recursive=True)
        for f in self.file_iterator:
            self.scan_temp = max(self.scan_temp, os.stat(f).st_mtime)
            if stop_time is not None and time.perf_counter_ns() > stop_time:
                # Timeout exceeded. The scan will pick up from here on the next call.
                break
        else:
            # Scan finished. Update the modification time and restart the scan:
            self.scan_last, self.scan_temp = self.scan_temp, 0
            self.file_iterator = None
        return self.scan_last

    def get_field_info(self) -> FieldInfoPacket:
        field_info = FieldInfoPacket()
        self.game_interface.update_field_info_packet(field_info)
        return field_info

    def get_rigid_body_tick(self):
        """Get the most recent state of the physics engine."""
        return self.game_interface.update_rigid_body_tick(self.rigid_body_tick)

    def set_game_state(self, game_state: GameState) -> None:
        self.game_interface.set_game_state(game_state)

    def get_ball_prediction(self):
        return self.game_interface.get_ball_prediction()

    def get_match_settings(self) -> MatchSettings:
        return self.game_interface.get_match_settings()

    def get_ball_prediction_struct(self):
        raise NotImplementedError

    def prepare_for_run(self):
        raise NotImplementedError

    def call_agent(self, agent: BaseAgent, agent_class):
        raise NotImplementedError

    def get_game_time(self):
        raise NotImplementedError

    def pull_data_from_game(self):
        raise NotImplementedError

    def get_spawn_id(self):
        raise NotImplementedError
Esempio n. 4
0
class Hivemind(BotHelperProcess):
    # TODO Maybe use __slots__ for better performance?

    def __init__(self, agent_metadata_queue, quit_event, options):
        super().__init__(agent_metadata_queue, quit_event, options)
        self.logger = get_logger('Hivemind')
        self.game_interface = GameInterface(self.logger)
        self.running_indices = set()

    def try_receive_agent_metadata(self):
        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):
        """Runs once, sets up the hivemind and its agents."""
        # Prints stuff into the console.
        self.logger.info("Hivemind A C T I V A T E D")
        message = random.choice([
            "Breaking the meta",
            "Welcoming r0bbi3",
            "Annoying chip by reinventing the wheel",
            "Actually texting her",
            "Banning anime",
            "Killing that guy",
            "Trying to pronounce jeroen",
            "Getting banned by Redox",
            "Becomind a mod",
        ])
        self.logger.info(message)

        # Loads game interface.
        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()

        # Runs the game loop where the hivemind will spend the rest of its time.
        self.game_loop()

    def game_loop(self):
        """The main game loop. This is where your hivemind code goes."""

        # Setting up rate limiter.
        rate_limit = rate_limiter.RateLimiter(120)

        # Setting up data.
        field_info = FieldInfoPacket()
        self.game_interface.update_field_info_packet(field_info)
        packet = GameTickPacket()
        self.game_interface.update_live_data_packet(packet)

        data.setup(self, packet, field_info, self.running_indices)

        self.ball.predict = BallPrediction()
        # https://github.com/RLBot/RLBotPythonExample/wiki/Ball-Path-Prediction

        # MAIN LOOP:
        while True:
            # Updating the game packet from the game.
            self.game_interface.update_live_data_packet(packet)

            # Processing packet.
            data.process(self, packet)

            # Ball prediction.
            self.game_interface.update_ball_prediction(self.ball.predict)

            # Planning.
            brain.plan(self)

            # Rendering.
            self.render_debug(self.game_interface.renderer)

            # For each drone under the hivemind's control, do something.
            for drone in self.drones:

                # The controls are reset each frame.
                drone.ctrl = PlayerInput(
                )  # Basically the same as SimpleControllerState().

                # Role execution.
                if drone.role is not None:
                    drone.role.execute(self, drone)

                    self.render_role(self.game_interface.renderer, drone)

                # Send the controls to the bots.
                self.game_interface.update_player_input(
                    drone.ctrl, drone.index)

            # Rate limit sleep.
            rate_limit.acquire()

    def render_debug(hive, rndr):
        """Debug rendering for all manner of things.
        
        Arguments:
            hive {Hivemind} -- The hivemind.
            rndr {?} -- The renderer.
        """
        # Rendering Ball prediction.
        locations = [
            step.physics.location for step in hive.ball.predict.slices
        ]
        rndr.begin_rendering('ball prediction')
        rndr.draw_polyline_3d(locations, rndr.pink())
        rndr.end_rendering()

    def render_role(hive, rndr, drone):
        """Renders roles above the drones.
        
        Arguments:
            hive {Hivemind} -- The hivemind.
            rndr {?} -- The renderer.
            drone {Drone} -- The drone who's role is being rendered.
        """
        # Rendering role names above drones.
        above = drone.pos + a3l([0, 0, 100])
        rndr.begin_rendering(f'role_{hive.team}_{drone.index}')
        rndr.draw_string_3d(above, 1, 1, drone.role.name, rndr.cyan())
        rndr.end_rendering()
Esempio n. 5
0
class ExampleHivemind(BotHelperProcess):

    # Some terminology:
    # hivemind = the process which controls the drones.
    # drone = a bot under the hivemind's control.

    def __init__(self, agent_metadata_queue, quit_event, options):
        super().__init__(agent_metadata_queue, quit_event, options)

        # Sets up the logger. The string is the name of your hivemind.
        # Call this something unique so people can differentiate between hiveminds.
        self.logger = get_logger('Example Hivemind')

        # The game interface is how you get access to things
        # like ball prediction, the game tick packet, or rendering.
        self.game_interface = GameInterface(self.logger)

        # Running indices is a set of bot indices
        # which requested this hivemind with the same key.
        self.running_indices = set()

    def try_receive_agent_metadata(self):
        """Adds all drones with the correct key to our set of running indices."""
        while True:  # will exit on queue.Empty
            try:
                # Adds drone indices to running_indices.
                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):
        """Runs once, sets up the hivemind and its agents."""
        # Prints an activation message into the console.
        # This let's you know that the process is up and running.
        self.logger.info("Hello World!")

        # Loads game interface.
        self.game_interface.load_interface()

        # Wait a moment for all agents to have a chance to start up and send metadata.
        self.logger.info("Snoozing for 3 seconds; give me a moment.")
        time.sleep(3)
        self.try_receive_agent_metadata()

        # This is how you access field info.
        # First create the initialise the object...
        field_info = FieldInfoPacket()
        # Then update it.
        self.game_interface.update_field_info_packet(field_info)

        # Same goes for the packet, but that is
        # also updated in the main loop every tick.
        packet = GameTickPacket()
        self.game_interface.update_live_data_packet(packet)
        # Ball prediction works the same. Check the main loop.

        # Create a Ball object for the ball that holds its information.
        self.ball = Ball()

        # Create a Drone object for every drone that holds its information.
        self.drones = []
        for index in range(packet.num_cars):
            if index in self.running_indices:
                self.drones.append(Drone(index, packet.game_cars[index].team))

        # Other attribute initialisation.
        self.state = State.SETUP
        self.pinch_target = None

        # Runs the game loop where the hivemind will spend the rest of its time.
        self.game_loop()

    def game_loop(self):
        """The main game loop. This is where your hivemind code goes."""

        # Creating packet and ball prediction objects which will be updated every tick.
        packet = GameTickPacket()
        ball_prediction = BallPrediction()

        # Nicknames the renderer to shorten code.
        draw = self.game_interface.renderer

        # MAIN LOOP:
        while True:

            previous_packet = packet

            # Updating the game tick packet.
            self.game_interface.update_live_data_packet(packet)

            # Checking if packet is new, otherwise sleep.
            if previous_packet.game_info.seconds_elapsed == packet.game_info.seconds_elapsed:
                time.sleep(0.001)

            else:
                # Begins rendering at the start of the loop; makes life easier.
                # https://discordapp.com/channels/348658686962696195/446761380654219264/610879527089864737
                draw.begin_rendering(f'Hivemind{self.drones[0].team}')

                # PRE-PROCESSING:

                # Updates the ball prediction.
                self.game_interface.update_ball_prediction(ball_prediction)

                # Processing ball data.
                self.ball.pos = a3v(packet.game_ball.physics.location)

                # Processing drone data.
                for drone in self.drones:
                    drone.pos = a3v(packet.game_cars[drone.index].physics.location)
                    drone.rot = a3r(packet.game_cars[drone.index].physics.rotation)
                    drone.vel = a3v(packet.game_cars[drone.index].physics.velocity)
                    drone.boost = packet.game_cars[drone.index].boost
                    drone.orient_m = orient_matrix(drone.rot)

                    # Reset ctrl every tick.
                    # PlayerInput is practically identical to SimpleControllerState.
                    drone.ctrl = PlayerInput()

                # Game time.
                game_time = packet.game_info.seconds_elapsed

                # Example Team Pinches (2 bots only)
                # There's nothing stopping you from doing it with more ;) Give it a shot!
                if len(self.drones) == 2:

                    # Sorts the drones left to right. (More understandble code below)
                    #right_to_left_drones = sorted(self.drones, key=lambda drone: drone.pos[0]*team_sign(drone.team))

                    # Finds the right and left drones.
                    sign = team_sign(self.drones[0].team)
                    if self.drones[0].pos[0]*sign <= self.drones[1].pos[0]*sign:
                        right = self.drones[0]
                        left = self.drones[1]
                    else:
                        right = self.drones[1]
                        left = self.drones[0]

                    # Bots get boost and go to wait positions.
                    if self.state == State.SETUP:

                        # Some guide positions.
                        right_boost = a3l([-3072.0, -4096.0, 71.1])*sign
                        right_wait = a3l([-1792.0, -4184.0, 71.1])*sign
                        # Making use of symmetry
                        left_boost = right_boost * a3l([-1, 1, 1])
                        left_wait = right_wait * a3l([-1, 1, 1])

                        # First get boost and then go to wait position.
                        if right.boost < 100:
                            slow_to_pos(right, right_boost)
                        else:
                            slow_to_pos(right, right_wait)

                        if left.boost < 100:
                            slow_to_pos(left, left_boost)
                        else:
                            slow_to_pos(left, left_wait)

                        # If both bots are in wait position, switch to WAIT state.
                        if np.linalg.norm(right.pos-right_wait) + np.linalg.norm(left.pos-left_wait) < 200:
                            self.state = State.WAIT

                    # Bots try to face the ball, waiting for perfect moment to team pinch.
                    elif self.state == State.WAIT:

                        # Each drone should try to face the ball.
                        for drone in self.drones:
                            turn_to_pos(drone, self.ball.pos, game_time)

                        # Filters out all the predictions where the ball is too far off the ground.
                        # Result is a list of tuples of positions and time.
                        filtered_prediction = [(a3v(step.physics.location), step.game_seconds)
                                            for step in ball_prediction.slices if step.physics.location.z < 100]

                        if len(filtered_prediction) > 0:
                            # Turns the predition into a numpy array for fast vectorized calculations.
                            filtered_prediction = np.array(filtered_prediction)

                            # Gets the vectors from the drones to the ball prediction.
                            positions = np.vstack(filtered_prediction[:, 0])
                            right_to_prediction = positions - right.pos
                            left_to_prediction = positions - left.pos

                            # Calculates the distances.
                            # Cool blog post about einsum: http://ajcr.net/Basic-guide-to-einsum/
                            right_distances = np.sqrt(
                                np.einsum('ij,ij->i', right_to_prediction, right_to_prediction))
                            left_distances = np.sqrt(
                                np.einsum('ij,ij->i', left_to_prediction, left_to_prediction))

                            # Filters out the predictions which are too close or too far.
                            good_distances = (CLOSEST <= right_distances) & (FARTHEST >= right_distances) & (
                                CLOSEST <= left_distances) & (FARTHEST >= left_distances)
                            valid_targets = filtered_prediction[good_distances]

                            if len(valid_targets) > 0:
                                # Getting the remaining distances after filter.
                                right_distances = right_distances[good_distances]
                                left_distances = left_distances[good_distances]

                                # Getting time estimates to go that distance. (Assuming boosting, and going in a straight line.)
                                # https://www.geogebra.org/m/nnsat4pj
                                right_times = right_distances**0.55 / 41.53
                                right_times[right_distances > 2177.25] = 1/2300 * \
                                    right_distances[right_distances > 2177.25] + 0.70337
                                right_times += game_time + TIME_BUFFER

                                left_times = left_distances**0.55 / 41.53
                                left_times[left_distances > 2177.25] = 1/2300 * \
                                    left_distances[left_distances > 2177.25] + 0.70337
                                left_times += game_time + TIME_BUFFER

                                # Filters out the predictions which we can't get to.
                                good_times = (valid_targets[:, 1] > right_times) & (
                                    valid_targets[:, 1] > left_times)
                                valid_targets = valid_targets[good_times]

                                # To avoid flukes or anomalies, check that the ball is valid for at least 10 steps.
                                # Not exact because there could be more bounce spots but good enough to avoid flukes.
                                if len(valid_targets) > 10:
                                    # Select first valid target.
                                    self.pinch_target = valid_targets[0]
                                    # Reset drone's going attribute.
                                    right.going = False
                                    left.going = False
                                    # Set the state to PINCH.
                                    self.state = State.PINCH

                        # Rendering number of positions viable after each condition.
                        draw.draw_string_2d(
                            10, 70, 2, 2, f'Good height: {len(filtered_prediction)}', draw.white())
                        draw.draw_string_2d(
                            10, 100, 2, 2, f'Good distance: {len(valid_targets)}', draw.white())
                        # Render circles to show distances.
                        draw.draw_polyline_3d(make_circle(
                            CLOSEST, right.pos, 20), draw.cyan())
                        draw.draw_polyline_3d(make_circle(
                            CLOSEST, left.pos, 20), draw.cyan())
                        draw.draw_polyline_3d(make_circle(
                            FARTHEST, right.pos, 30), draw.pink())
                        draw.draw_polyline_3d(make_circle(
                            FARTHEST, left.pos, 30), draw.pink())

                    elif self.state == State.PINCH:

                        # Checks if the ball has been hit recently.
                        if packet.game_ball.latest_touch.time_seconds + 0.1 > game_time:
                            self.pinch_target = None
                            self.state = State.SETUP

                        elif self.pinch_target is not None:
                            if not right.going:
                                # Get the distance to the target.
                                right_distance = np.linalg.norm(
                                    self.pinch_target[0] - right.pos)
                                # Get a time estimate
                                right_time = right_distance**0.55 / \
                                    41.53 if right_distance <= 2177.25 else 1/2300 * right_distance + 0.70337

                                # Waits until time is right to go. Otherwise turns to face the target position.
                                if game_time + right_time + TIME_ERROR >= self.pinch_target[1]:
                                    right.going = True
                                else:
                                    turn_to_pos(
                                        right, self.pinch_target[0], game_time)

                            else:
                                fast_to_pos(right, self.pinch_target[0])

                            # Same for left.
                            if not left.going:
                                left_distance = np.linalg.norm(
                                    self.pinch_target[0] - left.pos)
                                left_time = left_distance**0.55 / \
                                    41.53 if left_distance <= 2177.25 else 1/2300 * left_distance + 0.70337
                                if game_time + left_time + TIME_ERROR >= self.pinch_target[1]:
                                    left.going = True
                                else:
                                    turn_to_pos(
                                        left, self.pinch_target[0], game_time)
                            else:
                                fast_to_pos(left, self.pinch_target[0])

                            # Some rendering.
                            draw.draw_string_2d(
                                10, 70, 2, 2, f'Right going: {right.going}', draw.white())
                            draw.draw_string_2d(
                                10, 100, 2, 2, f'Left going: {left.going}', draw.white())

                else:
                    draw.draw_string_2d(
                        10, 10, 2, 2, 'This example version has only been coded for 2 HiveBots.', draw.red())

                # Use this to send the drone inputs to the drones.
                for drone in self.drones:
                    self.game_interface.update_player_input(
                        drone.ctrl, drone.index)

                # Some example rendering:
                draw.draw_string_2d(10, 10, 3, 3, f'{self.state}', draw.pink())
                # Renders ball prediction
                path = [step.physics.location for step in ball_prediction.slices[::10]]
                draw.draw_polyline_3d(path, draw.pink())

                # Renders drone indices.
                for drone in self.drones:
                    draw.draw_string_3d(drone.pos, 1, 1, str(
                        drone.index), draw.white())

                # Team pinch info.
                if self.pinch_target is not None:
                    draw.draw_rect_3d(
                        self.pinch_target[0], 10, 10, True, draw.red())

                # Ending rendering.
                draw.end_rendering()
Esempio n. 6
0
class Hivemind(BotHelperProcess):
    def __init__(self, agent_metadata_queue, quit_event, options):
        super().__init__(agent_metadata_queue, quit_event, options)
        self.logger = get_logger('Hivemind')
        self.game_interface = GameInterface(self.logger)
        self.running_indices = set()

    def try_receive_agent_metadata(self):
        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):
        """Runs once, sets up the hivemind and its agents."""
        # Prints stuff into the console.
        self.logger.info("Hivemind A C T I V A T E D")
        self.logger.info("Breaking the meta")
        self.logger.info("Welcoming @r0bbi3#0269")

        # Loads game interface.
        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()

        # Runs the game loop where the hivemind will spend the rest of its time.
        self.game_loop()

    def game_loop(self):
        """The main game loop. This is where your hivemind code goes."""

        # Setting up rate limiter.
        rate_limit = rate_limiter.RateLimiter(120)

        # Setting up data.
        field_info = FieldInfoPacket()
        self.game_interface.update_field_info_packet(field_info)
        packet = GameTickPacket()
        self.game_interface.update_live_data_packet(packet)

        data.setup(self, packet, field_info, self.running_indices)

        self.ball.predict = BallPrediction()

        # MAIN LOOP:
        while True:
            # Updating the game packet from the game.
            self.game_interface.update_live_data_packet(packet)

            # Processing packet.
            data.process(self, packet)

            # Ball prediction.
            self.game_interface.update_ball_prediction(self.ball.predict)

            brain.think(self)

            for drone in self.drones:
                drone.ctrl = PlayerInput()
                if drone.role is not None:
                    drone.role.execute(self, drone)
                self.game_interface.update_player_input(
                    drone.ctrl, drone.index)

            self.draw_debug()

            # Rate limit sleep.
            rate_limit.acquire()

    def draw_debug(self):
        self.game_interface.renderer.begin_rendering()
        path = [
            a3v(step.physics.location) for step in self.ball.predict.slices
        ]
        self.game_interface.renderer.draw_polyline_3d(
            path, self.game_interface.renderer.pink())

        for drone in self.drones:
            if drone.role is not None:
                self.game_interface.renderer.draw_string_3d(
                    drone.pos, 1, 1, drone.role.name,
                    self.game_interface.renderer.white())
        self.game_interface.renderer.end_rendering()