Exemple #1
0
 def best_intercept(self, cars: List[Car]) -> Intercept:
     if not cars:
         return Intercept(Car(), self.info.ball_predictions)
     intercepts = [
         Intercept(car, self.info.ball_predictions) for car in cars
     ]
     return min(intercepts, key=lambda intercept: intercept.time)
Exemple #2
0
    def simulate_landing(self):
        dummy = Car(self.car)
        self.trajectory = [vec3(dummy.position)]
        self.landing = False
        collision_normal: Optional[vec3] = None

        dt = 1 / 60
        simulation_duration = 0.8
        for i in range(int(simulation_duration / dt)):
            dummy.step(Input(), dt)
            self.trajectory.append(vec3(dummy.position))

            collision_sphere = sphere(dummy.position, 50)
            collision_ray = Field.collide(collision_sphere)
            collision_normal = collision_ray.direction

            if (norm(collision_normal) > 0.0
                    or dummy.position[2] < 0) and i > 20:
                self.landing = True
                self.landing_pos = dummy.position
                break

        if self.landing:
            u = collision_normal
            f = normalize(dummy.velocity - dot(dummy.velocity, u) * u)
            l = normalize(cross(u, f))
            self.aerial_turn.target = mat3(f[0], l[0], u[0], f[1], l[1], u[1],
                                           f[2], l[2], u[2])
        else:
            target_direction = normalize(
                normalize(self.car.velocity) - vec3(0, 0, 3))
            self.aerial_turn.target = look_at(target_direction, vec3(0, 0, 1))
Exemple #3
0
 def simulate(self):
     ball_prediction = self.get_ball_prediction_struct()
     duration_estimate = math.floor(
         get_time_at_height(self.game.ball.location[2], 0.2) * 10) / 10
     for i in range(6):
         car = Car(self.game.my_car)
         ball = Ball(self.game.ball)
         batmobile = obb()
         batmobile.half_width = vec3(64.4098892211914, 42.335182189941406,
                                     14.697200775146484)
         batmobile.center = car.location + dot(car.rotation,
                                               vec3(9.01, 0, 12.09))
         batmobile.orientation = car.rotation
         dodge = Dodge(car)
         dodge.duration = duration_estimate + i / 60
         dodge.target = ball.location
         for j in range(round(60 * dodge.duration)):
             dodge.target = ball.location
             dodge.step(1 / 60)
             car.step(dodge.controls, 1 / 60)
             prediction_slice = ball_prediction.slices[j]
             physics = prediction_slice.physics
             ball_location = vec3(physics.location.x, physics.location.y,
                                  physics.location.z)
             dodge.target = ball_location
             batmobile.center = car.location + dot(car.rotation,
                                                   vec3(9.01, 0, 12.09))
             batmobile.orientation = car.rotation
             if intersect(sphere(ball_location, 93.15), batmobile) and abs(
                     ball_location[2] - car.location[2]
             ) < 25 and car.location[2] < ball_location[2]:
                 return True, j / 60, ball_location
     return False, None, None
Exemple #4
0
    def __init__(self, car: Car, ball_predictions, predicate: callable = None):
        self.ball: Ball = None
        self.is_viable = True

        #find the first reachable ball slice that also meets the predicate
        test_car = Car(car)
        test_aerial = Aerial(car)
        
        for ball in ball_predictions:
            test_aerial.target = ball.position
            test_aerial.arrival_time = ball.time

            # fake our car state :D
            dir_to_target = ground_direction(test_car.position, test_aerial.target)
            test_car.velocity = dir_to_target * max(norm(test_car.velocity), 1200)
            test_car.orientation = look_at(dir_to_target, vec3(0,0,1))

            if test_aerial.is_viable() and (predicate is None or predicate(car, ball)):
                self.ball = ball
                break

        #if no slice is found, use the last one
        if self.ball is None:
            self.ball = ball_predictions[-1]
            self.is_viable = False

        self.time = self.ball.time
        self.ground_pos = ground(self.ball.position)
        self.position = self.ball.position
Exemple #5
0
 def car_trajectory(self, car: Car, end_time: float, dt: float = 1 / 10):
     steps = []
     test_car = Car(car)
     while test_car.time < end_time:
         dt = min(dt, end_time - test_car.time)
         test_car.step(Input(), dt)
         test_car.time += dt
         steps.append(vec3(test_car.position))
     self.polyline(steps)
