Skip to content

Commit 9bd614c

Browse files
committed
Initial commit & port to v5
0 parents  commit 9bd614c

20 files changed

+859
-0
lines changed

.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
PYTHONPATH=src

.gitignore

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024
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.

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# RLBotPythonExample
2+
3+
Example of a Python bot using the RLBot framework
4+
5+
## Quick Start
6+
7+
- Install Python 3.10 or later
8+
- Create a Python virtual environment
9+
- `python -m venv venv`
10+
- Install the required packages
11+
- `pip install -r requirements.txt`
12+
- Download `RLBotServer.exe` and place it in the root directory
13+
- <https://github.com/RLBot/core>
14+
- Modify `rlbot.toml` to your liking
15+
- Note: `dev.toml` also exists with a few changed settings that might be useful for development
16+
- Start a match with `python run.py`
17+
18+
## Changing the bot
19+
20+
- Bot behavior is controlled by `src/bot.py`
21+
- Bot appearance is controlled by `src/looks.toml`

dev.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[rlbot]
2+
launcher = "steam"
3+
# We'll run the bot ourself so we don't have restart the match
4+
# `python src/bot.py` in our case
5+
auto_start_bots = false
6+
7+
[match]
8+
# There can only be one rlbot-controlled car in a match
9+
# unless you feed the bot it's spawn id
10+
# by setting the `BOT_SPAWN_ID` environment variable
11+
num_cars = 1
12+
enable_rendering = true
13+
14+
[mutators]
15+
match_length = "Unlimited"
16+
17+
[[cars]]
18+
config = "src/bot.toml"
19+
team = 0

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# rlbot==5.*
2+
rlbot @ git+https://github.com/VirxEC/python-interface@master

rlbot.toml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
[rlbot]
2+
# rocket_league_exe_path = "epicgames/path/to/rocketleague.exe"
3+
# "Steam", "Epic"
4+
launcher = "steam"
5+
# Should RLBot start the bot processes automatically, or will a separate script start them
6+
auto_start_bots = true
7+
8+
[match]
9+
# Number of bots/players which will be spawned.
10+
num_cars = 2
11+
# Number of scripts which will be spawned.
12+
num_scripts = 0
13+
# What game mode the game should load.
14+
# Accepted values are "Soccer", "Hoops", "Dropshot", "Hockey", "Rumble", "Heatseeker", "Gridiron"
15+
game_mode = "Soccer"
16+
# Which map the game should load into
17+
game_map_upk = "Stadium_P"
18+
# Automatically skip replays after a goal. Also stops match replays from being saved.
19+
skip_replays = false
20+
# Skip the kickoff countdown
21+
start_without_countdown = false
22+
# What should we do if you click run while a match is already in progress?
23+
# "Restart_If_Different", "Restart", "Continue_And_Spawn"
24+
existing_match_behavior = "Restart"
25+
enable_rendering = true
26+
enable_state_setting = true
27+
auto_save_replay = false
28+
# Whether or not to use freeplay instead of an exhibition match
29+
freeplay = false
30+
31+
[mutators]
32+
# "Five_Minutes", "Ten_Minutes", "Twenty_Minutes", "Unlimited"
33+
match_length = "Five_Minutes"
34+
# "Default", "One_Goal", "Three_Goals", "Five_Goals", "Seven Goals", "Unlimited"
35+
max_score = "Default"
36+
# "One", "Two", "Four", "Six"
37+
multi_ball = "One"
38+
# "Unlimited", "Five_Max_First_Score", "Five_Max_Random_Team"
39+
overtime = "Unlimited"
40+
# "Default", "Slo_Mo", "Time_Warp"
41+
game_speed = "Default"
42+
# "Default", "Slow", "Fast", "Super_Fast"
43+
ball_max_speed = "Default"
44+
# "Default", "Cube", "Puck", "Basketball", "Beachball", "Anniversary", "Haunted"
45+
ball_type = "Default"
46+
# "Default", "Light", "Heavy", "Super_Light", "Curve_Ball", "Beach_Ball_Curve"
47+
ball_weight = "Default"
48+
# "Default", "Small", "Large", "Gigantic"
49+
ball_size = "Default"
50+
# "Default", "Low", "High", "Super_High"
51+
ball_bounciness = "Default"
52+
# "Normal_Boost", "Unlimited_Boost", "Slow_Recharge", "Rapid_Recharge", "No_Boost"
53+
boost_amount = "Normal_Boost"
54+
# "No_Rumble", "Default", "Slow", "Civilized", "Destruction_Derby", "Spring_Loaded", "Spikes_Only", "Spike_Rush", "Haunted_Ball_Beam", "Tactical"
55+
rumble = "No_Rumble"
56+
# "One", "OneAndAHalf", "Two", "Ten"
57+
boost_strength = "One"
58+
# "Default", "Low", "High", "Super_High", "Reverse"
59+
gravity = "Default"
60+
# "Default", "Disabled", "Friendly_Fire", "On_Contact", "On_Contact_FF"
61+
demolish = "Default"
62+
# "Three_Seconds", "Two_Seconds", "One_Seconds", "Disable_Goal_Reset"
63+
respawn_time = "Three_Seconds"
64+
65+
[[cars]]
66+
type = "human"
67+
team = 0
68+
69+
[[cars]]
70+
config = "src/bot.toml"
71+
team = 0
72+

