-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
13 changed files
with
985 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# 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 | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
[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 = ./bot.py | ||
|
||
# Name of the bot in-game | ||
name = Noob Black | ||
|
||
# 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 | ||
developer = Scumclass | ||
|
||
# Short description of the bot | ||
description = Noob Bot V3, made with code from the black market. | ||
As always, mainly designed for 3s. | ||
|
||
# Fun fact about the bot | ||
fun_fact = The bot has a Roblox background. | ||
|
||
# Tags | ||
tags = teamplay | ||
|
||
# Link to github repository | ||
github = https://github.com/RLBot/RLBotPythonExample | ||
|
||
# Programming language | ||
language = python |
Large diffs are not rendered by default.
Oops, something went wrong.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Oops, something went wrong.