Exemple #6
0
    def step(self, dt):
        if self.aerialing:
            self.aerial.target_orientation = look_at(
                direction(self.car, self.target), vec3(0, 0, -1))
            self.aerial.step(dt)
            self.controls = self.aerial.controls
            self.finished = self.aerial.finished

        else:
            super().step(dt)

            # simulate aerial from current state
            simulated_car = self.simulate_flight(self.car)

            # if the car ended up too far, we're too fast and we need to slow down
            if ground_distance(self.car,
                               self.aerial.target) + 100 < ground_distance(
                                   self.car, simulated_car):
                # self.controls.throttle = -1
                pass

            # if it ended up near the target, we could take off
            elif distance(simulated_car,
                          self.aerial.target) < self.MAX_DISTANCE_ERROR:
                if angle_to(self.car, self.aerial.target) < 0.1 or norm(
                        self.car.velocity) < 1000:

                    if self.DELAY_TAKEOFF:
                        # extrapolate current state a small amount of time
                        future_car = Car(self.car)
                        time = 0.2
                        future_car.time += time
                        displacement = future_car.velocity * time if norm(future_car.velocity) > 500\
                            else normalize(future_car.velocity) * 500 * time
                        future_car.position += displacement

                        # simulate aerial fot the extrapolated car again
                        future_simulated_car = self.simulate_flight(
                            future_car, write_to_flight_path=False)

                        # if the aerial is also successful, that means we should continue driving instead of taking off
                        # this makes sure that we go for the most late possible aerials, which are the most effective
                        if distance(
                                future_simulated_car,
                                self.aerial.target) > self.MAX_DISTANCE_ERROR:
                            self.aerialing = True
                        else:
                            self.too_early = True
                    else:
                        self.aerialing = True

            else:
                # self.controls.boost = True
                self.controls.throttle = 1
    def simulate(self, bot) -> vec3:
        # print('simulate intercept')

        # Init vars
        c = Car(bot.game.my_car)
        b = Ball(bot.game.ball)
        t = vec3(bot.target)
        intercept = self.location
        dt = 1.0 / 60.0
        hit = False
        min_error = None

        # Drive towards intercept (moving in direction of c.forward())
        c.rotation = look_at(intercept, c.up())
        direction = normalize(intercept - c.location)  #c.forward()
        advance_distance = norm(intercept - c.location) - c.hitbox(
        ).half_width[0] - b.collision_radius
        translation = direction * advance_distance
        sim_start_state: ThrottleFrame = BoostAnalysis().travel_distance(
            advance_distance, norm(c.velocity))
        c.velocity = direction * sim_start_state.speed
        c.location += translation
        c.time += sim_start_state.time
        bot.ball_predictions = [vec3(b.location)]

        while b.time < c.time:
            b.step(dt)
            bot.ball_predictions.append(vec3(b.location))

        # print(c.time, b.time)
        # print(c.location, b.location)

        # Simulate the collision and resulting
        for i in range(60 * 3):
            c.location += c.velocity * dt
            b.step(dt, c)

            # Check if we hit the ball yet
            if norm(b.location - c.location) < (c.hitbox().half_width[0] +
                                                b.collision_radius) * 1.05:
                hit = True
                # print('hit')

            # Measure dist from target
            error = t - b.location
            if hit and (min_error == None or norm(error) < norm(min_error)):
                min_error = error

            # Record trajectory
            bot.ball_predictions.append(vec3(b.location))

        if not hit: return None
        return min_error
Exemple #8
0
    def best_intercept(self, cars, max_height=9999) -> Intercept:
        best_intercept = None
        best_car = None

        for car in cars:
            intercept = Intercept(car, self.info.ball_predictions, lambda car, ball: ball.position[2] < max_height)
            if best_intercept is None or intercept.time <= best_intercept.time:
                best_intercept = intercept
                best_car = car

        if best_intercept is None:
            best_car = Car()
            best_intercept = Intercept(best_car, [])

        return best_intercept, best_car
    def simulate_flight(car: Car, aerial: Aerial, flight_path: List[vec3] = None) -> Car:
        test_car = Car(car)
        test_aerial = Aerial(test_car)
        test_aerial.target = aerial.target
        test_aerial.arrival_time = aerial.arrival_time
        test_aerial.angle_threshold = aerial.angle_threshold
        test_aerial.up = aerial.up
        test_aerial.single_jump = aerial.single_jump

        if flight_path: flight_path.clear()

        while not test_aerial.finished:
            test_aerial.step(1 / 120)
            test_car.boost = 100  # TODO: fix boost depletion in RLU car sim
            test_car.step(test_aerial.controls, 1 / 120)

            if flight_path:
                flight_path.append(vec3(test_car.position))

        return test_car
