diff --git a/.env b/.env new file mode 100644 index 0000000..56a282d --- /dev/null +++ b/.env @@ -0,0 +1 @@ +PYTHONPATH=src diff --git a/.gitignore b/.gitignore index 51410e9..122e90e 100644 --- a/.gitignore +++ b/.gitignore @@ -79,9 +79,6 @@ celerybeat-schedule # SageMath parsed files *.sage.py -# dotenv -.env - # virtualenv .venv venv/ @@ -108,4 +105,8 @@ ENV/ /build # Gradle files -/.gradle \ No newline at end of file +/.gradle + +# VSCode +.vscode/ +*.code-workspace diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..9db66d4 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,2 @@ +[TYPECHECK] +generated-members=QuickChats.* diff --git a/README.md b/README.md index b1435e5..61129d2 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,17 @@ # RLBotPythonExample -Example of a python bot using the RLBot framework +Example of a Python bot using the RLBot framework -## Installation +## Quick Start +The easiest way to start a Python bot is demonstrated here! +https://youtu.be/YJ69QZ-EX7k -### Video guide - -https://www.youtube.com/watch?v=UjsQFNN0nSA - -### Plain instructions - -1. Make sure you've installed [Python 3.6 64 bit](https://www.python.org/ftp/python/3.6.5/python-3.6.5-amd64.exe). During installation: - - Select "Add Python to PATH" - - Make sure pip is included in the installation -2. Open Rocket League -3. Download or clone this repository -3. In the files from the previous step, find and double click on run-gui.bat -4. Click the 'Run' button +It shows you how to: +- Install the RLBot GUI +- Use it to create a new bot ## Changing the bot -- Bot behavior is controlled by `python_example/python_example.py` -- Bot appearance is controlled by `python_example/appearance.cfg` +- Bot behavior is controlled by `src/bot.py` +- Bot appearance is controlled by `src/appearance.cfg` See https://github.com/RLBot/RLBotPythonExample/wiki for documentation and tutorials. diff --git a/python_example/python_example.py b/python_example/python_example.py deleted file mode 100644 index 60fd081..0000000 --- a/python_example/python_example.py +++ /dev/null @@ -1,81 +0,0 @@ -import math - -from rlbot.agents.base_agent import BaseAgent, SimpleControllerState -from rlbot.utils.structures.game_data_struct import GameTickPacket - - -class PythonExample(BaseAgent): - - def initialize_agent(self): - #This runs once before the bot starts up - self.controller_state = SimpleControllerState() - - def get_output(self, packet: GameTickPacket) -> SimpleControllerState: - ball_location = Vector2(packet.game_ball.physics.location.x, packet.game_ball.physics.location.y) - - my_car = packet.game_cars[self.index] - car_location = Vector2(my_car.physics.location.x, my_car.physics.location.y) - car_direction = get_car_facing_vector(my_car) - car_to_ball = ball_location - car_location - - steer_correction_radians = car_direction.correction_to(car_to_ball) - - if steer_correction_radians > 0: - # Positive radians in the unit circle is a turn to the left. - turn = -1.0 # Negative value for a turn to the left. - action_display = "turn left" - else: - turn = 1.0 - action_display = "turn right" - - self.controller_state.throttle = 1.0 - self.controller_state.steer = turn - - draw_debug(self.renderer, my_car, packet.game_ball, action_display) - - return self.controller_state - -class Vector2: - def __init__(self, x=0, y=0): - self.x = float(x) - self.y = float(y) - - def __add__(self, val): - return Vector2(self.x + val.x, self.y + val.y) - - def __sub__(self, val): - return Vector2(self.x - val.x, self.y - val.y) - - def correction_to(self, ideal): - # The in-game axes are left handed, so use -x - current_in_radians = math.atan2(self.y, -self.x) - ideal_in_radians = math.atan2(ideal.y, -ideal.x) - - correction = ideal_in_radians - current_in_radians - - # Make sure we go the 'short way' - if abs(correction) > math.pi: - if correction < 0: - correction += 2 * math.pi - else: - correction -= 2 * math.pi - - return correction - - -def get_car_facing_vector(car): - pitch = float(car.physics.rotation.pitch) - yaw = float(car.physics.rotation.yaw) - - facing_x = math.cos(pitch) * math.cos(yaw) - facing_y = math.cos(pitch) * math.sin(yaw) - - return Vector2(facing_x, facing_y) - -def draw_debug(renderer, car, ball, action_display): - renderer.begin_rendering() - # draw a line from the car to the ball - renderer.draw_line_3d(car.physics.location, ball.physics.location, renderer.white()) - # print the action that the bot is taking - renderer.draw_string_3d(car.physics.location, 2, 2, action_display, renderer.white()) - renderer.end_rendering() diff --git a/requirements.txt b/requirements.txt index 25c0c97..008fac3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ # Include everything the framework requires # You will automatically get updates for all versions starting with "1.". rlbot==1.* +rlbot_gui rlbottraining # This will cause pip to auto-upgrade and stop scaring people with warning messages diff --git a/rlbot.cfg b/rlbot.cfg index 3424a1a..a568454 100644 --- a/rlbot.cfg +++ b/rlbot.cfg @@ -5,10 +5,13 @@ # Visit https://github.com/RLBot/RLBot/wiki/Config-File-Documentation to see what you can put here. [Match Configuration] -# Number of bots/players which will be spawned. We support up to max 10. +# Visit https://github.com/RLBot/RLBot/wiki/Config-File-Documentation to see what you can put here. +# Number of bots/players which will be spawned. We support up to max 64. num_participants = 2 game_mode = Soccer game_map = Mannfield +enable_rendering = True +enable_state_setting = True [Mutator Configuration] # Visit https://github.com/RLBot/RLBot/wiki/Config-File-Documentation to see what you can put here. @@ -16,16 +19,16 @@ game_map = Mannfield [Participant Configuration] # Put the name of your bot config file here. Only num_participants config files will be read! # Everything needs a config, even players and default bots. We still set loadouts and names from config! -participant_config_0 = python_example/python_example.cfg -participant_config_1 = python_example/python_example.cfg -participant_config_2 = python_example/python_example.cfg -participant_config_3 = python_example/python_example.cfg -participant_config_4 = python_example/python_example.cfg -participant_config_5 = python_example/python_example.cfg -participant_config_6 = python_example/python_example.cfg -participant_config_7 = python_example/python_example.cfg -participant_config_8 = python_example/python_example.cfg -participant_config_9 = python_example/python_example.cfg +participant_config_0 = src/bot.cfg +participant_config_1 = src/bot.cfg +participant_config_2 = src/bot.cfg +participant_config_3 = src/bot.cfg +participant_config_4 = src/bot.cfg +participant_config_5 = src/bot.cfg +participant_config_6 = src/bot.cfg +participant_config_7 = src/bot.cfg +participant_config_8 = src/bot.cfg +participant_config_9 = src/bot.cfg # team 0 shoots on positive goal, team 1 shoots on negative goal participant_team_0 = 0 diff --git a/run-gui.bat b/run-gui.bat deleted file mode 100644 index a1c00d9..0000000 --- a/run-gui.bat +++ /dev/null @@ -1,11 +0,0 @@ -@echo off - -@rem Change the working directory to the location of this file so that relative paths will work -cd /D "%~dp0" - -@rem Make sure the environment variables are up-to-date. This is useful if the user installed python a moment ago. -call ./RefreshEnv.cmd - -python run.py gui - -pause diff --git a/run.bat b/run.bat deleted file mode 100644 index b3318d9..0000000 --- a/run.bat +++ /dev/null @@ -1,11 +0,0 @@ -@echo off - -@rem Change the working directory to the location of this file so that relative paths will work -cd /D "%~dp0" - -@rem Make sure the environment variables are up-to-date. This is useful if the user installed python a moment ago. -call ./RefreshEnv.cmd - -python run.py - -pause diff --git a/run.py b/run.py index a679076..23ea98a 100644 --- a/run.py +++ b/run.py @@ -1,11 +1,6 @@ +import subprocess import sys -# https://stackoverflow.com/a/51704613 -try: - from pip import main as pipmain -except ImportError: - from pip._internal import main as pipmain - DEFAULT_LOGGER = 'rlbot' if __name__ == '__main__': @@ -18,7 +13,8 @@ logger.log(logging_utils.logging_level, 'Skipping upgrade check for now since it looks like you have no internet') elif public_utils.is_safe_to_upgrade(): - pipmain(['install', '-r', 'requirements.txt', '--upgrade', '--upgrade-strategy=eager']) + subprocess.call([sys.executable, "-m", "pip", "install", '-r', 'requirements.txt']) + subprocess.call([sys.executable, "-m", "pip", "install", 'rlbot', '--upgrade']) # https://stackoverflow.com/a/44401013 rlbots = [module for module in sys.modules if module.startswith('rlbot')] @@ -26,17 +22,11 @@ sys.modules.pop(rlbot_module) except ImportError: - pipmain(['install', '-r', 'requirements.txt', '--upgrade', '--upgrade-strategy=eager']) + subprocess.call([sys.executable, "-m", "pip", "install", '-r', 'requirements.txt', '--upgrade', '--upgrade-strategy=eager']) try: - if len(sys.argv) > 1 and sys.argv[1] == 'gui': - from rlbot.gui.qt_root import RLBotQTGui - - RLBotQTGui.main() - else: - from rlbot import runner - - runner.main() + from rlbot import runner + runner.main() except Exception as e: print("Encountered exception: ", e) print("Press enter to close.") diff --git a/run_gui.py b/run_gui.py new file mode 100644 index 0000000..a3bced8 --- /dev/null +++ b/run_gui.py @@ -0,0 +1,7 @@ +from rlbot_gui import gui + +# This is a useful way to start up RLBotGUI directly from your bot project. You can use it to +# arrange a match with the settings you like, and if you have a good IDE like PyCharm, +# you can do breakpoint debugging on your bot. +if __name__ == '__main__': + gui.start() diff --git a/python_example/__init__.py b/src/__init__.py similarity index 100% rename from python_example/__init__.py rename to src/__init__.py diff --git a/python_example/appearance.cfg b/src/appearance.cfg similarity index 78% rename from python_example/appearance.cfg rename to src/appearance.cfg index 468bbe3..45840e3 100644 --- a/python_example/appearance.cfg +++ b/src/appearance.cfg @@ -1,3 +1,7 @@ +# You don't have to manually edit this file! +# RLBotGUI has an appearance editor with a nice colorpicker, database of items and more! +# To open it up, simply click the (i) icon next to your bot's name and then click Edit Appearance + [Bot Loadout] team_color_id = 60 custom_color_id = 0 diff --git a/python_example/python_example.cfg b/src/bot.cfg similarity index 81% rename from python_example/python_example.cfg rename to src/bot.cfg index 99dd157..3aa2023 100644 --- a/python_example/python_example.cfg +++ b/src/bot.cfg @@ -3,11 +3,14 @@ looks_config = ./appearance.cfg # Path to python file. Can use relative path from here. -python_file = ./python_example.py +python_file = ./bot.py # Name of the bot in-game name = PythonExampleBot +# The maximum number of ticks per second that your bot wishes to receive. +maximum_tick_rate_preference = 120 + [Details] # These values are optional but useful metadata for helper programs # Name of the bot's creator/developer @@ -18,7 +21,7 @@ description = This is a multi-line description of the official python example bot # Fun fact about the bot -fun_fact = +fun_fact = # Link to github repository github = https://github.com/RLBot/RLBotPythonExample diff --git a/src/bot.py b/src/bot.py new file mode 100644 index 0000000..8d21345 --- /dev/null +++ b/src/bot.py @@ -0,0 +1,89 @@ +from rlbot.agents.base_agent import BaseAgent, SimpleControllerState +from rlbot.messages.flat.QuickChatSelection import QuickChatSelection +from rlbot.utils.structures.game_data_struct import GameTickPacket + +from util.ball_prediction_analysis import find_slice_at_time +from util.boost_pad_tracker import BoostPadTracker +from util.drive import steer_toward_target +from util.sequence import Sequence, ControlStep +from util.vec import Vec3 + + +class MyBot(BaseAgent): + + def __init__(self, name, team, index): + super().__init__(name, team, index) + self.active_sequence: Sequence = None + self.boost_pad_tracker = BoostPadTracker() + + def initialize_agent(self): + # Set up information about the boost pads now that the game is active and the info is available + self.boost_pad_tracker.initialize_boosts(self.get_field_info()) + + def get_output(self, packet: GameTickPacket) -> SimpleControllerState: + """ + This function will be called by the framework many times per second. This is where you can + see the motion of the ball, etc. and return controls to drive your car. + """ + + # Keep our boost pad info updated with which pads are currently active + self.boost_pad_tracker.update_boost_status(packet) + + # This is good to keep at the beginning of get_output. It will allow you to continue + # any sequences that you may have started during a previous call to get_output. + if self.active_sequence is not None and not self.active_sequence.done: + controls = self.active_sequence.tick(packet) + if controls is not None: + return controls + + # Gather some information about our car and the ball + my_car = packet.game_cars[self.index] + car_location = Vec3(my_car.physics.location) + car_velocity = Vec3(my_car.physics.velocity) + ball_location = Vec3(packet.game_ball.physics.location) + + # By default we will chase the ball, but target_location can be changed later + target_location = ball_location + + if car_location.dist(ball_location) > 1500: + # We're far away from the ball, let's try to lead it a little bit + ball_prediction = self.get_ball_prediction_struct() # This can predict bounces, etc + ball_in_future = find_slice_at_time(ball_prediction, packet.game_info.seconds_elapsed + 2) + + # ball_in_future might be None if we don't have an adequate ball prediction right now, like during + # replays, so check it to avoid errors. + if ball_in_future is not None: + target_location = Vec3(ball_in_future.physics.location) + self.renderer.draw_line_3d(ball_location, target_location, self.renderer.cyan()) + + # Draw some things to help understand what the bot is thinking + self.renderer.draw_line_3d(car_location, target_location, self.renderer.white()) + self.renderer.draw_string_3d(car_location, 1, 1, f'Speed: {car_velocity.length():.1f}', self.renderer.white()) + self.renderer.draw_rect_3d(target_location, 8, 8, True, self.renderer.cyan(), centered=True) + + if 750 < car_velocity.length() < 800: + # We'll do a front flip if the car is moving at a certain speed. + return self.begin_front_flip(packet) + + controls = SimpleControllerState() + controls.steer = steer_toward_target(my_car, target_location) + controls.throttle = 1.0 + # You can set more controls if you want, like controls.boost. + + return controls + + def begin_front_flip(self, packet): + # Send some quickchat just for fun + self.send_quick_chat(team_only=False, quick_chat=QuickChatSelection.Information_IGotIt) + + # Do a front flip. We will be committed to this for a few seconds and the bot will ignore other + # logic during that time because we are setting the active_sequence. + self.active_sequence = Sequence([ + ControlStep(duration=0.05, controls=SimpleControllerState(jump=True)), + ControlStep(duration=0.05, controls=SimpleControllerState(jump=False)), + ControlStep(duration=0.2, controls=SimpleControllerState(jump=True, pitch=-1)), + ControlStep(duration=0.8, controls=SimpleControllerState()), + ]) + + # Return the controls associated with the beginning of the sequence so we can start right away. + return self.active_sequence.tick(packet) diff --git a/src/util/__init__.py b/src/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/util/ball_prediction_analysis.py b/src/util/ball_prediction_analysis.py new file mode 100644 index 0000000..31f6e81 --- /dev/null +++ b/src/util/ball_prediction_analysis.py @@ -0,0 +1,48 @@ +from typing import Callable + +from rlbot.utils.structures.ball_prediction_struct import BallPrediction, Slice + +# field length(5120) + ball radius(93) = 5213 however that results in false positives +GOAL_THRESHOLD = 5235 + +# We will jump this number of frames when looking for a moment where the ball is inside the goal. +# Big number for efficiency, but not so big that the ball could go in and then back out during that +# time span. Unit is the number of frames in the ball prediction, and the prediction is at 60 frames per second. +GOAL_SEARCH_INCREMENT = 20 + + +def find_slice_at_time(ball_prediction: BallPrediction, game_time: float): + """ + This will find the future position of the ball at the specified time. The returned + Slice object will also include the ball's velocity, etc. + """ + start_time = ball_prediction.slices[0].game_seconds + approx_index = int((game_time - start_time) * 60) # We know that there are 60 slices per second. + if 0 <= approx_index < ball_prediction.num_slices: + return ball_prediction.slices[approx_index] + return None + + +def predict_future_goal(ball_prediction: BallPrediction): + """ + Analyzes the ball prediction to see if the ball will enter one of the goals. Only works on standard arenas. + Will return the first ball slice which appears to be inside the goal, or None if it does not enter a goal. + """ + return find_matching_slice(ball_prediction, 0, lambda s: abs(s.physics.location.y) >= GOAL_THRESHOLD, + search_increment=20) + + +def find_matching_slice(ball_prediction: BallPrediction, start_index: int, predicate: Callable[[Slice], bool], + search_increment=1): + """ + Tries to find the first slice in the ball prediction which satisfies the given predicate. For example, + you could find the first slice below a certain height. Will skip ahead through the packet by search_increment + for better efficiency, then backtrack to find the exact first slice. + """ + for coarse_index in range(start_index, ball_prediction.num_slices, search_increment): + if predicate(ball_prediction.slices[coarse_index]): + for j in range(max(start_index, coarse_index - search_increment), coarse_index): + ball_slice = ball_prediction.slices[j] + if predicate(ball_slice): + return ball_slice + return None diff --git a/src/util/boost_pad_tracker.py b/src/util/boost_pad_tracker.py new file mode 100644 index 0000000..3b6a7ef --- /dev/null +++ b/src/util/boost_pad_tracker.py @@ -0,0 +1,43 @@ +from dataclasses import dataclass +from typing import List + +from rlbot.utils.structures.game_data_struct import GameTickPacket, FieldInfoPacket + +from util.vec import Vec3 + + +@dataclass +class BoostPad: + location: Vec3 + is_full_boost: bool + is_active: bool # Active means it's available to be picked up + timer: float # Counts the number of seconds that the pad has been *inactive* + + +class BoostPadTracker: + """ + This class merges together the boost pad location info with the is_active info so you can access it + in one convenient list. For it to function correctly, you need to call initialize_boosts once when the + game has started, and then update_boost_status every frame so that it knows which pads are active. + """ + + def __init__(self): + self.boost_pads: List[BoostPad] = [] + self._full_boosts_only: List[BoostPad] = [] + + def initialize_boosts(self, game_info: FieldInfoPacket): + raw_boosts = [game_info.boost_pads[i] for i in range(game_info.num_boosts)] + self.boost_pads: List[BoostPad] = [BoostPad(Vec3(rb.location), rb.is_full_boost, False, 0) for rb in raw_boosts] + # Cache the list of full boosts since they're commonly requested. + # They reference the same objects in the boost_pads list. + self._full_boosts_only: List[BoostPad] = [bp for bp in self.boost_pads if bp.is_full_boost] + + def update_boost_status(self, packet: GameTickPacket): + for i in range(packet.num_boost): + our_pad = self.boost_pads[i] + packet_pad = packet.game_boosts[i] + our_pad.is_active = packet_pad.is_active + our_pad.timer = packet_pad.timer + + def get_full_boosts(self) -> List[BoostPad]: + return self._full_boosts_only diff --git a/src/util/drive.py b/src/util/drive.py new file mode 100644 index 0000000..b89160b --- /dev/null +++ b/src/util/drive.py @@ -0,0 +1,25 @@ +import math + +from rlbot.utils.structures.game_data_struct import PlayerInfo + +from util.orientation import Orientation, relative_location +from util.vec import Vec3 + + +def limit_to_safe_range(value: float) -> float: + """ + Controls like throttle, steer, pitch, yaw, and roll need to be in the range of -1 to 1. + This will ensure your number is in that range. Something like 0.45 will stay as it is, + but a value of -5.6 would be changed to -1. + """ + if value < -1: + return -1 + if value > 1: + return 1 + return value + + +def steer_toward_target(car: PlayerInfo, target: Vec3) -> float: + relative = relative_location(Vec3(car.physics.location), Orientation(car.physics.rotation), target) + angle = math.atan2(relative.y, relative.x) + return limit_to_safe_range(angle * 5) diff --git a/src/util/orientation.py b/src/util/orientation.py new file mode 100644 index 0000000..2cf2e9a --- /dev/null +++ b/src/util/orientation.py @@ -0,0 +1,47 @@ +import math + +from util.vec import Vec3 + + +# This is a helper class for calculating directions relative to your car. You can extend it or delete if you want. +class Orientation: + """ + This class describes the orientation of an object from the rotation of the object. + Use this to find the direction of cars: forward, right, up. + It can also be used to find relative locations. + """ + + def __init__(self, rotation): + self.yaw = float(rotation.yaw) + self.roll = float(rotation.roll) + self.pitch = float(rotation.pitch) + + cr = math.cos(self.roll) + sr = math.sin(self.roll) + cp = math.cos(self.pitch) + sp = math.sin(self.pitch) + cy = math.cos(self.yaw) + sy = math.sin(self.yaw) + + self.forward = Vec3(cp * cy, cp * sy, sp) + self.right = Vec3(cy*sp*sr-cr*sy, sy*sp*sr+cr*cy, -cp*sr) + self.up = Vec3(-cr*cy*sp-sr*sy, -cr*sy*sp+sr*cy, cp*cr) + + +# Sometimes things are easier, when everything is seen from your point of view. +# This function lets you make any location the center of the world. +# For example, set center to your car's location and ori to your car's orientation, then the target will be +# relative to your car! +def relative_location(center: Vec3, ori: Orientation, target: Vec3) -> Vec3: + """ + Returns target as a relative location from center's point of view, using the given orientation. The components of + the returned vector describes: + + * x: how far in front + * y: how far right + * z: how far above + """ + x = (target - center).dot(ori.forward) + y = (target - center).dot(ori.right) + z = (target - center).dot(ori.up) + return Vec3(x, y, z) diff --git a/src/util/sequence.py b/src/util/sequence.py new file mode 100644 index 0000000..af53895 --- /dev/null +++ b/src/util/sequence.py @@ -0,0 +1,63 @@ +from dataclasses import dataclass +from typing import List + +from rlbot.agents.base_agent import SimpleControllerState +from rlbot.utils.structures.game_data_struct import GameTickPacket + + +@dataclass +class StepResult: + controls: SimpleControllerState + done: bool + + +class Step: + def tick(self, packet: GameTickPacket) -> StepResult: + """ + Return appropriate controls for this step in the sequence. If the step is over, you should + set done to True in the result, and we'll move on to the next step during the next frame. + If you panic and can't return controls at all, you may return None and we will move on to + the next step immediately. + """ + raise NotImplementedError + + +class ControlStep(Step): + """ + This allows you to repeat the same controls every frame for some specified duration. It's useful for + scheduling the button presses needed for kickoffs / dodges / etc. + """ + def __init__(self, duration: float, controls: SimpleControllerState): + self.duration = duration + self.controls = controls + self.start_time: float = None + + def tick(self, packet: GameTickPacket) -> StepResult: + if self.start_time is None: + self.start_time = packet.game_info.seconds_elapsed + elapsed_time = packet.game_info.seconds_elapsed - self.start_time + return StepResult(controls=self.controls, done=elapsed_time > self.duration) + + +class Sequence: + def __init__(self, steps: List[Step]): + self.steps = steps + self.index = 0 + self.done = False + + def tick(self, packet: GameTickPacket): + while self.index < len(self.steps): + step = self.steps[self.index] + result = step.tick(packet) + if result is None or result.controls is None or result.done: + self.index += 1 + if self.index >= len(self.steps): + # The bot will know not to use this sequence next frame, even though we may be giving it controls. + self.done = True + if result is not None and result.controls is not None: + # If the step was able to give us controls, return them to the bot. + return result.controls + # Otherwise we will loop to the next step in the sequence. + # If we reach here, we ran out of steps to attempt. + self.done = True + return None diff --git a/src/util/spikes.py b/src/util/spikes.py new file mode 100644 index 0000000..582c30a --- /dev/null +++ b/src/util/spikes.py @@ -0,0 +1,36 @@ +from rlbot.utils.structures.game_data_struct import PlayerInfo, GameTickPacket + +from util.vec import Vec3 + +# When the ball is attached to a car's spikes, the distance will vary a bit depending on whether the ball is +# on the front bumper, the roof, etc. It tends to be most far away when the ball is on one of the front corners +# and that distance is a little under 200. We want to be sure that it's never over 200, otherwise bots will +# suffer from bad bugs when they don't think the ball is spiked to them but it actually is; they'll probably +# drive in circles. The opposite problem, where they think it's spiked before it really is, is not so bad because +# they usually spike it for real a split second later. +MAX_DISTANCE_WHEN_SPIKED = 200 + +class SpikeWatcher: + def __init__(self): + self.carrying_car: PlayerInfo = None + self.spike_moment = 0 + self.carry_duration = 0 + + def read_packet(self, packet: GameTickPacket): + ball_location = Vec3(packet.game_ball.physics.location) + closest_candidate: PlayerInfo = None + closest_distance = 999999 + for i in range(packet.num_cars): + car = packet.game_cars[i] + car_location = Vec3(car.physics.location) + distance = car_location.dist(ball_location) + if distance < MAX_DISTANCE_WHEN_SPIKED: + if distance < closest_distance: + closest_candidate = car + closest_distance = distance + if closest_candidate != self.carrying_car and closest_candidate is not None: + self.spike_moment = packet.game_info.seconds_elapsed + + self.carrying_car = closest_candidate + if self.carrying_car is not None: + self.carry_duration = packet.game_info.seconds_elapsed - self.spike_moment diff --git a/src/util/vec.py b/src/util/vec.py new file mode 100644 index 0000000..9ce74ac --- /dev/null +++ b/src/util/vec.py @@ -0,0 +1,109 @@ +import math +from typing import Union + +from rlbot.utils.structures.game_data_struct import Vector3 + + +class Vec3: + """ + This class should provide you with all the basic vector operations that you need, but feel free to extend its + functionality when needed. + The vectors found in the GameTickPacket will be flatbuffer vectors. Cast them to Vec3 like this: + `car_location = Vec3(car.physics.location)`. + + Remember that the in-game axis are left-handed. + + When in doubt visit the wiki: https://github.com/RLBot/RLBot/wiki/Useful-Game-Values + """ + # https://docs.python.org/3/reference/datamodel.html#slots + __slots__ = [ + 'x', + 'y', + 'z' + ] + + def __init__(self, x: Union[float, 'Vec3', 'Vector3']=0, y: float=0, z: float=0): + """ + Create a new Vec3. The x component can alternatively be another vector with an x, y, and z component, in which + case the created vector is a copy of the given vector and the y and z parameter is ignored. Examples: + + a = Vec3(1, 2, 3) + + b = Vec3(a) + + """ + + if hasattr(x, 'x'): + # We have been given a vector. Copy it + self.x = float(x.x) + self.y = float(x.y) if hasattr(x, 'y') else 0 + self.z = float(x.z) if hasattr(x, 'z') else 0 + else: + self.x = float(x) + self.y = float(y) + self.z = float(z) + + def __getitem__(self, item: int): + return (self.x, self.y, self.z)[item] + + def __add__(self, other: 'Vec3') -> 'Vec3': + return Vec3(self.x + other.x, self.y + other.y, self.z + other.z) + + def __sub__(self, other: 'Vec3') -> 'Vec3': + return Vec3(self.x - other.x, self.y - other.y, self.z - other.z) + + def __neg__(self): + return Vec3(-self.x, -self.y, -self.z) + + def __mul__(self, scale: float) -> 'Vec3': + return Vec3(self.x * scale, self.y * scale, self.z * scale) + + def __rmul__(self, scale): + return self * scale + + def __truediv__(self, scale: float) -> 'Vec3': + scale = 1 / float(scale) + return self * scale + + def __str__(self): + return f"Vec3({self.x:.2f}, {self.y:.2f}, {self.z:.2f})" + + def __repr__(self): + return self.__str__() + + def flat(self): + """Returns a new Vec3 that equals this Vec3 but projected onto the ground plane. I.e. where z=0.""" + return Vec3(self.x, self.y, 0) + + def length(self): + """Returns the length of the vector. Also called magnitude and norm.""" + return math.sqrt(self.x**2 + self.y**2 + self.z**2) + + def dist(self, other: 'Vec3') -> float: + """Returns the distance between this vector and another vector using pythagoras.""" + return (self - other).length() + + def normalized(self): + """Returns a vector with the same direction but a length of one.""" + return self / self.length() + + def rescale(self, new_len: float) -> 'Vec3': + """Returns a vector with the same direction but a different length.""" + return new_len * self.normalized() + + def dot(self, other: 'Vec3') -> float: + """Returns the dot product.""" + return self.x*other.x + self.y*other.y + self.z*other.z + + def cross(self, other: 'Vec3') -> 'Vec3': + """Returns the cross product.""" + return Vec3( + self.y * other.z - self.z * other.y, + self.z * other.x - self.x * other.z, + self.x * other.y - self.y * other.x + ) + + def ang_to(self, ideal: 'Vec3') -> float: + """Returns the angle to the ideal vector. Angle will be between 0 and pi.""" + cos_ang = self.dot(ideal) / (self.length() * ideal.length()) + return math.acos(cos_ang) diff --git a/training/hello_world_training.py b/training/hello_world_training.py index f25bb2e..f1cba03 100644 --- a/training/hello_world_training.py +++ b/training/hello_world_training.py @@ -16,16 +16,27 @@ def make_match_config_with_my_bot() -> MatchConfig: # Makes a config which only has our bot in it for now. - # For more defails: https://youtu.be/uGFmOZCpel8?t=375 + # For more details: https://youtu.be/uGFmOZCpel8?t=375 match_config = make_empty_match_config() match_config.player_configs = [ PlayerConfig.bot_config( - Path(__file__).absolute().parent.parent / 'python_example' / 'python_example.cfg', + Path(__file__).absolute().parent.parent / 'src' / 'bot.cfg', Team.BLUE ), ] return match_config + +def add_my_bot_to_playlist(exercises: Playlist) -> Playlist: + """ + Updates the match config for each excercise to include + the bot from this project + """ + for exercise in exercises: + exercise.match_config = make_match_config_with_my_bot() + return exercises + + @dataclass class StrikerPatience(StrikerExercise): """ @@ -90,7 +101,4 @@ def make_default_playlist() -> Playlist: DrivesToBallExercise('Get close to ball'), DrivesToBallExercise('Get close-ish to ball', grader=DriveToBallGrader(min_dist_to_pass=1000)) ] - for exercise in exercises: - exercise.match_config = make_match_config_with_my_bot() - - return exercises + return add_my_bot_to_playlist(exercises) diff --git a/training/unit_tests.py b/training/unit_tests.py index a147c35..9326280 100644 --- a/training/unit_tests.py +++ b/training/unit_tests.py @@ -3,7 +3,7 @@ from rlbot.training.training import Pass, Fail from rlbottraining.exercise_runner import run_playlist -from hello_world_training import StrikerPatience +from hello_world_training import StrikerPatience, add_my_bot_to_playlist class PatienceTest(unittest.TestCase): """ @@ -18,7 +18,8 @@ class PatienceTest(unittest.TestCase): """ def test_patience_required(self): - result_iter = run_playlist([StrikerPatience(name='patience required')]) + playlist = [StrikerPatience(name='patience required')] + result_iter = run_playlist(add_my_bot_to_playlist(playlist)) results = list(result_iter) self.assertEqual(len(results), 1) result = results[0] @@ -26,7 +27,8 @@ def test_patience_required(self): self.assertIsInstance(result.grade, Fail) # If you make the bot is smarter, update this assert that we pass. def test_no_patience_required(self): - result_iter = run_playlist([StrikerPatience(name='no patience required', car_start_x=-1000)]) + playlist = [StrikerPatience(name='no patience required', car_start_x=-1000)] + result_iter = run_playlist(add_my_bot_to_playlist(playlist)) results = list(result_iter) self.assertEqual(len(results), 1) result = results[0]