diff --git a/.gitignore b/.gitignore index 51410e9..a0b7f53 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,11 @@ +# RLBot specific +RefreshEnv.cmd +requirements.txt +rlbot.cfg +run.bat +run-gui.bat +run.py + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/README.md b/README.md index 8134594..a15a9ae 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,3 @@ https://www.youtube.com/watch?v=UjsQFNN0nSA 1. Download or clone this repository 1. In the files from the previous step, find and double click on run-gui.bat 1. Click the 'Run' button - -## Changing the bot - -- Bot behavior is controlled by `python_example/python_example.py` -- Bot appearance is controlled by `python_example/appearance.cfg` - -See https://github.com/RLBot/RLBotPythonExample/wiki for documentation and tutorials. diff --git a/RefreshEnv.cmd b/RefreshEnv.cmd deleted file mode 100644 index 567fd78..0000000 --- a/RefreshEnv.cmd +++ /dev/null @@ -1,66 +0,0 @@ -@echo off -:: This file is taken from chocolatey: -:: https://github.com/chocolatey/choco/blob/master/src/chocolatey.resources/redirects/RefreshEnv.cmd -:: -:: RefreshEnv.cmd -:: -:: Batch file to read environment variables from registry and -:: set session variables to these values. -:: -:: With this batch file, there should be no need to reload command -:: environment every time you want environment changes to propagate - -::echo "RefreshEnv.cmd only works from cmd.exe, please install the Chocolatey Profile to take advantage of refreshenv from PowerShell" -echo | set /p dummy="Refreshing environment variables from registry for cmd.exe. Please wait..." - -goto main - -:: Set one environment variable from registry key -:SetFromReg - "%WinDir%\System32\Reg" QUERY "%~1" /v "%~2" > "%TEMP%\_envset.tmp" 2>NUL - for /f "usebackq skip=2 tokens=2,*" %%A IN ("%TEMP%\_envset.tmp") do ( - echo/set "%~3=%%B" - ) - goto :EOF - -:: Get a list of environment variables from registry -:GetRegEnv - "%WinDir%\System32\Reg" QUERY "%~1" > "%TEMP%\_envget.tmp" - for /f "usebackq skip=2" %%A IN ("%TEMP%\_envget.tmp") do ( - if /I not "%%~A"=="Path" ( - call :SetFromReg "%~1" "%%~A" "%%~A" - ) - ) - goto :EOF - -:main - echo/@echo off >"%TEMP%\_env.cmd" - - :: Slowly generating final file - call :GetRegEnv "HKLM\System\CurrentControlSet\Control\Session Manager\Environment" >> "%TEMP%\_env.cmd" - call :GetRegEnv "HKCU\Environment">>"%TEMP%\_env.cmd" >> "%TEMP%\_env.cmd" - - :: Special handling for PATH - mix both User and System - call :SetFromReg "HKLM\System\CurrentControlSet\Control\Session Manager\Environment" Path Path_HKLM >> "%TEMP%\_env.cmd" - call :SetFromReg "HKCU\Environment" Path Path_HKCU >> "%TEMP%\_env.cmd" - - :: Caution: do not insert space-chars before >> redirection sign - echo/set "Path=%%Path_HKLM%%;%%Path_HKCU%%" >> "%TEMP%\_env.cmd" - - :: Cleanup - del /f /q "%TEMP%\_envset.tmp" 2>nul - del /f /q "%TEMP%\_envget.tmp" 2>nul - - :: capture user / architecture - SET "OriginalUserName=%USERNAME%" - SET "OriginalArchitecture=%PROCESSOR_ARCHITECTURE%" - - :: Set these variables - call "%TEMP%\_env.cmd" - - :: reset user / architecture - SET "USERNAME=%OriginalUserName%" - SET "PROCESSOR_ARCHITECTURE=%OriginalArchitecture%" - - echo | set /p dummy="Finished." - echo . \ No newline at end of file diff --git a/bots/.gitignore b/bots/.gitignore new file mode 100644 index 0000000..155e8c2 --- /dev/null +++ b/bots/.gitignore @@ -0,0 +1,2 @@ +models/ +bottus/ diff --git a/python_example/__init__.py b/bots/__init__.py similarity index 100% rename from python_example/__init__.py rename to bots/__init__.py diff --git a/bots/appearance.cfg b/bots/appearance.cfg new file mode 100644 index 0000000..0a33320 --- /dev/null +++ b/bots/appearance.cfg @@ -0,0 +1,56 @@ +[Bot Loadout] +# Primary Color selection +team_color_id = 35 +# Secondary Color selection +custom_color_id = 0 +# Car type (Octane, Merc, etc +car_id = 23 +# Type of decal +decal_id = 0 +# Wheel selection +wheels_id = 376 +# Boost selection +boost_id = 63 +# Antenna Selection +antenna_id = 0 +# Hat Selection +hat_id = 0 +# Paint Type (for first color) +paint_finish_id = 270 +# Paint Type (for secondary color) +custom_finish_id = 270 +# Engine Audio Selection +engine_audio_id = 0 +# Car trail Selection +trails_id = 1948 +# Goal Explosion Selection +goal_explosion_id = 1903 + +[Bot Loadout Orange] +# Primary Color selection +team_color_id = 33 +# Secondary Color selection +custom_color_id = 0 +# Car type (Octane, Merc, etc +car_id = 23 +# Type of decal +decal_id = 0 +# Wheel selection +wheels_id = 376 +# Boost selection +boost_id = 63 +# Antenna Selection +antenna_id = 0 +# Hat Selection +hat_id = 0 +# Paint Type (for first color) +paint_finish_id = 270 +# Paint Type (for secondary color) +custom_finish_id = 270 +# Engine Audio Selection +engine_audio_id = 0 +# Car trail Selection +trails_id = 1948 +# Goal Explosion Selection +goal_explosion_id = 1903 + diff --git a/bots/learner/__init__.py b/bots/learner/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python_example/python_example.cfg b/bots/learner/learner.cfg similarity index 61% rename from python_example/python_example.cfg rename to bots/learner/learner.cfg index 99dd157..3932431 100644 --- a/python_example/python_example.cfg +++ b/bots/learner/learner.cfg @@ -1,27 +1,26 @@ [Locations] # Path to loadout config. Can use relative path from here. -looks_config = ./appearance.cfg +looks_config = ../appearance.cfg # Path to python file. Can use relative path from here. -python_file = ./python_example.py +python_file = ./learner.py # Name of the bot in-game -name = PythonExampleBot +name = Learner [Details] # These values are optional but useful metadata for helper programs # Name of the bot's creator/developer -developer = The RLBot community +developer = r0bbi3 # Short description of the bot -description = This is a multi-line description - of the official python example bot +description = # Fun fact about the bot fun_fact = # Link to github repository -github = https://github.com/RLBot/RLBotPythonExample +github = # Programming language language = python diff --git a/bots/learner/learner.py b/bots/learner/learner.py new file mode 100644 index 0000000..d207b98 --- /dev/null +++ b/bots/learner/learner.py @@ -0,0 +1,170 @@ +import os +import sys +from random import shuffle + +import numpy as np +from rlbot.agents.base_agent import BaseAgent, SimpleControllerState +from rlbot.utils.structures.game_data_struct import GameTickPacket +from rlbot.utils.game_state_util import GameState + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from utility.collect_data import format_data, format_labels, data_size, label_size, from_labels +from utility.dummy_renderer import DummyRenderer + + +game_speed = 1.6 +dummy_render = False + + +class Learner(BaseAgent): + + def initialize_agent(self): + self.controller_state = SimpleControllerState() + self.last_save = 0 + self.state_set = False + + # Variables + self.epochs = 50 + self.steps_used = None + self.training_steps = None + self.update_training_params() + self.save_time = 600 + + # Data and labels + self.gathered_data = [] + self.gathered_labels = [] + + # Teacher + try: + sys.path.append(r'C:/Users/wood3/Documents/RLBot/Bots/Dweller') + from dweller import Dweller as Teacher + except Exception as e: + print(e) + from teacher import Teacher + self.teacher_name = Teacher.__name__ + self.teacher = Teacher(self, self.team, self.index) + self.reset_teacher_functions(first_time = True) + self.teacher.initialize_agent() + + # Tensorflow + import tensorflow as tf + from tensorflow.keras import layers + #self.tf = tf + + # Network + regularisation_rate = 0.0000001 + inputs = layers.Input(shape = (data_size,)) + x = layers.Dense(data_size, activation = 'linear', kernel_regularizer = tf.keras.regularizers.l2(l = regularisation_rate))(inputs) + x = layers.Dense(data_size, activation = 'linear', kernel_regularizer = tf.keras.regularizers.l2(l = regularisation_rate))(x) + output_one = layers.Dense(label_size[0], activation = 'tanh', kernel_regularizer = tf.keras.regularizers.l2(l = regularisation_rate))(x) + output_two = layers.Dense(label_size[1], activation = 'tanh', kernel_regularizer = tf.keras.regularizers.l2(l = regularisation_rate))(x) + output_three = layers.Dense(label_size[2], activation = 'tanh', kernel_regularizer = tf.keras.regularizers.l2(l = regularisation_rate))(x) + self.model = tf.keras.Model(inputs = inputs, outputs = [output_one, output_two, output_three]) + jump_weight = 8 + self.model.compile(optimizer = tf.compat.v1.train.AdamOptimizer(0.0005), loss = ['mse', 'mse', 'mse'], loss_weights=[1, 1, jump_weight]) + + def get_output(self, packet: GameTickPacket) -> SimpleControllerState: + car = packet.game_cars[self.index] + time = packet.game_info.seconds_elapsed + + # State-set the game speed + if packet.game_info.is_round_active: + if not self.state_set: + game_state = GameState(console_commands=['Set WorldInfo TimeDilation ' + str(game_speed)]) + self.set_game_state(game_state) + self.state_set = True + else: + self.state_set = False + + if not packet.game_info.is_round_active or car.is_demolished: + return self.controller_state + + data = format_data(self.index, packet, self.get_ball_prediction_struct()) + labels = None + + # Get the labels + self.reset_teacher_functions() + teacher_output = self.teacher.get_output(packet) + labels = format_labels(teacher_output, car) + self.gathered_data.append(data) + self.gathered_labels.append(labels) + + # Get our own predicted output + output = self.model.predict(data.reshape((1, data_size))) + + # Train + self.renderer.begin_rendering('Status') + if len(self.gathered_data) >= self.training_steps: + self.renderer.draw_string_2d(10, 10 + 100 * self.index, 2, 2, 'Training', self.renderer.team_color(car.team, True)) + self.renderer.end_rendering() + + # Randomise data and labels + c = list(zip(self.gathered_data, self.gathered_labels)) + shuffle(c) + data, labels = zip(*c) + + # Begin training + steps = int(self.training_steps * self.steps_used) + labels_to_use = [[x[0] for x in labels[:steps]], [x[1] for x in labels[:steps]], [x[2] for x in labels[:steps]]] + self.train([data[:steps]], labels_to_use) + + self.gathered_data.clear() + self.gathered_labels.clear() + + self.update_training_params(time) + else: + self.renderer.draw_string_2d(10, 10 * (self.index + 1), 2, 2, 'Playing ('\ + + str(int(len(self.gathered_data) / self.training_steps * 100))\ + + '%)', self.renderer.team_color(car.team)) + self.renderer.end_rendering() + + # Save model + if time - self.last_save > self.save_time: + self.last_save = time + self.save() + + self.controller_state = from_labels(output) + return self.controller_state + + def train(self, data, labels): + for i in range(2): + game_state = GameState(console_commands=['Pause']) + self.set_game_state(game_state) + if i == 0: self.model.fit(data, labels, epochs = self.epochs) + + def save(self): + try: + path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\ + , 'models/' + str(int(self.last_save)) + "_" + self.teacher_name + '_model.h5') + print('[' + self.name + '] Saving to: ' + str(path).replace('\\', '/')) + self.model.save(str(path)) + except Exception as e: + print(e) + + def reset_teacher_functions(self, first_time: bool = False): + if dummy_render: + self.teacher.renderer = DummyRenderer(self.renderer) + else: + self.teacher.renderer = self.renderer + + if first_time: + self.teacher.get_field_info = self.get_field_info + self.teacher.get_ball_prediction_struct = self.get_ball_prediction_struct + self.teacher.send_quick_chat = self.send_quick_chat + + def update_training_params(self, time: float = 0): + self.training_steps = min(1000, max(200, time // 4)) + #self.steps_used = max(0.3, 1 / max(1, time / 1000)) + #self.training_steps = 500 + self.steps_used = 1 + + +def max_loss(predicted_y, desired_y): + import tensorflow as tf + return tf.reduce_max(tf.abs(predicted_y - desired_y), reduction_indices = [-1]) + + +def cubic_activation(inputs): + import tensorflow as tf + linearness = 1 / 2.5 + return tf.add(inputs * linearness, tf.pow(inputs, 3)) / (1 + linearness) diff --git a/bots/models/__init__.py b/bots/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bots/runner/__init__.py b/bots/runner/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bots/runner/runner.cfg b/bots/runner/runner.cfg new file mode 100644 index 0000000..065b595 --- /dev/null +++ b/bots/runner/runner.cfg @@ -0,0 +1,26 @@ +[Locations] +# Path to loadout config. Can use relative path from here. +looks_config = ../appearance.cfg + +# Path to python file. Can use relative path from here. +python_file = ./runner.py + +# Name of the bot in-game +name = Runner + +[Details] +# These values are optional but useful metadata for helper programs +# Name of the bot's creator/developer +developer = r0bbi3 + +# Short description of the bot +description = + +# Fun fact about the bot +fun_fact = + +# Link to github repository +github = + +# Programming language +language = python diff --git a/bots/runner/runner.py b/bots/runner/runner.py new file mode 100644 index 0000000..b72bf3f --- /dev/null +++ b/bots/runner/runner.py @@ -0,0 +1,50 @@ +import os +import sys + +from rlbot.agents.base_agent import BaseAgent, SimpleControllerState +from rlbot.utils.structures.game_data_struct import GameTickPacket + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from utility.collect_data import format_data, data_size, from_labels + + +model_extension = '.h5' +model_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'models/') + + +class Runner(BaseAgent): + + def initialize_agent(self): + self.controller_state = SimpleControllerState() + + # Tensorflow + import tensorflow as tf + from tensorflow.keras import layers + + # Network + model_name = find_model() + self.model = tf.keras.models.load_model(model_path + model_name) + print('[' + self.name + '] Loaded model: ' + str(model_path + model_name).replace('\\', '/')) + + def get_output(self, packet: GameTickPacket) -> SimpleControllerState: + car = packet.game_cars[self.index] + if not packet.game_info.is_round_active or car.is_demolished: + return self.controller_state + + data = format_data(self.index, packet, self.get_ball_prediction_struct()) + + # Get our own predicted output + output = self.model.predict(data.reshape((1, data_size))) + + self.controller_state = from_labels(output) + return self.controller_state + + +# Find a model. +def find_model(manual_name: str = None): + for i in range(2 if manual_name else 1): + for file in os.listdir(model_path): + if not file.endswith(model_extension): continue + if i == 0 and manual_name and str(file) != manual_name: continue + return file + return None diff --git a/bots/teacher/__init__.py b/bots/teacher/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bots/teacher/teacher.cfg b/bots/teacher/teacher.cfg new file mode 100644 index 0000000..586a6ea --- /dev/null +++ b/bots/teacher/teacher.cfg @@ -0,0 +1,26 @@ +[Locations] +# Path to loadout config. Can use relative path from here. +looks_config = ../appearance.cfg + +# Path to python file. Can use relative path from here. +python_file = ./teacher.py + +# Name of the bot in-game +name = Teacher + +[Details] +# These values are optional but useful metadata for helper programs +# Name of the bot's creator/developer +developer = r0bbi3 + +# Short description of the bot +description = + +# Fun fact about the bot +fun_fact = + +# Link to github repository +github = + +# Programming language +language = python diff --git a/python_example/python_example.py b/bots/teacher/teacher.py similarity index 83% rename from python_example/python_example.py rename to bots/teacher/teacher.py index ccaabfe..042b79f 100644 --- a/python_example/python_example.py +++ b/bots/teacher/teacher.py @@ -1,13 +1,17 @@ import math +import os +import sys from rlbot.agents.base_agent import BaseAgent, SimpleControllerState from rlbot.utils.structures.game_data_struct import GameTickPacket -from util.orientation import Orientation -from util.vec import Vec3 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from utility.orientation import Orientation +from utility.vec import Vec3 +from utility.utility import * -class PythonExample(BaseAgent): +class Teacher(BaseAgent): def initialize_agent(self): # This runs once before the bot starts up @@ -26,19 +30,19 @@ def get_output(self, packet: GameTickPacket) -> SimpleControllerState: car_direction = car_orientation.forward steer_correction_radians = find_correction(car_direction, car_to_ball) + turn = clamp11(steer_correction_radians * -3) + #turn = -math.copysign(1, steer_correction_radians) 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) + #draw_debug(self.renderer, my_car, packet.game_ball, action_display) return self.controller_state diff --git a/bots/utility/__init__.py b/bots/utility/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bots/utility/collect_data.py b/bots/utility/collect_data.py new file mode 100644 index 0000000..4ee1b24 --- /dev/null +++ b/bots/utility/collect_data.py @@ -0,0 +1,175 @@ +''' +BALL: +ball: (X, Y, Z) / pitch width +ball velocity normalised: X, Y, Z +ball velocity magnitude / pitch width + +CARS: +our car: (X, Y, Z) / pitch width +our car nose: X, Y, Z +local angular velocity: X, Y, Z +local ball normalised: X, Y, Z +local ball prediction normalised (each half second for 3 seconds): X, Y, Z +our ball distance / pitch width +our car boost / 100 +car velocity magnitude / pitch width +car velocity forward component +is super sonic +has wheel contact +has double jumped +repeat all our car info for closest opponent to ball + +MISC: +is kickoff +is round active +''' + +from math import log10 as log + +import numpy as np +from rlbot.utils.structures.game_data_struct import GameTickPacket, PlayerInfo, GameInfo, BallInfo, rotate_game_tick_packet_boost_omitted as flip_packet +from rlbot.utils.structures.ball_prediction_struct import BallPrediction +from rlbot.agents.base_agent import SimpleControllerState + +from .vec import Vec3 +from .constants import * +from .orientation import Orientation, relative_location +from .utility import * + + +opponent_data = False + +data_size = (3 + 3 + 1 + (3 * 3) + (3 + 3 + 3 + 3 + 18 + 7) * (2 if opponent_data else 1) + 2) +car_data_size = 37 +label_size = (5, 2, 1) + + +def format_data(index: int, packet: GameTickPacket, prediction: BallPrediction): + data = np.zeros(shape = data_size) # Blank data + + flip = (packet.game_cars[index].team == 1) + if flip: flip_packet(packet) + + # Ball + ball: BallData = packet.game_ball + ball_position = Vec3(ball.physics.location) / pitch_side_uu + ball_velocity = Vec3(ball.physics.velocity) + ball_velocity_magnitude = ball_velocity.length() / pitch_side_uu + ball_velocity = ball_velocity.normalised() + data[0] = ball_position.x + data[1] = ball_position.y + data[2] = ball_position.z + data[3] = ball_velocity.x + data[4] = ball_velocity.y + data[5] = ball_velocity.z + data[6] = ball_velocity_magnitude + + # Ball prediction + ball_position = Vec3(ball.physics.location) # Rescale + + + # Cars + my_car = packet.game_cars[index] + enemy_car = (get_enemy_car(index, packet) if opponent_data else None) + for i, car in enumerate([my_car, enemy_car]): + if not car: continue + car_position = Vec3(car.physics.location) / pitch_side_uu + data[16 + i * car_data_size] = car_position.x + data[17 + i * car_data_size] = car_position.y + data[18 + i * car_data_size] = car_position.z + car_orientation = Orientation(car.physics.rotation) + car_direction = car_orientation.forward.normalised() + data[19 + i * car_data_size] = car_direction.x + data[20 + i * car_data_size] = car_direction.y + data[21 + i * car_data_size] = car_direction.z + car_position = Vec3(car.physics.location) # Rescale + local = relative_location(car_position, car_orientation, ball_position).normalised() + data[22 + i * car_data_size] = local.x + data[23 + i * car_data_size] = local.y + data[24 + i * car_data_size] = local.z + for j in range(6): + frame = (j + 1) * 30 + predicted_location = Vec3(prediction.slices[frame].physics.location) + if flip: predicted_location = Vec3(-predicted_location.x, -predicted_location.y, predicted_location.z) + local = relative_location(car_position, car_orientation, predicted_location).normalised() + data[25 + i * car_data_size + j * 3] = local.x + data[26 + i * car_data_size + j * 3] = local.y + data[27 + i * car_data_size + j * 3] = local.z + data[43 + i * car_data_size] = car_position.dist(ball_position) / pitch_side_uu + data[44 + i * car_data_size] = car.boost / 100 + car_velocity_magnitude = Vec3(car.physics.velocity).length() + data[45 + i * car_data_size] = car_velocity_magnitude / pitch_side_uu + data[46 + i * car_data_size] = car_direction.dot(Vec3(car.physics.velocity).normalised()) + data[47 + i * car_data_size] = (1 if car.is_super_sonic else -1) + data[48 + i * car_data_size] = (1 if car.has_wheel_contact else -1) + data[49 + i * car_data_size] = (1 if not car.double_jumped else -1) + ang_vel = relative_location(Vec3(0, 0, 0), car_orientation, Vec3(car.physics.angular_velocity)) + data[50 + i * car_data_size] = ang_vel.x + data[51 + i * car_data_size] = ang_vel.y + data[52 + i * car_data_size] = ang_vel.z + + # Misc + data[53 + (0 if not opponent_data else car_data_size)] = (1 if packet.game_info.is_kickoff_pause else -1) + data[54 + (0 if not opponent_data else car_data_size)] = (1 if packet.game_info.is_round_active else -1) + + return data + + +def format_labels(controls: SimpleControllerState, car: PlayerInfo, mask: bool = False): + labels = np.array([np.zeros(label_size[0]), np.zeros(label_size[1]), np.zeros(label_size[2])]) # Blank labels + + if mask: + labels[0][0] = (1 if controls.boost and car.boost >= 1 else clamp11(controls.throttle)) + air: bool = (not car.has_wheel_contact or (controls.jump and not car.double_jumped)) + labels[0][1] = (clamp11(controls.steer) if not air else 0) + labels[0][2] = (clamp11(controls.pitch) if air else 0) + labels[0][3] = (clamp11(controls.yaw) if air else 0) + labels[0][4] = (clamp11(controls.roll) if air else 0) + labels[1][0] = (1 if controls.boost and car.boost >= 1 else -1) + labels[1][1] = (1 if controls.handbrake and not air else -1) + labels[2][0] = (1 if controls.jump and (air or not car.double_jumped) else -1) + else: + labels[0][0] = clamp11(controls.throttle) + labels[0][1] = clamp11(controls.steer) + labels[0][2] = clamp11(controls.pitch) + labels[0][3] = clamp11(controls.yaw) + labels[0][4] = clamp11(controls.roll) + labels[1][0] = (1 if controls.boost else -1) + labels[1][1] = (1 if controls.handbrake else -1) + labels[2][0] = (1 if controls.jump else -1) + + return labels + + +def from_labels(labels) -> SimpleControllerState: + controls = SimpleControllerState() + labels = (labels[0][0], labels[1][0], labels[2][0]) + controls.throttle = clamp11(labels[0][0]) + controls.steer = clamp11(labels[0][1]) + controls.pitch = clamp11(labels[0][2]) + controls.yaw = clamp11(labels[0][3]) + controls.roll = clamp11(labels[0][4]) + controls.boost = (labels[1][0] > 0) + controls.handbrake = (labels[1][1] > 0) + controls.jump = (labels[2][0] > 0) + controls.use_item = False + return controls + + +def get_enemy_car(index: int, packet: GameTickPacket) -> PlayerInfo: + ball_position = Vec3(packet.game_ball.physics.location) + team = (1 - packet.game_cars[index].team) + + closest_car = None + min_distance = 0 + + for index, car in enumerate(packet.game_cars[:packet.num_cars]): + if not car or car.team != team: continue + car_position = Vec3(car.physics.location) + distance = car_position.dist(ball_position) + + if not closest_car or distance < min_distance: + closest_car = car + min_distance = distance + + return closest_car diff --git a/bots/utility/constants.py b/bots/utility/constants.py new file mode 100644 index 0000000..bc564b1 --- /dev/null +++ b/bots/utility/constants.py @@ -0,0 +1,21 @@ +# https://discordapp.com/channels/348658686962696195/446761380654219264/546468104348237854 +ball_radius_uu = 92.75 +octane_com_height_uu = 16.5 +gravity_uu_s2 = 650.0 +max_throttle_speed_uu_s = 1410.0 +max_boost_speed_uu_s = 2300.0 +car_mass = 180.0 +accel_boost_uu_s2 = 991.0 + 2.0/3.0 +accel_brake_uu_s2 = -3500.0 +accel_coast_uu_s2 = -525.0 +ball_restitution = 0.6 +jump_hold_duration_s = 0.2 +max_angular_accel_yaw_rads_s2 = 9.11 +max_angular_accel_pitch_rads_s2 = 12.46 +max_angular_accel_roll_rads_s2 = 38.34 +jump_initial_vel_uu_s = 300.0 +accel_jumping_uu_s2 = 1400.0 +physics_slice_time_s = 1.0/60.0 +pitch_side_uu = 4096.0 +pitch_back_uu = 5120.0 +pitch_ceiling_uu = 2044.0 diff --git a/bots/utility/dummy_renderer.py b/bots/utility/dummy_renderer.py new file mode 100644 index 0000000..f1b5ef9 --- /dev/null +++ b/bots/utility/dummy_renderer.py @@ -0,0 +1,51 @@ +from rlbot.utils.rendering.rendering_manager import RenderingManager + +class DummyRenderer(RenderingManager): + + def __init__(self, renderer): + self.renderGroup = renderer.renderGroup + self.render_state = renderer.render_state + self.builder = renderer.builder + self.render_list = renderer.render_list + self.group_id = renderer.group_id + self.bot_index = renderer.bot_index + self.bot_team = renderer.bot_team + + def clear_screen(self, group_id='default'): + pass + + def draw_line_2d(self, x1, y1, x2, y2, color): + return self + + def draw_polyline_2d(self, vectors, color): + return self + + def draw_line_3d(self, vec1, vec2, color): + return self + + def draw_polyline_3d(self, vectors, color): + return self + + def draw_line_2d_3d(self, x, y, vec, color): + return self + + def draw_rect_2d(self, x, y, width, height, filled, color): + return self + + def draw_rect_3d(self, vec, width, height, filled, color, centered=False): + return self + + def draw_string_2d(self, x, y, scale_x, scale_y, text, color): + return self + + def draw_string_3d(self, vec, scale_x, scale_y, text, color): + return self + + def begin_rendering(self, group_id='default'): + pass + + def end_rendering(self): + pass + + def clear_all_touched_render_groups(self): + pass diff --git a/python_example/util/orientation.py b/bots/utility/orientation.py similarity index 98% rename from python_example/util/orientation.py rename to bots/utility/orientation.py index 2cf2e9a..a2d54ed 100644 --- a/python_example/util/orientation.py +++ b/bots/utility/orientation.py @@ -1,6 +1,6 @@ import math -from util.vec import Vec3 +from utility.vec import Vec3 # This is a helper class for calculating directions relative to your car. You can extend it or delete if you want. diff --git a/bots/utility/utility.py b/bots/utility/utility.py new file mode 100644 index 0000000..a249634 --- /dev/null +++ b/bots/utility/utility.py @@ -0,0 +1,5 @@ +def clamp11(value): + return max(-1, min(1, value)) + +def clamp01(value): + return max(0, min(1, value)) diff --git a/python_example/util/vec.py b/bots/utility/vec.py similarity index 95% rename from python_example/util/vec.py rename to bots/utility/vec.py index 6850e4f..c956fd0 100644 --- a/python_example/util/vec.py +++ b/bots/utility/vec.py @@ -74,7 +74,12 @@ def dist(self, other: 'Vec3') -> float: def normalized(self): """Returns a vector with the same direction but a length of one.""" - return self / self.length() + length = self.length() + if length == 0: return Vec3(self) + return self / length + + def normalised(self): + return self.normalized() def rescale(self, new_len: float) -> 'Vec3': """Returns a vector with the same direction but a different length.""" diff --git a/python_example/appearance.cfg b/python_example/appearance.cfg deleted file mode 100644 index 468bbe3..0000000 --- a/python_example/appearance.cfg +++ /dev/null @@ -1,49 +0,0 @@ -[Bot Loadout] -team_color_id = 60 -custom_color_id = 0 -car_id = 23 -decal_id = 0 -wheels_id = 1565 -boost_id = 35 -antenna_id = 0 -hat_id = 0 -paint_finish_id = 1681 -custom_finish_id = 1681 -engine_audio_id = 0 -trails_id = 3220 -goal_explosion_id = 3018 - -[Bot Loadout Orange] -team_color_id = 3 -custom_color_id = 0 -car_id = 23 -decal_id = 0 -wheels_id = 1565 -boost_id = 35 -antenna_id = 0 -hat_id = 0 -paint_finish_id = 1681 -custom_finish_id = 1681 -engine_audio_id = 0 -trails_id = 3220 -goal_explosion_id = 3018 - -[Bot Paint Blue] -car_paint_id = 12 -decal_paint_id = 0 -wheels_paint_id = 7 -boost_paint_id = 7 -antenna_paint_id = 0 -hat_paint_id = 0 -trails_paint_id = 2 -goal_explosion_paint_id = 0 - -[Bot Paint Orange] -car_paint_id = 12 -decal_paint_id = 0 -wheels_paint_id = 14 -boost_paint_id = 14 -antenna_paint_id = 0 -hat_paint_id = 0 -trails_paint_id = 14 -goal_explosion_paint_id = 0 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 25c0c97..0000000 --- a/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -# Include everything the framework requires -# You will automatically get updates for all versions starting with "1.". -rlbot==1.* -rlbottraining - -# This will cause pip to auto-upgrade and stop scaring people with warning messages -pip diff --git a/rlbot.cfg b/rlbot.cfg deleted file mode 100644 index 3424a1a..0000000 --- a/rlbot.cfg +++ /dev/null @@ -1,73 +0,0 @@ -[RLBot Configuration] -# Visit https://github.com/RLBot/RLBot/wiki/Config-File-Documentation to see what you can put here. - -[Team Configuration] -# 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. -num_participants = 2 -game_mode = Soccer -game_map = Mannfield - -[Mutator Configuration] -# Visit https://github.com/RLBot/RLBot/wiki/Config-File-Documentation to see what you can put here. - -[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 - -# team 0 shoots on positive goal, team 1 shoots on negative goal -participant_team_0 = 0 -participant_team_1 = 1 -participant_team_2 = 0 -participant_team_3 = 1 -participant_team_4 = 0 -participant_team_5 = 1 -participant_team_6 = 0 -participant_team_7 = 1 -participant_team_8 = 0 -participant_team_9 = 1 - -# Accepted values are "human", "rlbot", "psyonix", and "party_member_bot" -# You can have up to 4 local players and they must be activated in game or it will crash. -# If no player is specified you will be spawned in as spectator! -# human - not controlled by the framework -# rlbot - controlled by the framework -# psyonix - default bots (skill level can be changed with participant_bot_skill -# party_member_bot - controlled by the framework but the game detects it as a human -participant_type_0 = rlbot -participant_type_1 = rlbot -participant_type_2 = rlbot -participant_type_3 = rlbot -participant_type_4 = rlbot -participant_type_5 = rlbot -participant_type_6 = rlbot -participant_type_7 = rlbot -participant_type_8 = rlbot -participant_type_9 = rlbot - - -# If participant is a bot and not RLBot controlled, this value will be used to set bot skill. -# 0.0 is Rookie, 0.5 is pro, 1.0 is all-star. You can set values in-between as well. -# Please leave a value here even if it isn't used :) -participant_bot_skill_0 = 1.0 -participant_bot_skill_1 = 1.0 -participant_bot_skill_2 = 1.0 -participant_bot_skill_3 = 1.0 -participant_bot_skill_4 = 1.0 -participant_bot_skill_5 = 1.0 -participant_bot_skill_6 = 1.0 -participant_bot_skill_7 = 1.0 -participant_bot_skill_8 = 1.0 -participant_bot_skill_9 = 1.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 deleted file mode 100644 index a679076..0000000 --- a/run.py +++ /dev/null @@ -1,43 +0,0 @@ -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__': - - try: - from rlbot.utils import public_utils, logging_utils - - logger = logging_utils.get_logger(DEFAULT_LOGGER) - if not public_utils.have_internet(): - 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']) - - # https://stackoverflow.com/a/44401013 - rlbots = [module for module in sys.modules if module.startswith('rlbot')] - for rlbot_module in rlbots: - sys.modules.pop(rlbot_module) - - except ImportError: - pipmain(['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() - except Exception as e: - print("Encountered exception: ", e) - print("Press enter to close.") - input() diff --git a/training/drive_to_ball_grader.py b/training/drive_to_ball_grader.py deleted file mode 100644 index 948be82..0000000 --- a/training/drive_to_ball_grader.py +++ /dev/null @@ -1,48 +0,0 @@ -from dataclasses import dataclass, field -from math import sqrt -from typing import Optional - -from rlbot.training.training import Grade, Pass, Fail - -from rlbottraining.grading.training_tick_packet import TrainingTickPacket -from rlbottraining.common_graders.timeout import FailOnTimeout -from rlbottraining.common_graders.compound_grader import CompoundGrader -from rlbottraining.grading.grader import Grader - - -""" -This file shows how to create Graders which specify when the Exercises finish -and whether the bots passed the exercise. -""" - - -class DriveToBallGrader(CompoundGrader): - """ - Checks that the car gets to the ball in a reasonable amount of time. - """ - def __init__(self, timeout_seconds=4.0, min_dist_to_pass=200): - super().__init__([ - PassOnNearBall(min_dist_to_pass=min_dist_to_pass), - FailOnTimeout(timeout_seconds), - ]) - -@dataclass -class PassOnNearBall(Grader): - """ - Returns a Pass grade once the car is sufficiently close to the ball. - """ - - min_dist_to_pass: float = 200 - car_index: int = 0 - - def on_tick(self, tick: TrainingTickPacket) -> Optional[Grade]: - car = tick.game_tick_packet.game_cars[self.car_index].physics.location - ball = tick.game_tick_packet.game_ball.physics.location - - dist = sqrt( - (car.x - ball.x) ** 2 + - (car.y - ball.y) ** 2 - ) - if dist <= self.min_dist_to_pass: - return Pass() - return None diff --git a/training/example_playlist.py b/training/example_playlist.py deleted file mode 100644 index 860ba20..0000000 --- a/training/example_playlist.py +++ /dev/null @@ -1,12 +0,0 @@ -import hello_world_training -import rlbottraining.common_exercises.bronze_goalie as bronze_goalie - -def make_default_playlist(): - exercises = ( - hello_world_training.make_default_playlist() + - bronze_goalie.make_default_playlist() - ) - for exercise in exercises: - exercise.match_config = hello_world_training.make_match_config_with_my_bot() - - return exercises diff --git a/training/hello_world_training.py b/training/hello_world_training.py deleted file mode 100644 index f25bb2e..0000000 --- a/training/hello_world_training.py +++ /dev/null @@ -1,96 +0,0 @@ -from pathlib import Path -from dataclasses import dataclass, field -from math import pi - -from rlbot.utils.game_state_util import GameState, BoostState, BallState, CarState, Physics, Vector3, Rotator -from rlbot.matchconfig.match_config import MatchConfig, PlayerConfig, Team -from rlbottraining.common_exercises.common_base_exercises import StrikerExercise -from rlbottraining.rng import SeededRandomNumberGenerator -from rlbottraining.match_configs import make_empty_match_config -from rlbottraining.grading.grader import Grader -from rlbottraining.training_exercise import TrainingExercise, Playlist - -import training_util -from drive_to_ball_grader import DriveToBallGrader - - -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 - match_config = make_empty_match_config() - match_config.player_configs = [ - PlayerConfig.bot_config( - Path(__file__).absolute().parent.parent / 'python_example' / 'python_example.cfg', - Team.BLUE - ), - ] - return match_config - -@dataclass -class StrikerPatience(StrikerExercise): - """ - Drops the ball from a certain height, requiring the bot to not drive - underneath the ball until it's in reach. - """ - - car_start_x: float = 0 - - def make_game_state(self, rng: SeededRandomNumberGenerator) -> GameState: - return GameState( - ball=BallState(physics=Physics( - location=Vector3(0, 4400, 1000), - velocity=Vector3(0, 0, 200), - angular_velocity=Vector3(0, 0, 0))), - cars={ - 0: CarState( - physics=Physics( - location=Vector3(self.car_start_x, 3000, 0), - rotation=Rotator(0, pi / 2, 0), - velocity=Vector3(0, 0, 0), - angular_velocity=Vector3(0, 0, 0)), - jumped=False, - double_jumped=False, - boost_amount=0) - }, - boosts={i: BoostState(0) for i in range(34)}, - ) - -@dataclass -class DrivesToBallExercise(TrainingExercise): - """ - Checks that we drive to the ball when it's in the center of the field. - """ - grader: Grader = field(default_factory=DriveToBallGrader) - - def make_game_state(self, rng: SeededRandomNumberGenerator) -> GameState: - return GameState( - ball=BallState(physics=Physics( - location=Vector3(0, 0, 100), - velocity=Vector3(0, 0, 0), - angular_velocity=Vector3(0, 0, 0))), - cars={ - 0: CarState( - physics=Physics( - location=Vector3(0, 2000, 0), - rotation=Rotator(0, -pi / 2, 0), - velocity=Vector3(0, 0, 0), - angular_velocity=Vector3(0, 0, 0)), - jumped=False, - double_jumped=False, - boost_amount=100) - }, - boosts={i: BoostState(0) for i in range(34)}, - ) - - -def make_default_playlist() -> Playlist: - exercises = [ - StrikerPatience('start perfectly center'), - StrikerPatience('start on the right', car_start_x=-1000), - 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 diff --git a/training/training_util.py b/training/training_util.py deleted file mode 100644 index 4ac53da..0000000 --- a/training/training_util.py +++ /dev/null @@ -1,7 +0,0 @@ -from rlbottraining.rng import SeededRandomNumberGenerator - -from rlbot.utils.game_state_util import Vector3 - - -def get_car_start_near_goal(rng: SeededRandomNumberGenerator) -> Vector3: - return Vector3(rng.uniform(1000, 2000), 3000, 0) diff --git a/training/unit_tests.py b/training/unit_tests.py deleted file mode 100644 index a147c35..0000000 --- a/training/unit_tests.py +++ /dev/null @@ -1,37 +0,0 @@ -import unittest - -from rlbot.training.training import Pass, Fail -from rlbottraining.exercise_runner import run_playlist - -from hello_world_training import StrikerPatience - -class PatienceTest(unittest.TestCase): - """ - These units check that this bot behaves as we expect, - with regards to the StrikerPatience exercise. - - By default, the bot isn't very smart so it'll fail in the cases where - patience is required but passes in cases where no patience is required. - - Tutorial: - https://youtu.be/hCw250aGN8c?list=PL6LKXu1RlPdxh9vxmG1y2sghQwK47_gCH&t=187 - """ - - def test_patience_required(self): - result_iter = run_playlist([StrikerPatience(name='patience required')]) - results = list(result_iter) - self.assertEqual(len(results), 1) - result = results[0] - self.assertEqual(result.exercise.name, 'patience required') - 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)]) - results = list(result_iter) - self.assertEqual(len(results), 1) - result = results[0] - self.assertEqual(result.exercise.name, 'no patience required') - self.assertIsInstance(result.grade, Pass) - -if __name__ == '__main__': - unittest.main()