Exemple #10
0
    def simulate_flight(self, car: Car, write_to_flight_path=True) -> Car:
        test_car = Car(car)
        test_aerial = Aerial(test_car)
        test_aerial.target = self.aerial.target
        test_aerial.arrival_time = self.aerial.arrival_time
        test_aerial.angle_threshold = self.aerial.angle_threshold
        test_aerial.up = self.aerial.up
        test_aerial.single_jump = self.aerial.single_jump

        if write_to_flight_path:
            self._flight_path.clear()

        while not test_aerial.finished:
            test_aerial.step(1 / 120)
            test_car.boost = 100  # TODO: fix boost depletion in RLU car sim
            test_car.step(test_aerial.controls, 1 / 120)

            if write_to_flight_path:
                self._flight_path.append(vec3(test_car.position))

        return test_car
Exemple #11
0
    def reset_gamestate(self):
        print('> reset_gamestate()')

        # Initialize inputs
        self.reset_for_ground_shots()
        t = self.target
        b = Ball(self.game.ball)
        c = Car(self.game.cars[self.index])
        b.location = to_vec3(self.initial_ball_location)
        b.velocity = to_vec3(self.initial_ball_velocity)
        c.location = to_vec3(self.initial_car_location)
        c.velocity = to_vec3(self.initial_car_velocity)

        # Point car at ball
        c.rotation = look_at(
            vec3(b.location[0] - c.location[0], b.location[1] - c.location[1],
                 0), vec3(0, 0, 1))
        rotator = rotation_to_euler(c.rotation)

        # Reset
        self.aerial = None
        self.dodge = None
        self.rotation_input = None
        self.timer = 0.0

        # Set gamestate
        car_state = CarState(boost_amount=100,
                             physics=Physics(
                                 location=self.initial_car_location,
                                 velocity=self.initial_car_velocity,
                                 rotation=rotator,
                                 angular_velocity=Vector3(0, 0, 0)))
        ball_state = BallState(
            Physics(location=self.initial_ball_location,
                    velocity=self.initial_ball_velocity,
                    rotation=Rotator(0, 0, 0),
                    angular_velocity=Vector3(0, 0, 0)))
        game_state = GameState(ball=ball_state, cars={self.index: car_state})
        self.set_game_state(game_state)
from rlutilities.simulation import Car, Navigator
from rlutilities.mechanics import FollowPath
from rlutilities.linear_algebra import vec3, normalize

c = Car()

c.time = 0.0
c.position = vec3(0, 0, 0)
c.velocity = vec3(1000, 0, 0)
c.angular_velocity = vec3(0.1, -2.0, 1.2)
c.on_ground = False

navigator = Navigator(c)

drive = FollowPath(c)
drive.arrival_speed = 1234
drive.path = navigator.path_to(vec3(1000, 0, 0), vec3(1, 0, 0), 1000)

for p in drive.path.points:
    print(p)
