-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changed ball touches ground detection #9
base: master
Are you sure you want to change the base?
Changes from 7 commits
fc769b4
db8fc50
2935385
196ff5a
1407a1c
66827a8
b362e79
6cc8798
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
This module contains graders which mimic the the behaviour of Rocket League custom training. | ||
""" | ||
|
||
import math | ||
|
||
from dataclasses import dataclass | ||
from typing import Optional, Mapping, Union | ||
|
@@ -15,27 +16,123 @@ | |
from rlbottraining.common_graders.compound_grader import CompoundGrader | ||
from rlbottraining.common_graders.timeout import FailOnTimeout, PassOnTimeout | ||
from rlbottraining.common_graders.goal_grader import PassOnGoalForAllyTeam | ||
from rlbot.training.training import Pass, Fail, Grade | ||
|
||
|
||
class RocketLeagueStrikerGrader(CompoundGrader): | ||
""" | ||
A Grader which aims to match the striker training. | ||
""" | ||
|
||
def __init__(self, timeout_seconds=4.0, ally_team=0): | ||
def __init__(self, timeout_seconds=4.0, ally_team=0, timeout_override=False, ground_override=False): | ||
self.timeout_override = timeout_override | ||
self.ground_override = ground_override | ||
super().__init__([ | ||
PassOnGoalForAllyTeam(ally_team), | ||
FailOnBallOnGroundAfterTimeout(timeout_seconds), | ||
FailOnBallOnGround(), | ||
FailOnTimeout(timeout_seconds), | ||
]) | ||
|
||
class FailOnBallOnGroundAfterTimeout(FailOnTimeout): | ||
def on_tick(self, tick: TrainingTickPacket) -> Optional[Grade]: | ||
grades = [grader.on_tick(tick) for grader in self.graders] | ||
return self.grade_chooser(grades) | ||
|
||
def grade_chooser(self, grades) -> Optional[Grade]: | ||
""" | ||
Chooses the importance of the grades | ||
""" | ||
|
||
timeout = isinstance(grades[2], Fail) # True if timed out, false otherwise | ||
ball_on_ground = isinstance(grades[1], Fail) # True if ball touched the ground, false otherwise | ||
goal = isinstance(grades[0], Pass) # True if ball there was a goal, false otherwise | ||
|
||
if goal: # scoring and touching the ground on the same tick prefer scoring | ||
return grades[0] | ||
elif timeout: | ||
if self.timeout_override: | ||
return grades[2] | ||
elif ball_on_ground: | ||
return grades[1] | ||
elif self.ground_override and ball_on_ground: | ||
return grades[1] | ||
return None | ||
|
||
|
||
class FailOnBallOnGround(Grader): | ||
def __init__(self): | ||
self.previous_ang_x = None | ||
self.previous_ang_y = None | ||
self.previous_ang_z = None | ||
self.previous_total_goals = None | ||
|
||
class FailDueToGroundHit(Fail): | ||
def __init__(self): | ||
pass | ||
|
||
def __repr__(self): | ||
return f'{super().__repr__()}: Ball hit the ground' | ||
|
||
|
||
def set_previous_angular_velocity(self, ball, reset = False): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably better to store / copy a ball object rather than separating out the variables. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did that at first, but it passed by reference, so it would have the current values and not the previous ones There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that's why we need to copy the object not just create another reference to it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, thank you :) |
||
if not reset: | ||
self.previous_ang_x = ball.angular_velocity.x | ||
self.previous_ang_y = ball.angular_velocity.y | ||
self.previous_ang_z = ball.angular_velocity.z | ||
self.previous_vel_z = ball.velocity.z | ||
else: | ||
self.previous_ang_x = None | ||
self.previous_ang_y = None | ||
self.previous_ang_z = None | ||
self.previous_vel_z = None | ||
|
||
def set_previous_total_goals(self, total_goals, reset=False): | ||
if not reset: | ||
self.previous_total_goals = total_goals | ||
else: | ||
self.previous_total_goals = None | ||
|
||
def current_total_goals(self, packet): | ||
total_goals = 0 | ||
for car_id in range(packet.num_cars): | ||
goal = packet.game_cars[car_id].score_info.goals | ||
own_goal = packet.game_cars[car_id].score_info.own_goals | ||
total_goals += goal + own_goal | ||
return total_goals | ||
|
||
def on_tick(self, tick: TrainingTickPacket) -> Optional[Grade]: | ||
grade = super().on_tick(tick) | ||
if grade is None: | ||
return None | ||
assert isinstance(grade, FailOnTimeout.FailDueToTimeout) | ||
ball = tick.game_tick_packet.game_ball.physics | ||
if ball.location.z < 100 and ball.velocity.z >= 0: | ||
return grade | ||
hit_ground = False | ||
|
||
if self.previous_ang_z is None: | ||
self.set_previous_angular_velocity(ball) | ||
else: | ||
max_ang_vel = 5.9999601985025075 #Max angular velocity possible | ||
previous_ang_norm = math.sqrt(self.previous_ang_x**2 + self.previous_ang_y**2 + self.previous_ang_z**2) | ||
if ball.location.z <= 1900: #Making sure it doesnt count the ceiling | ||
if (ball.angular_velocity.x != self.previous_ang_x or ball.angular_velocity.y != self.previous_ang_y): | ||
# If the ball hit anything its angular velocity will change in the x or y axis | ||
if self.previous_ang_z == ball.angular_velocity.z and not ( 130 < ball.location.z < 150) : | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is true that it could happens at another z, I did not like that way as well. After sleeping on the case I have some new ideas to try out, like seeing if the last touch was on this tick, and if the ball is a certain couple of units above one of the cars There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that'll make it better but you should keep some of the tricky cases in mind: potential false positives:
potential false negatives:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. potential false positives:
punching doesn't update the last touch. this may become a problem, but currently we cant test it potential false negatives:
Once again, the last touch check should not allow this to happen There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 I didn't think of using the last touch info. |
||
# if the z angular velocity did not change it means it was a flat plane. | ||
#ignore dribbling | ||
hit_ground = True | ||
elif previous_ang_norm == max_ang_vel: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd recommend not doing exact equality on floating point numbers. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tested various times and the max was always like that. I would think that it could have some error from time to time, but all the tested times had exactly that value. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would like some help with how should I add unit testing, I'm not familiar with how you are doing that, neither how that is done on general There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://github.com/RLBot/RLBotPythonExample/blob/master/training/unit_tests.py Tests go here: https://github.com/RLBot/RLBotTraining/tree/master/tests To test these kinds of cases, create exercises within the test file itself which use this grader. e.g.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, thank you, this will also make my testing easier :) |
||
# if it was at maximum angular velocity, it may have changed z axis not to exceed max | ||
# fallback on old behaviour | ||
if ball.location.z < 100 and ball.velocity.z >= 0: | ||
hit_ground = True | ||
if ball.location.z <= 93.5 and ball.velocity.z >= 0: | ||
# if the car is pushing the ball on the ground | ||
hit_ground = True | ||
if math.sqrt(ball.velocity.x**2 + ball.velocity.y**2 + ball.velocity.z**2) == 0 and self.previous_vel_z != 0: | ||
# ball is stop on ground and not on its apex, which means it should fail anyway | ||
if self.previous_total_goals != self.current_total_goals(tick.game_tick_packet): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think goal logic should be in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will look into that, I agree with you. |
||
# There was a goal, let the goal handler handle it | ||
hit_ground = False | ||
else: | ||
hit_ground = True | ||
|
||
self.set_previous_angular_velocity(ball) | ||
self.set_previous_total_goals(self.current_total_goals(tick.game_tick_packet)) | ||
if hit_ground: | ||
self.set_previous_angular_velocity(ball, reset = True) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This reset seems unnecessary to me. How come it's here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reset is there so that in the next shot, after you set the state, it will not fail because the velocity changed. From what you said I'm guessing a new shot will initializate a new instance of a grader so that will never happen. I did not test for that. I will delete the reset and test There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, exercises are created and used once such that the graders are automatically and fully reset. |
||
return self.FailDueToGroundHit() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
[Locations] | ||
# Path to loadout config from runner | ||
looks_config = ./prop_bot_looks.cfg | ||
|
||
# Bot's python file. | ||
# Only need this if RLBot controlled | ||
python_file = prop_bot.py | ||
|
||
# The name that will be displayed in game | ||
name = Prop Bot |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
from rlbot.agents.base_agent import BaseAgent, SimpleControllerState | ||
from rlbot.utils.structures.game_data_struct import GameTickPacket | ||
|
||
class PropBot(BaseAgent): | ||
""" | ||
A bot which just sits there like a prop. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BrickBot already does this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Brick bot has handbrake on, this causes the ball to move it quite a bit when it hits it. |
||
""" | ||
|
||
def get_output(self, game_tick_packet: GameTickPacket) -> SimpleControllerState: | ||
seconds = game_tick_packet.game_info.seconds_elapsed | ||
controller_state = SimpleControllerState() | ||
controller_state.steer = 0 | ||
controller_state.handbrake = 0 | ||
return controller_state |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
[Bot Loadout] | ||
# Primary Color selection | ||
team_color_id = 11 | ||
# Secondary Color selection | ||
custom_color_id = 74 | ||
# Car type (Octane, Merc, etc | ||
car_id = 23 | ||
# Type of decal | ||
decal_id = 1618 | ||
# Wheel selection | ||
wheels_id = 1656 | ||
# Boost selection | ||
boost_id = 0 | ||
# Antenna Selection | ||
antenna_id = 0 | ||
# Hat Selection | ||
hat_id = 0 | ||
# Paint Type (for first color) | ||
paint_finish_id = 1978 | ||
# Paint Type (for secondary color) | ||
custom_finish_id = 1978 | ||
# Engine Audio Selection | ||
engine_audio_id = 1786 | ||
# Car trail Selection | ||
trails_id = 1898 | ||
# Goal Explosion Selection | ||
goal_explosion_id = 1971 | ||
|
||
[Bot Loadout Orange] | ||
# Primary Color selection | ||
team_color_id = 11 | ||
# Secondary Color selection | ||
custom_color_id = 74 | ||
# Car type (Octane, Merc, etc | ||
car_id = 23 | ||
# Type of decal | ||
decal_id = 1618 | ||
# Wheel selection | ||
wheels_id = 1656 | ||
# Boost selection | ||
boost_id = 0 | ||
# Antenna Selection | ||
antenna_id = 0 | ||
# Hat Selection | ||
hat_id = 0 | ||
# Paint Type (for first color) | ||
paint_finish_id = 1978 | ||
# Paint Type (for secondary color) | ||
custom_finish_id = 1978 | ||
# Engine Audio Selection | ||
engine_audio_id = 1786 | ||
# Car trail Selection | ||
trails_id = 1898 | ||
# Goal Explosion Selection | ||
goal_explosion_id = 1971 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
from dataclasses import dataclass, field | ||
from math import pi | ||
|
||
from rlbot.utils.game_state_util import GameState, BallState, CarState, Physics, Vector3, Rotator | ||
|
||
from rlbot.utils.game_state_util import GameState, BoostState, BallState, CarState, Physics, Vector3, Rotator | ||
|
||
from rlbottraining.common_exercises.rl_custom_training_import.rl_importer import RocketLeagueCustomStrikerTraining | ||
from rlbottraining.common_graders.rl_graders import RocketLeagueStrikerGrader | ||
from rlbottraining.rng import SeededRandomNumberGenerator | ||
from rlbottraining.training_exercise import Playlist | ||
from rlbottraining.paths import BotConfigs | ||
from rlbot.matchconfig.match_config import MatchConfig, PlayerConfig, Team | ||
from rlbottraining.grading.grader import Grader | ||
from rlbottraining.match_configs import make_default_match_config | ||
|
||
test_match_config = make_default_match_config() | ||
|
||
@dataclass | ||
class SimpleFallFromPerfectStill(RocketLeagueCustomStrikerTraining): | ||
|
||
"""Ball starts perfectly still""" | ||
|
||
grader: Grader = RocketLeagueStrikerGrader(timeout_seconds=4, timeout_override=True, ground_override=True) | ||
test_match_config.player_configs = [PlayerConfig.bot_config(BotConfigs.prop_bot, Team.BLUE), ] | ||
test_match_config.game_map = "ThrowbackStadium" | ||
match_config: MatchConfig = test_match_config | ||
|
||
def make_game_state(self, rng: SeededRandomNumberGenerator) -> GameState: | ||
car_pos = Vector3(5000, 0, 0) | ||
ball_pos = Vector3(0, 0, 1900) | ||
ball_vel = Vector3(0, 0, 0) | ||
ball_ang_vel = Vector3(0, 0, 0) | ||
|
||
ball_state = BallState(Physics(location=ball_pos, velocity=ball_vel, angular_velocity=ball_ang_vel)) | ||
car_state = CarState(boost_amount=100, jumped=False, double_jumped=False, | ||
physics=Physics(location=car_pos, rotation=Rotator(0, 0, 0), velocity=Vector3(0, 0, 0), | ||
angular_velocity=Vector3(0, 0, 0))) | ||
enemy_car = CarState(physics=Physics(location=Vector3(10000, 10000, 10000))) | ||
game_state = GameState(ball=ball_state, cars={0: car_state, 1: enemy_car}) | ||
return game_state | ||
|
||
|
||
@dataclass | ||
class SimpleFallFromRotatingStill(RocketLeagueCustomStrikerTraining): | ||
|
||
"""Ball starts only with angular velocity""" | ||
|
||
grader: Grader = RocketLeagueStrikerGrader(timeout_seconds=5, timeout_override=True, ground_override=True) | ||
test_match_config.player_configs = [PlayerConfig.bot_config(BotConfigs.prop_bot, Team.BLUE), ] | ||
test_match_config.game_map = "ThrowbackStadium" | ||
match_config: MatchConfig = test_match_config | ||
|
||
def make_game_state(self, rng: SeededRandomNumberGenerator) -> GameState: | ||
car_pos = Vector3(5000, 0, 0) | ||
ball_pos = Vector3(0, 0, 1900) | ||
ball_vel = Vector3(15, 0, 0) | ||
ball_ang_vel = Vector3(1, 1, 1) | ||
|
||
ball_state = BallState(Physics(location=ball_pos, velocity=ball_vel, angular_velocity= ball_ang_vel)) | ||
car_state = CarState(boost_amount=100, jumped=False, double_jumped=False, | ||
physics=Physics(location=car_pos, rotation=Rotator(0, 0, 0), velocity=Vector3(0, 0, 0), | ||
angular_velocity=Vector3(0, 0, 0))) | ||
enemy_car = CarState(physics=Physics(location=Vector3(10000, 10000, 10000))) | ||
game_state = GameState(ball=ball_state, cars={0: car_state, 1: enemy_car}) | ||
return game_state | ||
|
||
|
||
|
||
def make_default_playlist() -> Playlist: | ||
return [ | ||
SimpleFallFromPerfectStill('Fall From Perfect Still'), | ||
#SimpleFallFromRotatingStill('Fall with rotation'), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
from typing import Iterator, List | ||
import unittest | ||
|
||
from rlbot.training.training import Pass, Fail, FailDueToExerciseException | ||
|
||
from rlbottraining.exercise_runner import run_playlist | ||
from rlbottraining.history.exercise_result import ExerciseResult | ||
|
||
class rl_grader_tester(unittest.TestCase): | ||
''' | ||
This tests the grader that simulates rocket league environments, like the shooter training pack | ||
''' | ||
|
||
def assertGrades(self, result_iter: Iterator[ExerciseResult], want_grades: List[str]): | ||
got_grades = [] | ||
for result in result_iter: | ||
if isinstance(result.grade, FailDueToExerciseException): | ||
self.fail(str(result.grade)) | ||
break | ||
got_grades.append(result.grade.__class__.__name__) | ||
self.assertEqual(got_grades, want_grades) | ||
|
||
def test_rl_graders(self): | ||
from tests.test_exercises.rl_grader_exercises import make_default_playlist | ||
self.assertGrades( | ||
run_playlist(make_default_playlist()), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's easier to understand the test case if the exercises are constructed in this function. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, i will keep working on a separate file until I have all the tests done, then I will move it here. |
||
[ | ||
'FailDueToGroundHit', | ||
#'FailDueToGroundHit', | ||
#'FailDueToTimeout', | ||
] | ||
) | ||
|
||
if __name__ == '__main__': | ||
unittest.main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add documentation about what the overrides do.
Will users of this class care about this configurability?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably users will not care, I cant think of a good use case beyond testing the grader.