Skip to content
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

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 42 additions & 10 deletions rlbottraining/common_graders/rl_graders.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,63 @@
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):
Copy link
Collaborator

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?

Copy link
Member Author

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.

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):
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Collaborator

Choose a reason for hiding this comment

The 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.
copy.deepcopy may be what we're after if the ball is a normal object.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, thank you :)
I will do that, i don't like what i did either, but i didn't knew an alternative

if not reset:
self.previous_ang_x = ball.angular_velocity.x
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reset seems unnecessary to me. How come it's here?

Copy link
Member Author

@skyborgff skyborgff Apr 15, 2019

Choose a reason for hiding this comment

The 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

Copy link
Collaborator

Choose a reason for hiding this comment

The 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 grade
return self.FailDueToGroundHit()
Empty file.
74 changes: 74 additions & 0 deletions tests/test_exercises/rl_grader_exercises.py
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'),
]
35 changes: 35 additions & 0 deletions tests/test_rl_graders.py
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()),
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.
Main reason is to make it easy to see which grade corresponds with which exercise.
Note: the reason the other tests work differently is because the make_default_playlist() has other uses as well, whereas here it does not.

Copy link
Member Author

Choose a reason for hiding this comment

The 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()