Exemple #13
0
    def get_output(self, packet: GameTickPacket) -> SimpleControllerState:
        # Record start time
        self.tick_start = time.time()

        # Gather some information about our car and the ball
        my_car: CarState = packet.game_cars[self.index]
        car_location = Vec3(my_car.physics.location)
        car_velocity = Vec3(my_car.physics.velocity)
        car_direction = car_velocity.ang_to(Vec3(
            1, 0, 0)) if car_velocity.length() > 0 else 0
        ball_location = Vec3(packet.game_ball.physics.location)
        ball_velocity = Vec3(packet.game_ball.physics.velocity)
        ball_direction = ball_velocity.ang_to(Vec3(
            1, 0, 0)) if ball_velocity.length() > 0 else 0
        reset = False

        # Initialize simulation game model
        if self.game == None:
            Game.set_mode('soccar')
            self.game = Game(self.index, self.team)
            self.game.read_game_information(packet, self.get_rigid_body_tick(),
                                            self.get_field_info())
            self.target = vec3(
                0, 5120 + 880 / 2 if self.team is 0 else -(5120 + 880 / 2),
                642.775 / 2)  # Opposing net
            self.reset_gamestate()
            print('TEAM', self.team)
            return SimpleControllerState()

        # Update simulation
        self.game.read_game_information(packet, self.get_rigid_body_tick(),
                                        self.get_field_info())

        # Check for car hit ball
        if self.last_touch_location != packet.game_ball.latest_touch.hit_location:
            self.last_touch_location = Vec3(
                packet.game_ball.latest_touch.hit_location)
            print(f'> Car hit ball')
            self.not_hit_yet = False

        # Recalculate intercept every frame
        self.plan()

        # Re-simulate the aerial every frame
        if self.aerial is not None and not self.aerial.finished:
            simulate_aerial(self)
            simulate_alternate_aerials(self)

        # Update dodge (init or clean up old)
        # try_init_dodge(self)

        # Rendering
        if len(self.ball_predictions) > 2:
            self.renderer.draw_polyline_3d(self.ball_predictions,
                                           self.renderer.red())
        if self.aerial != None and self.aerial.target:
            self.renderer.draw_rect_3d(self.aerial.target,
                                       8,
                                       8,
                                       True,
                                       self.renderer.green(),
                                       centered=True)
            self.renderer.draw_line_3d(car_location, self.aerial.target,
                                       self.renderer.white())
            self.renderer.draw_line_3d(vec3_to_Vec3(self.target),
                                       self.target + self.avg_aerial_error,
                                       self.renderer.cyan())
        if self.intercept != None:
            self.renderer.draw_rect_3d(self.intercept.location,
                                       8,
                                       8,
                                       True,
                                       self.renderer.green(),
                                       centered=True)
        self.renderer.draw_rect_3d(vec3_to_Vec3(self.target),
                                   8,
                                   8,
                                   True,
                                   self.renderer.green(),
                                   centered=True)

        # Controller state
        if reset:
            self.reset_gamestate()
            return SimpleControllerState()
        # "Do a flip!"
        elif self.dodge is not None:
            if self.dodge.finished:
                self.dodge = None
                return SimpleControllerState()
            self.dodge.step(self.game.time_delta)
            return self.dodge.controls
        # Just do an aerial :4head:
        elif self.aerial is not None:
            aerial_step(self.aerial, Car(self.game.my_car),
                        self.rotation_input, self.game.time_delta)
            return self.aerial.controls
        # Just hit the ball :4head:
        elif self.intercept is not None:
            if self.intercept.dodge and abs(self.game.time - self.intercept.
                                            jump_time) <= self.game.time_delta:
                print('im gonna nut')
                self.dodge = Dodge(self.game.my_car)
                self.dodge.duration = 0.2
                self.dodge.preorientation = self.intercept.dodge_preorientation
                self.dodge.delay = self.intercept.dodge_delay + 0.1
                self.dodge.direction = self.intercept.dodge_direction
                self.dodge.step(self.game.time_delta)
                return self.dodge.controls
            return self.intercept.get_controls(
                my_car, self.game.my_car
            )  #drive_at(self, my_car, self.intercept.location)

        return SimpleControllerState()
