diff --git a/rlbottraining/common_graders/rl_graders.py b/rlbottraining/common_graders/rl_graders.py index 0ff32c9..b5dbcec 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 @@ -15,6 +16,8 @@ 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 +import copy class RocketLeagueStrikerGrader(CompoundGrader): @@ -22,20 +25,140 @@ 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_ball = None + + class FailDueToGroundHit(Fail): + def __init__(self): + pass + + def __repr__(self): + return f'{super().__repr__()}: Ball hit the ground' + + + def set_previous_info(self, ball): + self.previous_ball = copy.deepcopy(ball) def on_tick(self, tick: TrainingTickPacket) -> Optional[Grade]: - grade = super().on_tick(tick) - if grade is None: + packet = tick.game_tick_packet + ball = packet.game_ball.physics + hit_ground = False + + if self.previous_ball is None: + self.set_previous_info(ball) 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 + 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 + 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_info(ball) + if hit_ground: + return self.FailDueToGroundHit() 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' 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..a0a9c0e --- /dev/null +++ b/tests/test_exercises/rl_grader_exercises.py @@ -0,0 +1,147 @@ +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 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): + + """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 + +@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 new file mode 100644 index 0000000..2fb891c --- /dev/null +++ b/tests/test_rl_graders.py @@ -0,0 +1,38 @@ +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', + 'FailDueToTimeout', # Cant yet detect this case, should be FailDueToGroundHit + 'FailDueToGroundHit', + 'FailDueToTimeout', + #'FailDueToGroundHit', + #'FailDueToTimeout', + ] + ) + +if __name__ == '__main__': + unittest.main()