From fc769b4abb41a16f8123e36077e8d7d53a445476 Mon Sep 17 00:00:00 2001 From: Skyborg Date: Sun, 14 Apr 2019 23:24:05 +0100 Subject: [PATCH 1/8] Changed ball touches ground detection This is a much improved ball on ground detection that brings it closer to the behaviour of rocket league this allows for touch detection on top of the goal in throwback stadium, and for the ball stop on ground. --- rlbottraining/common_graders/rl_graders.py | 37 ++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/rlbottraining/common_graders/rl_graders.py b/rlbottraining/common_graders/rl_graders.py index 0ff32c9..1656575 100644 --- a/rlbottraining/common_graders/rl_graders.py +++ b/rlbottraining/common_graders/rl_graders.py @@ -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 @@ -29,6 +30,16 @@ def __init__(self, timeout_seconds=4.0, ally_team=0): ]) class FailOnBallOnGroundAfterTimeout(FailOnTimeout): + def __init__(self, max_duration_seconds): + super().__init__(max_duration_seconds) + self.previous_ang_x = None + self.previous_ang_y = None + self.previous_ang_z = None + + def set_previous_angular_velocity(self, ball): + self.previous_ang_x = ball.angular_velocity.x + self.previous_ang_y = ball.angular_velocity.y + self.previous_ang_z = ball.angular_velocity.z def on_tick(self, tick: TrainingTickPacket) -> Optional[Grade]: grade = super().on_tick(tick) @@ -36,6 +47,28 @@ def on_tick(self, tick: TrainingTickPacket) -> Optional[Grade]: 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: + # if the z angular velocity did not change it means it was a flat plane. + hit_ground = True + elif previous_ang_norm == max_ang_vel: + # 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 math.sqrt(self.previous_ang_x**2 + self.previous_ang_y**2 + self.previous_ang_z**2) == 0: + # ball is stop on ground, which means it should fail anyway + hit_ground = True + + self.set_previous_angular_velocity(ball) + if hit_ground: + return grade From db8fc507cff5872e3dbb69f3ac0b95bf72bfc05f Mon Sep 17 00:00:00 2001 From: Skyborg Date: Sun, 14 Apr 2019 23:43:06 +0100 Subject: [PATCH 2/8] Fixed ball stop detection --- rlbottraining/common_graders/rl_graders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rlbottraining/common_graders/rl_graders.py b/rlbottraining/common_graders/rl_graders.py index 1656575..da6d591 100644 --- a/rlbottraining/common_graders/rl_graders.py +++ b/rlbottraining/common_graders/rl_graders.py @@ -65,7 +65,7 @@ def on_tick(self, tick: TrainingTickPacket) -> Optional[Grade]: # fallback on old behaviour if ball.location.z < 100 and ball.velocity.z >= 0: hit_ground = True - if math.sqrt(self.previous_ang_x**2 + self.previous_ang_y**2 + self.previous_ang_z**2) == 0: + if math.sqrt(ball.velocity.x**2 + ball.velocity.y**2 + ball.velocity.x**2) == 0: # ball is stop on ground, which means it should fail anyway hit_ground = True From 2935385c9c23f39be3efeaa09594146205631082 Mon Sep 17 00:00:00 2001 From: Skyborg Date: Sun, 14 Apr 2019 23:43:36 +0100 Subject: [PATCH 3/8] fixed ball stop detection typo --- rlbottraining/common_graders/rl_graders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rlbottraining/common_graders/rl_graders.py b/rlbottraining/common_graders/rl_graders.py index da6d591..6bd9916 100644 --- a/rlbottraining/common_graders/rl_graders.py +++ b/rlbottraining/common_graders/rl_graders.py @@ -65,7 +65,7 @@ def on_tick(self, tick: TrainingTickPacket) -> Optional[Grade]: # fallback on old behaviour if ball.location.z < 100 and ball.velocity.z >= 0: hit_ground = True - if math.sqrt(ball.velocity.x**2 + ball.velocity.y**2 + ball.velocity.x**2) == 0: + if math.sqrt(ball.velocity.x**2 + ball.velocity.y**2 + ball.velocity.z**2) == 0: # ball is stop on ground, which means it should fail anyway hit_ground = True From 196ff5a8fe645ce50aadad248709dcb6b1f9a88e Mon Sep 17 00:00:00 2001 From: Skyborg Date: Mon, 15 Apr 2019 00:03:58 +0100 Subject: [PATCH 4/8] Fixed actions overriding if there was a goal, but at the same time the ball was teleported to the center, the detection for ball stopped would override the goal detection and the level would fail --- rlbottraining/common_graders/rl_graders.py | 36 +++++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/rlbottraining/common_graders/rl_graders.py b/rlbottraining/common_graders/rl_graders.py index 6bd9916..4de77fa 100644 --- a/rlbottraining/common_graders/rl_graders.py +++ b/rlbottraining/common_graders/rl_graders.py @@ -35,11 +35,31 @@ def __init__(self, max_duration_seconds): self.previous_ang_x = None self.previous_ang_y = None self.previous_ang_z = None + self.previous_total_goals = None - def set_previous_angular_velocity(self, ball): - self.previous_ang_x = ball.angular_velocity.x - self.previous_ang_y = ball.angular_velocity.y - self.previous_ang_z = ball.angular_velocity.z + def set_previous_angular_velocity(self, ball, reset = False): + 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 + else: + self.previous_ang_x = None + self.previous_ang_y = None + self.previous_ang_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) @@ -67,8 +87,14 @@ def on_tick(self, tick: TrainingTickPacket) -> Optional[Grade]: hit_ground = True if math.sqrt(ball.velocity.x**2 + ball.velocity.y**2 + ball.velocity.z**2) == 0: # ball is stop on ground, which means it should fail anyway - hit_ground = True + if self.previous_total_goals != self.current_total_goals(tick.game_tick_packet): + # 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) return grade From 1407a1c22720ed72d1bc509240363931a659a6da Mon Sep 17 00:00:00 2001 From: Skyborg Date: Mon, 15 Apr 2019 00:41:11 +0100 Subject: [PATCH 5/8] fixed dribbling and pushing fixed dribbling not changing angular velocity z, and pushing the ball on the ground changing it --- rlbottraining/common_graders/rl_graders.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/rlbottraining/common_graders/rl_graders.py b/rlbottraining/common_graders/rl_graders.py index 4de77fa..44d48c4 100644 --- a/rlbottraining/common_graders/rl_graders.py +++ b/rlbottraining/common_graders/rl_graders.py @@ -42,10 +42,12 @@ def set_previous_angular_velocity(self, ball, reset = False): 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: @@ -77,16 +79,20 @@ def on_tick(self, tick: TrainingTickPacket) -> Optional[Grade]: 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: + if self.previous_ang_z == ball.angular_velocity.z and not ( 130 < ball.location.z < 150) : # 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: # 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 math.sqrt(ball.velocity.x**2 + ball.velocity.y**2 + ball.velocity.z**2) == 0: - # ball is stop on ground, which means it should fail anyway + 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 goal, let the goal handler handle it hit_ground = False From 66827a8f2be548979cd4fdc0cd5e8e6e768dfcf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Ferreira?= Date: Tue, 16 Apr 2019 02:22:18 +0100 Subject: [PATCH 6/8] Adds prop_bot prop_bot does nothing. Can be used to test interactions between the ball and the bot --- .../example_bots/prop_bot/__init__.py | 0 .../example_bots/prop_bot/prop_bot.cfg | 10 ++++ .../example_bots/prop_bot/prop_bot.py | 14 +++++ .../example_bots/prop_bot/prop_bot_looks.cfg | 56 +++++++++++++++++++ rlbottraining/paths.py | 1 + 5 files changed, 81 insertions(+) create mode 100644 rlbottraining/example_bots/prop_bot/__init__.py create mode 100644 rlbottraining/example_bots/prop_bot/prop_bot.cfg create mode 100644 rlbottraining/example_bots/prop_bot/prop_bot.py create mode 100644 rlbottraining/example_bots/prop_bot/prop_bot_looks.cfg diff --git a/rlbottraining/example_bots/prop_bot/__init__.py b/rlbottraining/example_bots/prop_bot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rlbottraining/example_bots/prop_bot/prop_bot.cfg b/rlbottraining/example_bots/prop_bot/prop_bot.cfg new file mode 100644 index 0000000..2732319 --- /dev/null +++ b/rlbottraining/example_bots/prop_bot/prop_bot.cfg @@ -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 diff --git a/rlbottraining/example_bots/prop_bot/prop_bot.py b/rlbottraining/example_bots/prop_bot/prop_bot.py new file mode 100644 index 0000000..61e453a --- /dev/null +++ b/rlbottraining/example_bots/prop_bot/prop_bot.py @@ -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. + """ + + 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 diff --git a/rlbottraining/example_bots/prop_bot/prop_bot_looks.cfg b/rlbottraining/example_bots/prop_bot/prop_bot_looks.cfg new file mode 100644 index 0000000..1568390 --- /dev/null +++ b/rlbottraining/example_bots/prop_bot/prop_bot_looks.cfg @@ -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 + diff --git a/rlbottraining/paths.py b/rlbottraining/paths.py index 0a0d014..42efbba 100644 --- a/rlbottraining/paths.py +++ b/rlbottraining/paths.py @@ -26,6 +26,7 @@ class BotConfigs: Contains paths to example bots included in this repo. """ brick_bot = _example_bot_dir / 'brick_bot' / 'brick_bot.cfg' + prop_bot = _example_bot_dir / 'prop_bot' / 'prop_bot.cfg' simple_bot = _example_bot_dir / 'simple_bot' / 'simple_bot.cfg' line_goalie = _example_bot_dir / 'line_goalie' / 'line_goalie.cfg' From b362e791dd610bac92214b43e276f81863f625f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Ferreira?= Date: Tue, 16 Apr 2019 02:24:20 +0100 Subject: [PATCH 7/8] Adds unittests for rl_graders This will help improve and maintain the functionality of the rl_grader Following commits should take advantage of this to fix rl_grader --- rlbottraining/common_graders/rl_graders.py | 52 ++++++++++++--- tests/test_exercises/__init__.py | 0 tests/test_exercises/rl_grader_exercises.py | 74 +++++++++++++++++++++ tests/test_rl_graders.py | 35 ++++++++++ 4 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 tests/test_exercises/__init__.py create mode 100644 tests/test_exercises/rl_grader_exercises.py create mode 100644 tests/test_rl_graders.py diff --git a/rlbottraining/common_graders/rl_graders.py b/rlbottraining/common_graders/rl_graders.py index 44d48c4..52446d1 100644 --- a/rlbottraining/common_graders/rl_graders.py +++ b/rlbottraining/common_graders/rl_graders.py @@ -16,6 +16,7 @@ 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): @@ -23,20 +24,55 @@ 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 __init__(self, max_duration_seconds): - super().__init__(max_duration_seconds) + 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): if not reset: self.previous_ang_x = ball.angular_velocity.x @@ -64,10 +100,6 @@ def current_total_goals(self, packet): 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 hit_ground = False @@ -103,4 +135,4 @@ def on_tick(self, tick: TrainingTickPacket) -> Optional[Grade]: self.set_previous_total_goals(self.current_total_goals(tick.game_tick_packet)) if hit_ground: self.set_previous_angular_velocity(ball, reset = True) - return grade + return self.FailDueToGroundHit() diff --git a/tests/test_exercises/__init__.py b/tests/test_exercises/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_exercises/rl_grader_exercises.py b/tests/test_exercises/rl_grader_exercises.py new file mode 100644 index 0000000..b4bd243 --- /dev/null +++ b/tests/test_exercises/rl_grader_exercises.py @@ -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'), + ] diff --git a/tests/test_rl_graders.py b/tests/test_rl_graders.py new file mode 100644 index 0000000..735d46b --- /dev/null +++ b/tests/test_rl_graders.py @@ -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()), + [ + 'FailDueToGroundHit', + #'FailDueToGroundHit', + #'FailDueToTimeout', + ] + ) + +if __name__ == '__main__': + unittest.main() From 6cc8798777b4288ef454ff6a55b41da97fc97ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Ferreira?= Date: Tue, 16 Apr 2019 14:01:41 +0100 Subject: [PATCH 8/8] Refactor and Improves rl_grader, Adds More Tests Now dribbling doesnt cause a ground_hit error. Consequence: pushing ball on the ground doesnt cause it as well. Tests needed to improve Adds some more tests for rl_grader --- rlbottraining/common_graders/rl_graders.py | 144 ++++++++++++-------- tests/test_exercises/rl_grader_exercises.py | 75 +++++++++- tests/test_rl_graders.py | 3 + 3 files changed, 162 insertions(+), 60 deletions(-) diff --git a/rlbottraining/common_graders/rl_graders.py b/rlbottraining/common_graders/rl_graders.py index 52446d1..b5dbcec 100644 --- a/rlbottraining/common_graders/rl_graders.py +++ b/rlbottraining/common_graders/rl_graders.py @@ -17,6 +17,7 @@ from rlbottraining.common_graders.timeout import FailOnTimeout, PassOnTimeout from rlbottraining.common_graders.goal_grader import PassOnGoalForAllyTeam from rlbot.training.training import Pass, Fail, Grade +import copy class RocketLeagueStrikerGrader(CompoundGrader): @@ -60,10 +61,7 @@ def grade_chooser(self, grades) -> Optional[Grade]: 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 + self.previous_ball = None class FailDueToGroundHit(Fail): def __init__(self): @@ -73,66 +71,94 @@ def __repr__(self): return f'{super().__repr__()}: Ball hit the ground' - def set_previous_angular_velocity(self, ball, reset = False): - 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 set_previous_info(self, ball): + self.previous_ball = copy.deepcopy(ball) def on_tick(self, tick: TrainingTickPacket) -> Optional[Grade]: - ball = tick.game_tick_packet.game_ball.physics + packet = tick.game_tick_packet + ball = packet.game_ball.physics 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) : - # if the z angular velocity did not change it means it was a flat plane. - #ignore dribbling + if self.previous_ball is None: + self.set_previous_info(ball) + return None + + max_ang_vel = 5.9999601985025075 #Max angular velocity possible + previous_angular_velocity_norm = math.sqrt(self.previous_ball.angular_velocity.x**2 + + self.previous_ball.angular_velocity.y**2 + + self.previous_ball.angular_velocity.z**2 ) + + angular_velocity_norm = math.sqrt(ball.angular_velocity.x**2 + + ball.angular_velocity.y**2 + + ball.angular_velocity.z**2 ) + + if ball.location.z <= 1900: #Making sure it doesnt count the ceiling + + if ball.angular_velocity.x != self.previous_ball.angular_velocity.x or \ + ball.angular_velocity.y != self.previous_ball.angular_velocity.y or \ + ball.angular_velocity.z != self.previous_ball.angular_velocity.z: + # If the ball hit anything its angular velocity will change in at least one axis + if (previous_angular_velocity_norm or angular_velocity_norm) == max_ang_vel: + ''' + Implement correct way of dealing with maximum angular velocity + angular velocity gets rescaled which may change an axis that truly did not change, only got rescaled + ''' + #Todo: implement detection for this case + self.set_previous_info(ball) + return None + + elif self.previous_ball.angular_velocity.z == ball.angular_velocity.z: + ''' + Ball hit a flat horizontal surface + this only changes angular velocity z + we still have to deal with distingushing from a ground touch, or a bot touch + This will not hold true if the ball is being pushed on the ground by a bot. + Todo: detect pushing from bot + ''' + print('INFO') + print(packet.game_ball.latest_touch.time_seconds) + print(packet.game_info.seconds_elapsed - (2/60)) + + if packet.game_ball.latest_touch.time_seconds >= (packet.game_info.seconds_elapsed - (2/60)): + ''' + if there was a touch this tick the bot may be dribbling + ''' + #Todo: distinguish dribble from pushing on ground. + #Todo: write tests to test pushing. + self.set_previous_info(ball) + return None + else: hit_ground = True - elif previous_ang_norm == max_ang_vel: - # 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 goal, let the goal handler handle it - hit_ground = False - else: + else: + ''' + detect if is being pushed + ''' + if packet.game_ball.latest_touch.time_seconds != packet.game_info.seconds_elapsed: + ''' + ball not hit by a bot on this tick + ''' + self.set_previous_info(ball) + return None + else: + ''' + detect if its pushing along the ground or not + ''' + #Todo: implement detection for this case + pass + + velocity_norm = math.sqrt(ball.velocity.x**2 + + ball.velocity.y**2 + + ball.velocity.z**2) + + + previous_velocity_norm = math.sqrt(self.previous_ball.velocity.x ** 2 + + self.previous_ball.velocity.y ** 2 + + self.previous_ball.velocity.z ** 2 ) + if previous_velocity_norm == velocity_norm == 0: + ''' + ball is stopped on ground + ''' hit_ground = True - - self.set_previous_angular_velocity(ball) - self.set_previous_total_goals(self.current_total_goals(tick.game_tick_packet)) + self.set_previous_info(ball) if hit_ground: - self.set_previous_angular_velocity(ball, reset = True) return self.FailDueToGroundHit() diff --git a/tests/test_exercises/rl_grader_exercises.py b/tests/test_exercises/rl_grader_exercises.py index b4bd243..a0a9c0e 100644 --- a/tests/test_exercises/rl_grader_exercises.py +++ b/tests/test_exercises/rl_grader_exercises.py @@ -16,6 +16,30 @@ test_match_config = make_default_match_config() +@dataclass +class Still(RocketLeagueCustomStrikerTraining): + + """Ball keeps 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, 0) + 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 SimpleFallFromPerfectStill(RocketLeagueCustomStrikerTraining): @@ -40,7 +64,6 @@ def make_game_state(self, rng: SeededRandomNumberGenerator) -> GameState: game_state = GameState(ball=ball_state, cars={0: car_state, 1: enemy_car}) return game_state - @dataclass class SimpleFallFromRotatingStill(RocketLeagueCustomStrikerTraining): @@ -65,10 +88,60 @@ def make_game_state(self, rng: SeededRandomNumberGenerator) -> GameState: game_state = GameState(ball=ball_state, cars={0: car_state, 1: enemy_car}) return game_state +@dataclass +class RollFromGround(RocketLeagueCustomStrikerTraining): + + """Ball starts only with angular velocity""" + + grader: Grader = RocketLeagueStrikerGrader(timeout_seconds=2, 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(5600, 0, 0) + ball_pos = Vector3(rng.randrange(-800, 800), rng.randrange(-5000, 5000), 93) + ball_vel = Vector3(rng.randrange(-2000, 2000), rng.randrange(-2000, 2000), 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 SimpleFallOnCar(RocketLeagueCustomStrikerTraining): + + """Ball starts perfectly still onto a car""" + + 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(0, 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 def make_default_playlist() -> Playlist: return [ + Still('Ball Still'), SimpleFallFromPerfectStill('Fall From Perfect Still'), + RollFromGround('Roll From Ground'), + SimpleFallOnCar('Fall On Car'), #SimpleFallFromRotatingStill('Fall with rotation'), ] diff --git a/tests/test_rl_graders.py b/tests/test_rl_graders.py index 735d46b..2fb891c 100644 --- a/tests/test_rl_graders.py +++ b/tests/test_rl_graders.py @@ -26,6 +26,9 @@ def test_rl_graders(self): run_playlist(make_default_playlist()), [ 'FailDueToGroundHit', + 'FailDueToTimeout', # Cant yet detect this case, should be FailDueToGroundHit + 'FailDueToGroundHit', + 'FailDueToTimeout', #'FailDueToGroundHit', #'FailDueToTimeout', ]