Exemple #14
0
    def simulate(self, global_target=None):
        lol = 0
        # Initialize the ball prediction
        # Estimate the probable duration of the jump and round it down to the floor decimal
        ball_prediction = self.get_ball_prediction_struct()
        if self.info.my_car.boost < 6:
            duration_estimate = math.floor(
                get_time_at_height(self.info.ball.position[2]) * 10) / 10
        else:
            adjacent = norm(
                vec2(self.info.my_car.position - self.info.ball.position))
            opposite = (self.info.ball.position[2] -
                        self.info.my_car.position[2])
            theta = math.atan(opposite / adjacent)
            t = get_time_at_height_boost(self.info.ball.position[2], theta,
                                         self.info.my_car.boost)
            duration_estimate = (math.ceil(t * 10) / 10)
        # Loop for 6 frames meaning adding 0.1 to the estimated duration. Keeps the time constraint under 0.3s
        for i in range(6):
            # Copy the car object and reset the values for the hitbox
            car = Car(self.info.my_car)
            # Create a dodge object on the copied car object
            # Direction is from the ball to the enemy goal
            # Duration is estimated duration plus the time added by the for loop
            # preorientation is the rotation matrix from the ball to the goal
            # TODO make it work on both sides
            #  Test with preorientation. Currently it still picks a low duration at a later time meaning it
            #  wont do any of the preorientation.
            dodge = Dodge(car)
            prediction_slice = ball_prediction.slices[round(
                60 * (duration_estimate + i / 60))]
            physics = prediction_slice.physics
            ball_location = vec3(physics.location.x, physics.location.y,
                                 physics.location.z)
            # ball_location = vec3(0, ball_y, ball_z)
            dodge.duration = duration_estimate + i / 60
            if dodge.duration > 1.4:
                break

            if global_target is not None:
                dodge.direction = vec2(global_target - ball_location)
                target = vec3(vec2(global_target)) + vec3(
                    0, 0, jeroens_magic_number * ball_location[2])
                dodge.preorientation = look_at(target - ball_location,
                                               vec3(0, 0, 1))
            else:
                dodge.target = ball_location
                dodge.direction = vec2(ball_location) + vec2(ball_location -
                                                             car.position)
                dodge.preorientation = look_at(ball_location, vec3(0, 0, 1))
            # Loop from now till the end of the duration
            fps = 30
            for j in range(round(fps * dodge.duration)):
                lol = lol + 1
                # Get the ball prediction slice at this time and convert the location to RLU vec3
                prediction_slice = ball_prediction.slices[round(60 * j / fps)]
                physics = prediction_slice.physics
                ball_location = vec3(physics.location.x, physics.location.y,
                                     physics.location.z)
                dodge.step(1 / fps)

                T = dodge.duration - dodge.timer
                if T > 0:
                    if dodge.timer < 0.2:
                        dodge.controls.boost = 1
                        dodge.controls.pitch = 1
                    else:
                        xf = car.position + 0.5 * T * T * vec3(
                            0, 0, -650) + T * car.velocity

                        delta_x = ball_location - xf
                        if angle_between(vec2(car.forward()),
                                         dodge.direction) < 0.3:
                            if norm(delta_x) > 50:
                                dodge.controls.boost = 1
                                dodge.controls.throttle = 0.0
                            else:
                                dodge.controls.boost = 0
                                dodge.controls.throttle = clip(
                                    0.5 * (200 / 3) * T * T, 0.0, 1.0)
                        else:
                            dodge.controls.boost = 0
                            dodge.controls.throttle = 0.0
                else:
                    dodge.controls.boost = 0

                car.step(dodge.controls, 1 / fps)
                succesfull = self.dodge_succesfull(car, ball_location, dodge)
                if succesfull is not None:
                    if succesfull:
                        return True, j / fps, ball_location
                    else:
                        break
        return False, None, None