run.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from pathlib import Path
2+
3+
from rlbot.managers import MatchManager
4+
5+
MATCH_CONFIG_FILE = "rlbot.toml"
6+
7+
if __name__ == "__main__":
8+
root_dir = Path(__file__).parent
9+
10+
match_manager = MatchManager(root_dir)
11+
match_manager.start_match(root_dir / MATCH_CONFIG_FILE, False)
12+
match_manager.disconnect()

src/__init__.py

Whitespace-only changes.

src/bot.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
from rlbot.flat import ControllerState, GameTickPacket
2+
from rlbot.managers import Bot
3+
from rlbot_flatbuffers import BallAnchor
4+
5+
from util.ball_prediction_analysis import find_slice_at_time
6+
from util.boost_pad_tracker import BoostPadTracker
7+
from util.drive import steer_toward_target
8+
from util.sequence import ControlStep, Sequence
9+
from util.vec import Vec3
10+
11+
12+
class MyBot(Bot):
13+
active_sequence: Sequence | None = None
14+
boost_pad_tracker = BoostPadTracker()
15+
16+
def initialize_agent(self):
17+
# Set up information about the boost pads now that the game is active and the info is available
18+
self.boost_pad_tracker.initialize_boosts(self.field_info)
19+
20+
def get_output(self, packet: GameTickPacket) -> ControllerState:
21+
"""
22+
This function will be called by the framework many times per second. This is where you can
23+
see the motion of the ball, etc. and return controls to drive your car.
24+
"""
25+
26+
# Keep our boost pad info updated with which pads are currently active
27+
self.boost_pad_tracker.update_boost_status(packet)
28+
29+
if len(packet.balls) == 0:
30+
# If there are no balls current in the game (likely due to being in a replay), skip this tick.
31+
return ControllerState()
32+
# we can now assume there's at least one ball in the match
33+
34+
# This is good to keep at the beginning of get_output. It will allow you to continue
35+
# any sequences that you may have started during a previous call to get_output.
36+
if self.active_sequence is not None and not self.active_sequence.done:
37+
controls = self.active_sequence.tick(packet)
38+
if controls is not None:
39+
return controls
40+
41+
# Gather some information about our car and the ball
42+
my_car = packet.players[self.index]
43+
car_location = Vec3(my_car.physics.location)
44+
car_velocity = Vec3(my_car.physics.velocity)
45+
ball_location = Vec3(packet.balls[0].physics.location)
46+
47+
# By default we will chase the ball, but target_location can be changed later
48+
target_location = ball_location
49+
50+
if car_location.dist(ball_location) > 1500:
51+
# We're far away from the ball, let's try to lead it a little bit
52+
# self.ball_prediction can predict bounces, etc
53+
ball_in_future = find_slice_at_time(
54+
self.ball_prediction, packet.game_info.seconds_elapsed + 2
55+
)
56+
57+
# ball_in_future might be None if we don't have an adequate ball prediction right now, like during
58+
# replays, so check it to avoid errors.
59+
if ball_in_future is not None:
60+
target_location = Vec3(ball_in_future.physics.location)
61+
62+
# BallAnchor(0) will dynamically start the point at the ball's current location
63+
# 0 makes it reference the ball at index 0 in the packet.balls list
64+
self.renderer.draw_line_3d(
65+
BallAnchor(0), target_location, self.renderer.cyan
66+
)
67+
68+
# Draw some things to help understand what the bot is thinking
69+
self.renderer.draw_line_3d(car_location, target_location, self.renderer.white)
70+
self.renderer.draw_string_3d(
71+
f"Speed: {car_velocity.length():.1f}",
72+
car_location,
73+
1,
74+
self.renderer.white,
75+
)
76+
self.renderer.draw_line_3d(
77+
target_location - Vec3(0, 0, 50),
78+
target_location + Vec3(0, 0, 50),
79+
self.renderer.cyan,
80+
)
81+
82+
if 750 < car_velocity.length() < 800:
83+
# We'll do a front flip if the car is moving at a certain speed.
84+
return self.begin_front_flip(packet) # type: ignore
85+
86+
controls = ControllerState()
87+
controls.steer = steer_toward_target(my_car, target_location)
88+
controls.throttle = 1.0
89+
# You can set more controls if you want, like controls.boost.
90+
91+
return controls
92+
93+
def begin_front_flip(self, packet: GameTickPacket):
94+
# Send some quickchat just for fun
95+
# There won't be any content of the message for other bots,
96+
# but "I got it!" will be display for a human to see!
97+
self.send_match_comm(b"", "I got it!")
98+
99+
# Do a front flip. We will be committed to this for a few seconds and the bot will ignore other
100+
# logic during that time because we are setting the active_sequence.
101+
self.active_sequence = Sequence(
102+
[
103+
ControlStep(duration=0.05, controls=ControllerState(jump=True)),
104+
ControlStep(duration=0.05, controls=ControllerState(jump=False)),
105+
ControlStep(
106+
duration=0.2, controls=ControllerState(jump=True, pitch=-1)
107+
),
108+
ControlStep(duration=0.8, controls=ControllerState()),
109+
]
110+
)
111+
112+
# Return the controls associated with the beginning of the sequence so we can start right away.
113+
return self.active_sequence.tick(packet)
114+
115+
116+
if __name__ == "__main__":
117+
# Connect to RLBot and run
118+
MyBot().run()

0 commit comments

Comments
 (0)