├── .env ├── .gitignore ├── .pylintrc ├── LICENSE ├── README.md ├── RefreshEnv.cmd ├── rlbot.cfg ├── run.py ├── run_gui.py └── src ├── __init__.py ├── action ├── continuous_act.py ├── default_act.py └── discrete_act.py ├── agent.py ├── appearance.cfg ├── bot.cfg ├── bot.py ├── obs ├── advanced_obs.py └── default_obs.py └── requirements.txt /.env: -------------------------------------------------------------------------------- 1 | PYTHONPATH=src 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # virtualenv 83 | .venv 84 | venv/ 85 | ENV/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | .spyproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | 94 | # mkdocs documentation 95 | /site 96 | 97 | # mypy 98 | .mypy_cache/ 99 | 100 | # Intellij 101 | *.iml 102 | /.idea 103 | 104 | # Build output 105 | /build 106 | 107 | # Gradle files 108 | /.gradle 109 | 110 | # VSCode 111 | .vscode/ 112 | *.code-workspace 113 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [TYPECHECK] 2 | generated-members=QuickChats.* 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 RLGym 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RLGymExampleBot 2 | RLGym example bot for the RLBot framework, based on the official RLBotPythonExample 3 | 4 | ## How to use this 5 | 6 | This bot runs the Actor class in `src/actor.py`, you're expected to replace that with the output of your model 7 | 8 | By default we use DefaultObs from RLGym, AdvancedObs is also available in this project. 9 | 10 | You can also provide your own custom ObservationBuilder by copying it over and replacing the `rlgym` imports with `rlgym_compat` (check `src/obs/` for some examples) 11 | 12 | ## Changing the bot 13 | 14 | - Bot behavior is controlled by `src/bot.py` 15 | - Bot appearance is controlled by `src/appearance.cfg` 16 | 17 | See https://github.com/RLBot/RLBotPythonExample/wiki for documentation and tutorials. 18 | 19 | ## Running a match 20 | 21 | You can start a match by running `run.py`, the match config for it is in `rlbot.cfg` 22 | -------------------------------------------------------------------------------- /RefreshEnv.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | :: This file is taken from chocolatey: 3 | :: https://github.com/chocolatey/choco/blob/master/src/chocolatey.resources/redirects/RefreshEnv.cmd 4 | :: 5 | :: RefreshEnv.cmd 6 | :: 7 | :: Batch file to read environment variables from registry and 8 | :: set session variables to these values. 9 | :: 10 | :: With this batch file, there should be no need to reload command 11 | :: environment every time you want environment changes to propagate 12 | 13 | ::echo "RefreshEnv.cmd only works from cmd.exe, please install the Chocolatey Profile to take advantage of refreshenv from PowerShell" 14 | echo | set /p dummy="Refreshing environment variables from registry for cmd.exe. Please wait..." 15 | 16 | goto main 17 | 18 | :: Set one environment variable from registry key 19 | :SetFromReg 20 | "%WinDir%\System32\Reg" QUERY "%~1" /v "%~2" > "%TEMP%\_envset.tmp" 2>NUL 21 | for /f "usebackq skip=2 tokens=2,*" %%A IN ("%TEMP%\_envset.tmp") do ( 22 | echo/set "%~3=%%B" 23 | ) 24 | goto :EOF 25 | 26 | :: Get a list of environment variables from registry 27 | :GetRegEnv 28 | "%WinDir%\System32\Reg" QUERY "%~1" > "%TEMP%\_envget.tmp" 29 | for /f "usebackq skip=2" %%A IN ("%TEMP%\_envget.tmp") do ( 30 | if /I not "%%~A"=="Path" ( 31 | call :SetFromReg "%~1" "%%~A" "%%~A" 32 | ) 33 | ) 34 | goto :EOF 35 | 36 | :main 37 | echo/@echo off >"%TEMP%\_env.cmd" 38 | 39 | :: Slowly generating final file 40 | call :GetRegEnv "HKLM\System\CurrentControlSet\Control\Session Manager\Environment" >> "%TEMP%\_env.cmd" 41 | call :GetRegEnv "HKCU\Environment">>"%TEMP%\_env.cmd" >> "%TEMP%\_env.cmd" 42 | 43 | :: Special handling for PATH - mix both User and System 44 | call :SetFromReg "HKLM\System\CurrentControlSet\Control\Session Manager\Environment" Path Path_HKLM >> "%TEMP%\_env.cmd" 45 | call :SetFromReg "HKCU\Environment" Path Path_HKCU >> "%TEMP%\_env.cmd" 46 | 47 | :: Caution: do not insert space-chars before >> redirection sign 48 | echo/set "Path=%%Path_HKLM%%;%%Path_HKCU%%" >> "%TEMP%\_env.cmd" 49 | 50 | :: Cleanup 51 | del /f /q "%TEMP%\_envset.tmp" 2>nul 52 | del /f /q "%TEMP%\_envget.tmp" 2>nul 53 | 54 | :: capture user / architecture 55 | SET "OriginalUserName=%USERNAME%" 56 | SET "OriginalArchitecture=%PROCESSOR_ARCHITECTURE%" 57 | 58 | :: Set these variables 59 | call "%TEMP%\_env.cmd" 60 | 61 | :: reset user / architecture 62 | SET "USERNAME=%OriginalUserName%" 63 | SET "PROCESSOR_ARCHITECTURE=%OriginalArchitecture%" 64 | 65 | echo | set /p dummy="Finished." 66 | echo . -------------------------------------------------------------------------------- /rlbot.cfg: -------------------------------------------------------------------------------- 1 | [RLBot Configuration] 2 | # Visit https://github.com/RLBot/RLBot/wiki/Config-File-Documentation to see what you can put here. 3 | 4 | [Team Configuration] 5 | # Visit https://github.com/RLBot/RLBot/wiki/Config-File-Documentation to see what you can put here. 6 | 7 | [Match Configuration] 8 | # Visit https://github.com/RLBot/RLBot/wiki/Config-File-Documentation to see what you can put here. 9 | # Number of bots/players which will be spawned. We support up to max 64. 10 | num_participants = 6 11 | game_mode = Soccer 12 | game_map = Mannfield 13 | enable_rendering = True 14 | enable_state_setting = True 15 | 16 | [Mutator Configuration] 17 | # Visit https://github.com/RLBot/RLBot/wiki/Config-File-Documentation to see what you can put here. 18 | 19 | [Participant Configuration] 20 | # Put the name of your bot config file here. Only num_participants config files will be read! 21 | # Everything needs a config, even players and default bots. We still set loadouts and names from config! 22 | participant_config_0 = src/bot.cfg 23 | participant_config_1 = src/bot.cfg 24 | participant_config_2 = src/bot.cfg 25 | participant_config_3 = src/bot.cfg 26 | participant_config_4 = src/bot.cfg 27 | participant_config_5 = src/bot.cfg 28 | participant_config_6 = src/bot.cfg 29 | participant_config_7 = src/bot.cfg 30 | participant_config_8 = src/bot.cfg 31 | participant_config_9 = src/bot.cfg 32 | 33 | # team 0 shoots on positive goal, team 1 shoots on negative goal 34 | participant_team_0 = 0 35 | participant_team_1 = 1 36 | participant_team_2 = 0 37 | participant_team_3 = 1 38 | participant_team_4 = 0 39 | participant_team_5 = 1 40 | participant_team_6 = 0 41 | participant_team_7 = 1 42 | participant_team_8 = 0 43 | participant_team_9 = 1 44 | 45 | # Accepted values are "human", "rlbot", "psyonix", and "party_member_bot" 46 | # You can have up to 4 local players and they must be activated in game or it will crash. 47 | # If no player is specified you will be spawned in as spectator! 48 | # human - not controlled by the framework 49 | # rlbot - controlled by the framework 50 | # psyonix - default bots (skill level can be changed with participant_bot_skill 51 | # party_member_bot - controlled by the framework but the game detects it as a human 52 | participant_type_0 = rlbot 53 | participant_type_1 = rlbot 54 | participant_type_2 = rlbot 55 | participant_type_3 = rlbot 56 | participant_type_4 = rlbot 57 | participant_type_5 = rlbot 58 | participant_type_6 = rlbot 59 | participant_type_7 = rlbot 60 | participant_type_8 = rlbot 61 | participant_type_9 = rlbot 62 | 63 | 64 | # If participant is a bot and not RLBot controlled, this value will be used to set bot skill. 65 | # 0.0 is Rookie, 0.5 is pro, 1.0 is all-star. You can set values in-between as well. 66 | # Please leave a value here even if it isn't used :) 67 | participant_bot_skill_0 = 1.0 68 | participant_bot_skill_1 = 1.0 69 | participant_bot_skill_2 = 1.0 70 | participant_bot_skill_3 = 1.0 71 | participant_bot_skill_4 = 1.0 72 | participant_bot_skill_5 = 1.0 73 | participant_bot_skill_6 = 1.0 74 | participant_bot_skill_7 = 1.0 75 | participant_bot_skill_8 = 1.0 76 | participant_bot_skill_9 = 1.0 77 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | 4 | DEFAULT_LOGGER = 'rlbot' 5 | 6 | if __name__ == '__main__': 7 | 8 | try: 9 | from rlbot.utils import public_utils, logging_utils 10 | 11 | logger = logging_utils.get_logger(DEFAULT_LOGGER) 12 | if not public_utils.have_internet(): 13 | logger.log(logging_utils.logging_level, 14 | 'Skipping upgrade check for now since it looks like you have no internet') 15 | elif public_utils.is_safe_to_upgrade(): 16 | subprocess.call([sys.executable, "-m", "pip", "install", '-r', 'requirements.txt']) 17 | subprocess.call([sys.executable, "-m", "pip", "install", 'rlbot', '--upgrade']) 18 | 19 | # https://stackoverflow.com/a/44401013 20 | rlbots = [module for module in sys.modules if module.startswith('rlbot')] 21 | for rlbot_module in rlbots: 22 | sys.modules.pop(rlbot_module) 23 | 24 | except ImportError: 25 | subprocess.call([sys.executable, "-m", "pip", "install", '-r', 'requirements.txt', '--upgrade', '--upgrade-strategy=eager']) 26 | 27 | try: 28 | from rlbot import runner 29 | runner.main() 30 | except Exception as e: 31 | print("Encountered exception: ", e) 32 | print("Press enter to close.") 33 | input() 34 | -------------------------------------------------------------------------------- /run_gui.py: -------------------------------------------------------------------------------- 1 | from rlbot_gui import gui 2 | 3 | # This is a useful way to start up RLBotGUI directly from your bot project. You can use it to 4 | # arrange a match with the settings you like, and if you have a good IDE like PyCharm, 5 | # you can do breakpoint debugging on your bot. 6 | if __name__ == '__main__': 7 | gui.start() 8 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RLGym/RLGymExampleBot/5411c07e2b0fb8ab6cdbcc4afa058a724aec5aca/src/__init__.py -------------------------------------------------------------------------------- /src/action/continuous_act.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from rlgym_compat import GameState 3 | 4 | 5 | class ContinuousAction: 6 | """ 7 | Simple continuous action space. Operates in the range -1 to 1, even for the binary actions which are converted back to binary later. 8 | This is for improved compatibility, stable baselines doesn't support tuple spaces right now. 9 | """ 10 | 11 | def __init__(self): 12 | pass 13 | 14 | def get_action_space(self): 15 | raise NotImplementedError("We don't implement get_action_space to remove the gym dependency") 16 | 17 | def parse_actions(self, actions: np.ndarray, state: GameState) -> np.ndarray: 18 | actions = actions.reshape((-1, 8)) 19 | 20 | actions[..., :5] = actions[..., :5].clip(-1, 1) 21 | # The final 3 actions handle are jump, boost and handbrake. They are inherently discrete so we convert them to either 0 or 1. 22 | actions[..., 5:] = actions[..., 5:] > 0 23 | 24 | return actions -------------------------------------------------------------------------------- /src/action/default_act.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from rlgym_compat import GameState 3 | from .continuous_act import ContinuousAction 4 | from typing import Union, List 5 | 6 | 7 | class DefaultAction(ContinuousAction): 8 | """ 9 | Continuous Action space, that also accepts a few other input formats for QoL reasons and to remain 10 | compatible with older versions. 11 | """ 12 | 13 | def __init__(self): 14 | super().__init__() 15 | 16 | def get_action_space(self): 17 | return super().get_action_space() 18 | 19 | def parse_actions(self, actions: Union[np.ndarray, List[np.ndarray], List[float]], state: GameState) -> np.ndarray: 20 | 21 | # allow other data types, this part should not be necessary but is nice to have in the default action parser. 22 | if type(actions) != np.ndarray: 23 | actions = np.asarray(actions) 24 | 25 | if len(actions.shape) == 1: 26 | actions = actions.reshape((-1, 8)) 27 | elif len(actions.shape) > 2: 28 | raise ValueError('{} is not a valid action shape'.format(actions.shape)) 29 | 30 | return super().parse_actions(actions, state) 31 | -------------------------------------------------------------------------------- /src/action/discrete_act.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from rlgym_compat import GameState 3 | 4 | 5 | class DiscreteAction: 6 | """ 7 | Simple discrete action space. All the analog actions have 3 bins by default: -1, 0 and 1. 8 | """ 9 | 10 | def __init__(self, n_bins=3): 11 | assert n_bins % 2 == 1, "n_bins must be an odd number" 12 | self._n_bins = n_bins 13 | 14 | def get_action_space(self): 15 | raise NotImplementedError("We don't implement get_action_space to remove the gym dependency") 16 | 17 | def parse_actions(self, actions: np.ndarray, state: GameState) -> np.ndarray: 18 | actions = actions.reshape((-1, 8)).astype(dtype=np.float32) 19 | 20 | # map all binned actions from {0, 1, 2 .. n_bins - 1} to {-1 .. 1}. 21 | actions[..., :5] = actions[..., :5] / (self._n_bins // 2) - 1 22 | 23 | return actions 24 | 25 | -------------------------------------------------------------------------------- /src/agent.py: -------------------------------------------------------------------------------- 1 | # import os 2 | 3 | 4 | class Agent: 5 | def __init__(self): 6 | # If you need to load your model from a file this is the time to do it 7 | # You can do something like: 8 | # 9 | # self.actor = # your Model 10 | # 11 | # cur_dir = os.path.dirname(os.path.realpath(__file__)) 12 | # with open(os.path.join(cur_dir, 'model.p'), 'rb') as file: 13 | # model = pickle.load(file) 14 | # self.actor.load_state_dict(model) 15 | pass 16 | 17 | def act(self, state): 18 | # Evaluate your model here 19 | action = [1, 0, 0, 0, 0, 0, 0, 0] 20 | return action 21 | -------------------------------------------------------------------------------- /src/appearance.cfg: -------------------------------------------------------------------------------- 1 | # You don't have to manually edit this file! 2 | # RLBotGUI has an appearance editor with a nice colorpicker, database of items and more! 3 | # To open it up, simply click the (i) icon next to your bot's name and then click Edit Appearance 4 | 5 | [Bot Loadout] 6 | team_color_id = 60 7 | custom_color_id = 0 8 | car_id = 23 9 | decal_id = 0 10 | wheels_id = 1565 11 | boost_id = 35 12 | antenna_id = 0 13 | hat_id = 0 14 | paint_finish_id = 1681 15 | custom_finish_id = 1681 16 | engine_audio_id = 0 17 | trails_id = 3220 18 | goal_explosion_id = 3018 19 | 20 | [Bot Loadout Orange] 21 | team_color_id = 3 22 | custom_color_id = 0 23 | car_id = 23 24 | decal_id = 0 25 | wheels_id = 1565 26 | boost_id = 35 27 | antenna_id = 0 28 | hat_id = 0 29 | paint_finish_id = 1681 30 | custom_finish_id = 1681 31 | engine_audio_id = 0 32 | trails_id = 3220 33 | goal_explosion_id = 3018 34 | 35 | [Bot Paint Blue] 36 | car_paint_id = 12 37 | decal_paint_id = 0 38 | wheels_paint_id = 7 39 | boost_paint_id = 7 40 | antenna_paint_id = 0 41 | hat_paint_id = 0 42 | trails_paint_id = 2 43 | goal_explosion_paint_id = 0 44 | 45 | [Bot Paint Orange] 46 | car_paint_id = 12 47 | decal_paint_id = 0 48 | wheels_paint_id = 14 49 | boost_paint_id = 14 50 | antenna_paint_id = 0 51 | hat_paint_id = 0 52 | trails_paint_id = 14 53 | goal_explosion_paint_id = 0 54 | -------------------------------------------------------------------------------- /src/bot.cfg: -------------------------------------------------------------------------------- 1 | [Locations] 2 | # Path to loadout config. Can use relative path from here. 3 | looks_config = ./appearance.cfg 4 | 5 | # Path to python file. Can use relative path from here. 6 | python_file = ./bot.py 7 | requirements_file = ./requirements.txt 8 | 9 | # Name of the bot in-game 10 | name = RLGymExampleBot 11 | 12 | # The maximum number of ticks per second that your bot wishes to receive. 13 | maximum_tick_rate_preference = 120 14 | 15 | [Details] 16 | # These values are optional but useful metadata for helper programs 17 | # Name of the bot's creator/developer 18 | developer = The RLBot community 19 | 20 | # Short description of the bot 21 | description = This is a multi-line description 22 | of the official rlgym example bot 23 | 24 | # Fun fact about the bot 25 | fun_fact = 26 | 27 | # Link to github repository 28 | github = https://github.com/RLGym/RLGymExampleBot 29 | 30 | # Programming language 31 | language = rlgym 32 | -------------------------------------------------------------------------------- /src/bot.py: -------------------------------------------------------------------------------- 1 | from rlbot.agents.base_agent import BaseAgent, SimpleControllerState 2 | from rlbot.utils.structures.game_data_struct import GameTickPacket 3 | 4 | import numpy as np 5 | 6 | from action.default_act import DefaultAction 7 | from agent import Agent 8 | from obs.default_obs import DefaultObs 9 | from rlgym_compat import GameState 10 | 11 | 12 | class RLGymExampleBot(BaseAgent): 13 | def __init__(self, name, team, index): 14 | super().__init__(name, team, index) 15 | 16 | # FIXME Hey, botmaker. Start here: 17 | # Swap the obs builder if you are using a different one, RLGym's AdvancedObs is also available 18 | self.obs_builder = DefaultObs() 19 | # Swap the action parser if you are using a different one, RLGym's Discrete and Continuous are also available 20 | self.act_parser = DefaultAction() 21 | # Your neural network logic goes inside the Agent class, go take a look inside src/agent.py 22 | self.agent = Agent() 23 | # Adjust the tickskip if your agent was trained with a different value 24 | self.tick_skip = 8 25 | 26 | self.game_state: GameState = None 27 | self.controls = None 28 | self.action = None 29 | self.update_action = True 30 | self.ticks = 0 31 | self.prev_time = 0 32 | print('RLGymExampleBot Ready - Index:', index) 33 | 34 | def initialize_agent(self): 35 | # Initialize the rlgym GameState object now that the game is active and the info is available 36 | self.game_state = GameState(self.get_field_info()) 37 | self.ticks = self.tick_skip # So we take an action the first tick 38 | self.prev_time = 0 39 | self.controls = SimpleControllerState() 40 | self.action = np.zeros(8) 41 | self.update_action = True 42 | 43 | def get_output(self, packet: GameTickPacket) -> SimpleControllerState: 44 | cur_time = packet.game_info.seconds_elapsed 45 | delta = cur_time - self.prev_time 46 | self.prev_time = cur_time 47 | 48 | ticks_elapsed = round(delta * 120) 49 | self.ticks += ticks_elapsed 50 | self.game_state.decode(packet, ticks_elapsed) 51 | 52 | if self.update_action: 53 | self.update_action = False 54 | 55 | # FIXME Hey, botmaker. Verify that this is what you need for your agent 56 | # By default we treat every match as a 1v1 against a fixed opponent, 57 | # by doing this your bot can participate in 2v2 or 3v3 matches. Feel free to change this 58 | player = self.game_state.players[self.index] 59 | teammates = [p for p in self.game_state.players if p.team_num == self.team] 60 | opponents = [p for p in self.game_state.players if p.team_num != self.team] 61 | 62 | if len(opponents) == 0: 63 | # There's no opponent, we assume this model is 1v0 64 | self.game_state.players = [player] 65 | else: 66 | # Sort by distance to ball 67 | teammates.sort(key=lambda p: np.linalg.norm(self.game_state.ball.position - p.car_data.position)) 68 | opponents.sort(key=lambda p: np.linalg.norm(self.game_state.ball.position - p.car_data.position)) 69 | 70 | # Grab opponent in same "position" relative to it's teammates 71 | opponent = opponents[min(teammates.index(player), len(opponents) - 1)] 72 | 73 | self.game_state.players = [player, opponent] 74 | 75 | obs = self.obs_builder.build_obs(player, self.game_state, self.action) 76 | self.action = self.act_parser.parse_actions(self.agent.act(obs), self.game_state)[0] # Dim is (N, 8) 77 | 78 | if self.ticks >= self.tick_skip - 1: 79 | self.update_controls(self.action) 80 | 81 | if self.ticks >= self.tick_skip: 82 | self.ticks = 0 83 | self.update_action = True 84 | 85 | return self.controls 86 | 87 | def update_controls(self, action): 88 | self.controls.throttle = action[0] 89 | self.controls.steer = action[1] 90 | self.controls.pitch = action[2] 91 | self.controls.yaw = 0 if action[5] > 0 else action[3] 92 | self.controls.roll = action[4] 93 | self.controls.jump = action[5] > 0 94 | self.controls.boost = action[6] > 0 95 | self.controls.handbrake = action[7] > 0 96 | -------------------------------------------------------------------------------- /src/obs/advanced_obs.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy as np 3 | from typing import Any, List 4 | from rlgym_compat import common_values 5 | from rlgym_compat import PlayerData, GameState, PhysicsObject 6 | 7 | 8 | class AdvancedObs: 9 | POS_STD = 2300 10 | ANG_STD = math.pi 11 | 12 | def __init__(self): 13 | super().__init__() 14 | 15 | def reset(self, initial_state: GameState): 16 | pass 17 | 18 | def build_obs(self, player: PlayerData, state: GameState, previous_action: np.ndarray) -> Any: 19 | 20 | if player.team_num == common_values.ORANGE_TEAM: 21 | inverted = True 22 | ball = state.inverted_ball 23 | pads = state.inverted_boost_pads 24 | else: 25 | inverted = False 26 | ball = state.ball 27 | pads = state.boost_pads 28 | 29 | obs = [ball.position / self.POS_STD, 30 | ball.linear_velocity / self.POS_STD, 31 | ball.angular_velocity / self.ANG_STD, 32 | previous_action, 33 | pads] 34 | 35 | player_car = self._add_player_to_obs(obs, player, ball, inverted) 36 | 37 | allies = [] 38 | enemies = [] 39 | 40 | for other in state.players: 41 | if other.car_id == player.car_id: 42 | continue 43 | 44 | if other.team_num == player.team_num: 45 | team_obs = allies 46 | else: 47 | team_obs = enemies 48 | 49 | other_car = self._add_player_to_obs(team_obs, other, ball, inverted) 50 | 51 | # Extra info 52 | team_obs.extend([ 53 | (other_car.position - player_car.position) / self.POS_STD, 54 | (other_car.linear_velocity - player_car.linear_velocity) / self.POS_STD 55 | ]) 56 | 57 | obs.extend(allies) 58 | obs.extend(enemies) 59 | return np.concatenate(obs) 60 | 61 | def _add_player_to_obs(self, obs: List, player: PlayerData, ball: PhysicsObject, inverted: bool): 62 | if inverted: 63 | player_car = player.inverted_car_data 64 | else: 65 | player_car = player.car_data 66 | 67 | rel_pos = ball.position - player_car.position 68 | rel_vel = ball.linear_velocity - player_car.linear_velocity 69 | 70 | obs.extend([ 71 | rel_pos / self.POS_STD, 72 | rel_vel / self.POS_STD, 73 | player_car.position / self.POS_STD, 74 | player_car.forward(), 75 | player_car.up(), 76 | player_car.linear_velocity / self.POS_STD, 77 | player_car.angular_velocity / self.ANG_STD, 78 | [player.boost_amount, 79 | int(player.on_ground), 80 | int(player.has_flip), 81 | int(player.is_demoed)]]) 82 | 83 | return player_car 84 | -------------------------------------------------------------------------------- /src/obs/default_obs.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy as np 3 | from typing import Any, List 4 | from rlgym_compat import common_values 5 | from rlgym_compat import PlayerData, GameState 6 | 7 | 8 | class DefaultObs: 9 | def __init__(self, pos_coef=1/2300, ang_coef=1/math.pi, lin_vel_coef=1/2300, ang_vel_coef=1/math.pi): 10 | """ 11 | :param pos_coef: Position normalization coefficient 12 | :param ang_coef: Rotation angle normalization coefficient 13 | :param lin_vel_coef: Linear velocity normalization coefficient 14 | :param ang_vel_coef: Angular velocity normalization coefficient 15 | """ 16 | super().__init__() 17 | self.POS_COEF = pos_coef 18 | self.ANG_COEF = ang_coef 19 | self.LIN_VEL_COEF = lin_vel_coef 20 | self.ANG_VEL_COEF = ang_vel_coef 21 | 22 | def reset(self, initial_state: GameState): 23 | pass 24 | 25 | def build_obs(self, player: PlayerData, state: GameState, previous_action: np.ndarray) -> Any: 26 | if player.team_num == common_values.ORANGE_TEAM: 27 | inverted = True 28 | ball = state.inverted_ball 29 | pads = state.inverted_boost_pads 30 | else: 31 | inverted = False 32 | ball = state.ball 33 | pads = state.boost_pads 34 | 35 | obs = [ball.position * self.POS_COEF, 36 | ball.linear_velocity * self.LIN_VEL_COEF, 37 | ball.angular_velocity * self.ANG_VEL_COEF, 38 | previous_action, 39 | pads] 40 | 41 | self._add_player_to_obs(obs, player, inverted) 42 | 43 | allies = [] 44 | enemies = [] 45 | 46 | for other in state.players: 47 | if other.car_id == player.car_id: 48 | continue 49 | 50 | if other.team_num == player.team_num: 51 | team_obs = allies 52 | else: 53 | team_obs = enemies 54 | 55 | self._add_player_to_obs(team_obs, other, inverted) 56 | 57 | obs.extend(allies) 58 | obs.extend(enemies) 59 | return np.concatenate(obs) 60 | 61 | def _add_player_to_obs(self, obs: List, player: PlayerData, inverted: bool): 62 | if inverted: 63 | player_car = player.inverted_car_data 64 | else: 65 | player_car = player.car_data 66 | 67 | obs.extend([ 68 | player_car.position * self.POS_COEF, 69 | player_car.forward(), 70 | player_car.up(), 71 | player_car.linear_velocity * self.LIN_VEL_COEF, 72 | player_car.angular_velocity * self.ANG_VEL_COEF, 73 | [player.boost_amount, 74 | int(player.on_ground), 75 | int(player.has_flip), 76 | int(player.is_demoed)]]) 77 | 78 | return player_car 79 | -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- 1 | # Include everything the framework requires 2 | # You will automatically get updates for all versions starting with "1.". 3 | rlbot==1.* 4 | --find-links https://download.pytorch.org/whl/torch_stable.html 5 | torch==1.9.1+cpu 6 | rlgym-compat>=1.1.0 7 | numpy 8 | 9 | # This will cause pip to auto-upgrade and stop scaring people with warning messages 10 | pip 11 | --------------------------------------------------------------------------------