Exemple #15
0
    def step(self, dt):
        time_left = self.aerial.arrival_time - self.car.time

        if self.aerialing:
            to_ball = direction(self.car, self.info.ball)

            # freestyling
            if self.car.position[2] > 200:
                # if time_left > 0.7:
                #     rotation = axis_to_rotation(self.car.forward() * 0.5)
                #     self.aerial.up = dot(rotation, self.car.up())
                # else:
                self.aerial.up = vec3(0, 0, -1) + xy(to_ball)

            self.aerial.target_orientation = look_at(to_ball,
                                                     vec3(0, 0, -3) + to_ball)
            self.aerial.step(dt)

            self.controls = self.aerial.controls
            self.finished = self.aerial.finished and time_left < -0.3

        else:
            super().step(dt)

            # simulate aerial from current state
            simulated_car = self.simulate_flight(self.car, self.aerial,
                                                 self._flight_path)

            speed_towards_target = dot(
                self.car.velocity,
                ground_direction(self.car, self.aerial.target_position))
            speed_needed = ground_distance(
                self.car, self.aerial.target_position) / time_left

            # too fast, slow down
            if speed_towards_target > speed_needed and angle_to(
                    self.car, self.aerial.target_position) < 0.1:
                self.controls.throttle = -1

            # if it ended up near the target, we could take off
            elif distance(
                    simulated_car,
                    self.aerial.target_position) < self.MAX_DISTANCE_ERROR:
                if angle_to(self.car,
                            self.aerial.target_position) < 0.1 or norm(
                                self.car.velocity) < 1000:

                    if self.DELAY_TAKEOFF and ground_distance(
                            self.car, self.aerial.target_position) > 1000:
                        # extrapolate current state a small amount of time
                        future_car = Car(self.car)
                        time = 0.5
                        future_car.time += time
                        displacement = future_car.velocity * time if norm(future_car.velocity) > 500\
                            else normalize(future_car.velocity) * 500 * time
                        future_car.position += displacement

                        # simulate aerial fot the extrapolated car again
                        future_simulated_car = self.simulate_flight(
                            future_car, self.aerial)

                        # if the aerial is also successful, that means we should continue driving instead of taking off
                        # this makes sure that we go for the most late possible aerials, which are the most effective
                        if distance(future_simulated_car, self.aerial.
                                    target_position) > self.MAX_DISTANCE_ERROR:
                            self.aerialing = True
                        else:
                            self.too_early = True
                    else:
                        self.aerialing = True

            else:
                # self.controls.boost = True
                self.controls.throttle = 1
    def calculate_old(car: Car,
                      ball: Ball,
                      target: vec3,
                      ball_predictions=None):
        # Init vars
        fake_car = Car(car)
        b = Ball(ball)

        # Generate predictions of ball path
        if ball_predictions is None:
            ball_predictions = [vec3(b.location)]
            for i in range(60 * 5):
                b.step(1.0 / 60.0)
                ball_predictions.append(vec3(b.location))

        # Gradually converge on ball location by aiming at a location, checking time to that location,
        # and then aiming at the ball's NEW position. Guaranteed to converge (typically in <10 iterations)
        # unless the ball is moving away from the car faster than the car's max boost speed
        intercept = Intercept(b.location)
        intercept.purpose = 'ball'
        intercept.boost = True
        intercept_ball_position = vec3(b.location)
        i = 0
        max_tries = 100
        analyzer = BoostAnalysis() if intercept.boost else ThrottleAnalysis()
        while i < max_tries:
            # Find optimal spot to hit the ball
            optimal_hit_vector = normalize(
                target - intercept_ball_position) * b.collision_radius
            optimal_hit_location = intercept_ball_position - optimal_hit_vector

            # Find ideal rotation, unless it intersects with ground
            optimal_rotation = look_at(
                optimal_hit_vector, vec3(0, 0, 1)
            )  #axis_to_rotation(optimal_hit_vector) # this might be wrong
            fake_car.rotation = optimal_rotation
            # print(f'fake_car.location {fake_car.location}')
            # print(f'get_car_front_center(fake_car) {get_car_front_center(fake_car)}')
            fake_car.location += optimal_hit_location - get_car_front_center(
                fake_car
            )  # try to position the car's front center directly on top of the best hit vector
            euler = rotation_to_euler(optimal_rotation)
            # todo put some super precise trigonometry in here to find the max angle allowed at given height
            if fake_car.location[2] <= fake_car.hitbox().half_width[0]:
                euler.pitch = 0
            fake_car.rotation = euler_to_rotation(
                vec3(euler.pitch, euler.yaw, euler.roll))
            fake_car.location += optimal_hit_location - get_car_front_center(
                fake_car
            )  # try to position the car's front center directly on top of the best hit vector

            # Adjust vertical position if it (still) intersects with ground
            if fake_car.location[2] < 17.0:
                fake_car.location[2] = 17.0
            intercept.location = get_car_front_center(fake_car)

            # Calculate jump time needed
            jump_height_time = JumpAnalysis().get_frame_by_height(
                intercept.location[2]).time  # or solve with motion equation

            # car_euler = rotation_to_euler(car.rotation)
            # jump_pitch_time = (euler.pitch - car_euler.pitch) / 5.5 + 0.35 # disregarding angular acceleration
            # jump_yaw_time = (euler.yaw - car_euler.yaw) / 5.5 + 0.35 # disregarding angular acceleration
            # jump_roll_time = (euler.roll - car_euler.roll) / 5.5 + 0.35 # disregarding angular acceleration
            # jump_time = max(jump_height_time, jump_pitch_time, jump_yaw_time, jump_roll_time)
            jump_time = jump_height_time  # todo revisit rotation time
            # print('jump_time', jump_time)

            # Calculate distance to drive before jumping (to arrive perfectly on target)
            total_translation = intercept.location - get_car_front_center(car)
            total_translation[2] = 0
            total_distance = norm(total_translation)
            start_index = analyzer.get_index_by_speed(norm(car.velocity))
            start_frame = analyzer.frames[start_index]
            custom_error_func = lambda frame: abs(total_distance - (
                frame.distance - start_frame.distance) - frame.speed *
                                                  jump_time)
            drive_analysis = analyzer.get_frame_by_error(
                custom_error_func, start_index)
            arrival_time = drive_analysis.time - start_frame.time + jump_time
            # print('drive_analysis.time', drive_analysis.time)
            # print('drive_analysis', start_index)

            # arrival_time = analyzer.travel_distance(total_distance, norm(car.velocity)).time

            # drive_analysis = analyzer.travel_distance(norm(intercept.location - c.location), norm(c.velocity))
            ball_index = int(round(arrival_time * 60))
            if ball_index >= len(ball_predictions):
                intercept.location = ball_predictions[-1]
                intercept.time = len(ball_predictions) / 60.0
                break
            ball_location = ball_predictions[ball_index]
            # print(f'Iteration {i} distance {norm(ball_location + vec3(optimal_hit_vector[0], optimal_hit_vector[1], 0) - intercept.location)}')
            if norm(ball_location - intercept_ball_position) <= 1:
                # if norm(intercept_ball_position - get_car_front_center(fake_car)) > 100:
                #     intercept.location = ball_predictions[-1]
                #     intercept.time = len(ball_predictions) / 60.0
                #     return intercept

                intercept.dodge = True  #jump_time > 0.2
                intercept.jump_time = car.time + arrival_time - jump_time
                intercept.dodge_preorientation = euler_to_rotation(
                    vec3(euler.pitch, euler.yaw, euler.roll))
                intercept.dodge_delay = jump_time
                intercept.dodge_direction = normalize(vec2(optimal_hit_vector))
                # print(f'intercept_ball_position', intercept_ball_position)
                # print(f'intercept.location', intercept.location)
                # print(f'time until jump {drive_analysis.time}')
                # print(f'time now {car.time}')
                # print(f'distance until jump {drive_analysis.distance}')
                # print(f'total distance to target {total_distance}')
                # print(f'horiz speed @ jump {drive_analysis.speed}')
                # print(f'time intended to be in air {jump_time}')
                # print(f'distance travelled in air {jump_time * drive_analysis.speed}')
                # print(f'distance remaining to target @ jump {total_distance - drive_analysis.distance}')
                # print(f'Intercept convergence in {i} iterations')
                # print(f'desired roll {euler.roll}')
                # print(f'actual roll {rotation_to_euler(c.rotation).roll}')
                break

            intercept_ball_position = vec3(ball_location)
            # intercept.location = vec3(ball_location)
            # intercept.location[2] = 0
            intercept.time = arrival_time
            i += 1

        if i >= max_tries:
            print(
                f'Warning: max tries ({max_tries}) exceeded for calculating intercept'
            )

        # Intercept is only meant for ground paths (and walls/cieling are only indirectly supported)
        # collision_radius = c.hitbox().half_width[2] * 2 + b.collision_radius + b.collision_radius * 8
        # on_ground = intercept.location[2] <= collision_radius
        # on_back_wall = abs(intercept.location[1]) >= 5120 - collision_radius
        # on_side_wall = abs(intercept.location[0]) >= 4096 - collision_radius
        # # on_cieling = intercept.location[2] >= 2044 - collision_radius
        # reachable = on_ground # or on_back_wall or on_side_wall # or on_cieling
        # if not reachable:
        #     return None

        return intercept
    def calculate(car: Car, ball: Ball, target: vec3, ball_predictions=None):
        # Init vars
        b = Ball(ball)
        dt = 1.0 / 60.0

        # Generate predictions of ball path
        if ball_predictions is None:
            ball_predictions = []
            for i in range(60 * 5):
                b.step(dt)
                ball_predictions.append(vec3(b.location))

        # Gradually converge on ball location by aiming at a location, checking time to that location,
        # and then aiming at the ball's NEW position. Guaranteed to converge (typically in <10 iterations)
        # unless the ball is moving away from the car faster than the car's max boost speed
        intercept = Intercept(b.location)
        intercept.purpose = 'ball'
        intercept.boost = True
        intercept_ball_position = vec3(b.location)
        collision_achieved = False
        last_horizontal_error = None
        last_horizontal_offset = None
        i = 0
        max_tries = 101
        analyzer = BoostAnalysis() if intercept.boost else ThrottleAnalysis()
        while i < max_tries:
            i += 1
            fake_car = Car(car)
            direction = normalize(intercept.location - car.location)
            fake_car.rotation = look_at(direction, fake_car.up())

            for t in range(60 * 5):
                # Step car location with throttle/boost analysis data
                # Not super efficient but POITROAE
                frame = analyzer.travel_time(dt, norm(fake_car.velocity))
                # print('in 1 frame I travel', frame.time, frame.distance, frame.speed)
                fake_car.location += direction * frame.distance
                fake_car.velocity = direction * frame.speed
                fake_car.time += dt
                ball_location = ball_predictions[t]

                # Check for collision
                p = closest_point_on_obb(fake_car.hitbox(), ball_location)
                if norm(p - ball_location) <= ball.collision_radius:
                    direction_vector = p - (fake_car.location - normalize(
                        fake_car.forward()) * 13.88)  # octane center of mass
                    direction_vector[2] = 0
                    target_direction_vector = target - ball_location
                    target_direction_vector[2] = 0
                    intercept_ball_position = ball_location
                    direction = atan2(direction_vector[1], direction_vector[0])
                    ideal_direction = atan2(target_direction_vector[1],
                                            target_direction_vector[0])
                    horizontal_error = direction - ideal_direction

                    # intercept.location = vec3(ball_location)
                    # intercept.time = fake_car.time
                    # return intercept

                    # Now descend the hit direction gradient
                    # Kick off the gradient descent with an arbitrary seed value
                    if last_horizontal_error is None:
                        last_horizontal_error = horizontal_error
                        last_horizontal_offset = 0
                        if horizontal_error > 0:
                            horizontal_offset = 25
                        else:
                            horizontal_offset = 25
                        intercept.location = ball_location - normalize(
                            fake_car.left()) * horizontal_offset
                        break

                    # Recursive case of gradient descent
                    if horizontal_offset == last_horizontal_offset:
                        gradient = 0
                    else:
                        gradient = (horizontal_error - last_horizontal_error
                                    ) / (horizontal_offset -
                                         last_horizontal_offset)

                    if gradient == 0:
                        predicted_horizontal_offset = horizontal_offset
                    else:
                        predicted_horizontal_offset = horizontal_offset - horizontal_error / gradient

                    # Base case (convergence)
                    if abs(gradient) < 0.0005:
                        print(f'convergence in {i} iterations')
                        print(f'gradient = {gradient}')
                        print(
                            f'last_horizontal_offset = {last_horizontal_offset}'
                        )
                        print(f'direction = {degrees(direction)}')
                        print(f'ideal direction = {degrees(ideal_direction)}')
                        print(f'target = {target}')
                        print(f'ball_location = {ball_location}')
                        return intercept

                    # Edge case exit: offset maxed out
                    max_horizontal_offset = car.hitbox(
                    ).half_width[1] + ball.collision_radius
                    if predicted_horizontal_offset > max_horizontal_offset:
                        predicted_horizontal_offset = max_horizontal_offset
                    elif predicted_horizontal_offset < -max_horizontal_offset:
                        predicted_horizontal_offset = -max_horizontal_offset
                    last_horizontal_offset = horizontal_offset
                    last_horizontal_error = horizontal_error
                    horizontal_offset = predicted_horizontal_offset

                    # Return the latest intercept location and continue descending the gradient
                    intercept.location = ball_location - normalize(
                        fake_car.left()) * predicted_horizontal_offset
                    print(f'iteration {i}')
                    print(f'gradient = {gradient}')
                    print(f'horizontal_offset = {horizontal_offset}')
                    print(f'horizontal_error = {degrees(horizontal_error)}')
                    # print(f'ideal direction = {degrees(ideal_direction)}')
                    break

                # Check for arrival
                if norm(fake_car.location -
                        intercept.location) < ball.collision_radius / 2:
                    intercept.location = ball_location
                    break

        if i >= max_tries:
            print(
                f'Warning: max tries ({max_tries}) exceeded for calculating intercept'
            )
        return intercept