def flip_towards(s, target_pos): ''' Takes into account velocity ''' towards_target_dir = normalize(target_pos - s.car.pos) desired_vel = towards_target_dir * min( MAX_CAR_SPEED * 1.05, s.car.speed + FLIP_SPEED_CHANGE * 1.0) acceleration_dir = normalize(desired_vel - s.car.vel) return flip_in_direction(s, acceleration_dir)
def pick_offset_target(predicted_slice): # pos = ball_pos(predicted_slice) # self.target_pos = self.get_target_pos(s) # pos += + 50 * UP # return pos future_ball = Ball(predicted_slice.physics) target_ball_pos = s.enemy_goal_center to_goal_dir = normalize(z0(target_ball_pos - future_ball.pos)) # DONE: predict the ball by some small amount. # DONE: avoid ball when coming back # DONE: hit at an angle to change ball velocity desired_ball_speed = MAX_CAR_SPEED desired_ball_vel = MAX_CAR_SPEED * to_goal_dir desired_ball_vel_change = desired_ball_vel - future_ball.vel normal_dir = -normalize( z0(desired_ball_vel)) # center of ball to hit point # alignment = dot(normal_dir, normalize(z0(s.car.pos - future_ball.pos))) # hit_radius = (0.9 if alignment > 0.7 else 1.5) * BALL_RADIUS hit_radius = 0.9 * BALL_RADIUS ball_hit_offset = hit_radius * normal_dir # ball_hit_offset = -0.8 * BALL_RADIUS * to_goal_dir target_pos = future_ball.pos + ball_hit_offset # Avoid the ball when coming back if dist(s.car.pos, target_ball_pos) < dist( future_ball.pos, target_ball_pos): # TODO: make sure options are in bounds avoid_back_ball_radius = BALL_RADIUS * 5.0 options = [ future_ball.pos + avoid_back_ball_radius * normalize(Vec3(1, -s.enemy_goal_dir, 0)), future_ball.pos + avoid_back_ball_radius * normalize(Vec3(-1, -s.enemy_goal_dir, 0)), ] best_avoid_option = min(options, key=lambda avoid_ball_pos: dist( s.car.pos, avoid_ball_pos)) # TODO: factor in current velocity maybe target_pos = best_avoid_option target_pos += 10.0 * UP # fudge return target_pos
def flip_in_direction(s, target_direction): ''' returns a (pitch, yaw, roll) tuple which will make the car flip in the @target_direction Flip testing notes: I call it flipping, others call it air-rolling, but I dislike "rolling" in this context as it implies it's slow. Flipping will set your vertical speed to 0. "flip_forward" is the normalized direction for car.forward projected onto the horizontal plane. I define a "front flip" to be the flip where the cars nose turns towards the wheels. When front flipping, velocity is added in the flip_forward direction. This implies that if you pich your car up slightly more than 90degrees and front flip, you'll gain speed in the direction what used to be your backwards (before pitching up). car-orientation-roll does not affect flip_forward. ''' target_direction = normalize(target_direction) flip_forward = normalize(z0(s.car.forward)) flip_right = cross(flip_forward, UP) yaw = 0.0 pitch = -dot(target_direction, flip_forward) roll = -dot(target_direction, flip_right) # pitch = 0 return (pitch, yaw, roll)
def get_pitch_yaw_roll(car, forward, up=UP): """ Returns a (pitch, yaw, roll) tuple which try to orient the car with the nose pointing in the @forward direction and with the roof pointing in the @up direction """ forward = normalize(forward) desired_facing_angular_vel = -cross(car.forward, forward) desired_up_angular_vel = -cross(car.up, up) pitch = dot(desired_facing_angular_vel, car.right) yaw = -dot(desired_facing_angular_vel, car.up) roll = dot(desired_up_angular_vel, car.forward) pitch_vel = dot(car.angular_vel, car.right) yaw_vel = -dot(car.angular_vel, car.up) roll_vel = dot(car.angular_vel, car.forward) # avoid getting stuck in directly-opposite states if dot(car.up, up) < -.8 and dot( car.forward, forward ) > .8: #abs(roll_vel) < .3 and dot(car.up, up) < -.98 and dot(car.forward, forward) > .8: if roll == 0: roll = 1 roll *= 1e10 if dot(car.forward, forward) < -.8: if pitch == 0: pitch = 1 pitch *= 1e10 # TODO: do this for pitch too. (yaw not required) if dot(car.forward, forward) < 0.0: pitch_vel *= -1 # PID control to stop overshooting. roll = 3 * roll + 0.30 * roll_vel yaw = 3 * yaw + 0.70 * yaw_vel pitch = 3 * pitch + 0.90 * pitch_vel # only start adjusting roll once we're roughly facing the right way if dot(car.forward, forward) < 0: roll = 0 # To debug a single-axis # pitch = 0 # yaw = 0 # roll = 0 return (pitch, yaw, roll)
def get_output(self, game_tick_packet: GameTickPacket) -> SimpleControllerState: s = EasyGameState(game_tick_packet, self.team, self.index) car_obj = game_tick_packet.game_cars[self.index] car = car_obj.physics # Find the time/position where we should intercept the ball. car_y = car.location.y to_ball_y = game_tick_packet.game_ball.physics.location.y - car_y intercept_y = car_y + copysign(self.AIM_Z_DIST, to_ball_y) prediction_struct = self.get_ball_prediction_struct() ball_intercept = prediction_struct.slices[0] to_enemy_goal_y = 1 if self.team == 0 else -1 desired_car_y = -5200 * to_enemy_goal_y + self.DESIRED_OFFSET_FROM_OWN_GOAL_Y * to_enemy_goal_y self.render_horizontal_line(desired_car_y, self.MIN_HIGH_JUMP_Z) for i, next_intercept in zip(range(prediction_struct.num_slices), prediction_struct.slices): if i == 0 or i == 1: continue if ( abs(next_intercept.physics.location.y - intercept_y) < abs(ball_intercept.physics.location.y - intercept_y) ): ball_intercept = next_intercept else: # Do not search further: we do not care about backboard bounces which might come closer. break ball_is_travelling_towards_line = ball_intercept.physics.velocity.y * to_ball_y < 0 ball_intercept.physics.location.y = intercept_y other_defenders = [ car for car in s.allies if ( abs(car.pos[TO_ORANGE] - intercept_y) < 5.0 * BALL_RADIUS and abs(car.pos[TO_STATUE]) < 1.2*GOAL_CENTER_TO_POST ) ] seconds_until_intercept = ball_intercept.game_seconds - game_tick_packet.game_info.seconds_elapsed seconds_until_jump = seconds_until_intercept - self.JUMP_BEFORE_INTERCEPT_SECONDS seconds_until_doge = seconds_until_intercept - self.DODGE_BEFORE_INTERCEPT_SECONDS state = self.State.GROUND if game_tick_packet.game_info.is_kickoff_pause and abs(car_y - desired_car_y) > BALL_RADIUS: state = self.State.KICKOFF elif car_obj.has_wheel_contact: if ball_is_travelling_towards_line: if abs(ball_intercept.physics.location.x) > GOAL_CENTER_TO_POST: # TODO: Side defence. state = self.State.IDLE elif ball_intercept.physics.location.z > self.MAX_HIGH_JUMP_Z: state = self.State.IDLE elif ball_intercept.physics.location.z > self.MIN_HIGH_JUMP_Z: if seconds_until_intercept < self.high_jump_before_intercept(ball_intercept.physics.location.z): state = self.State.HIGH_JUMP else: state = self.State.HIGH_JUMP_GROUND else: if seconds_until_intercept < self.JUMP_BEFORE_INTERCEPT_SECONDS: state = self.State.JUMPING else: state = self.State.GROUND else: state = self.State.IDLE else: if not ball_is_travelling_towards_line: state = self.State.IDLE elif ball_intercept.physics.location.z > self.MIN_HIGH_JUMP_Z: state = self.State.HIGH_JUMP else: if car.location.z <= self.HEIGHT_BEFORE_DODGING: state = self.State.JUMPING elif car_obj.double_jumped: state = self.State.IDLE else: state = self.State.DODGING # Flip to the ball if convenient if state == self.State.IDLE: near_future_ball = ball_pos(bisect_ball_prediction(prediction_struct, lambda slice: ( slice.game_seconds - s.time < self.JUMP_BEFORE_INTERCEPT_SECONDS ))) if ( dist(near_future_ball, s.car.pos) < self.MAX_CONVENIENT_JUMP_DISTANCE and near_future_ball[TO_CEILING] < self.MAX_CONVENIENT_JUMP_HEIGHT and abs(near_future_ball[TO_ORANGE]) < abs(s.car.pos[TO_ORANGE]) ): state = self.State.JUMPING # Don't over commit. Rely on team mates to jump for it if they're closer and in line wit the ball. if state in [self.State.HIGH_JUMP, self.State.JUMPING, self.State.HIGH_JUMP_GROUND]: if any(( dist(other.pos, s.ball.pos) < dist(s.ball.pos, s.car.pos) and 0.0 < dot(normalize(s.ball.pos - s.car.pos), other.pos - s.car.pos) < dist(s.ball.pos, s.car.pos) ) for other in other_defenders): state = self.State.IDLE assert state # De-noise state self.state_history_counter[self.state_history.popleft()] -= 1 self.state_history_counter[state] += 1 self.state_history.append(state) state = self.state_history_counter.most_common(1)[0][0] if state != self.State.DODGING: self.ticks_in_dodge_state = 0 out = SimpleControllerState() def clamp_into_goal(desired_x): max_abs_x = GOAL_CENTER_TO_POST - 1.5*BALL_RADIUS return clamp(desired_x, -max_abs_x, max_abs_x) def forward_adjust(desired_x): return ( # PD-controller 1.0 * (desired_x - car.location.x) + 0.3 * (0 - car.velocity.x) ) def avoid_others_x(desired_x): for other in other_defenders: other_x = clamp_into_goal(other.pos[TO_STATUE])*1.001 self_x = s.car.pos[TO_STATUE] if abs(other_x - desired_x) < abs(self_x - desired_x): desired_x = other_x + copysign(BALL_RADIUS*4.0, self_x-other_x) # other_ratio = (desired_x - self_x) / (desired_x - other_x) # # if there is a car between us and the desired_x, then don't go through that car. # if 0 < other_ratio < 1: # desired_x = other_x + copysign(BALL_RADIUS*4.0, self_x-other_x) return desired_x if state == self.State.GROUND: # Drive to ball_intercept.physics.location.x out.throttle = forward_adjust(clamp_into_goal(avoid_others_x(ball_intercept.physics.location.x))) out.boost = out.throttle > 200. elif state == self.State.JUMPING: out.jump = True elif state == self.State.DODGING: self.ticks_in_dodge_state += 1 if self.ticks_in_dodge_state < 2: out.jump = False elif self.ticks_in_dodge_state == 3: dodge_point = lerp( struct_vector3_to_numpy(ball_intercept.physics.location), s.ball.pos, .5 # fudge ) out.pitch, out.yaw, out.roll = flip_towards( s, dodge_point ) # out.roll = 1 # out.pitch = -0.008*forward_adjust(ball_intercept.physics.location.x) # length = 1/sqrt(out.roll**2 + out.pitch**2) # out.roll *= length # out.pitch *= length # if out.roll < .4: # # if we really need to go forwards/back, don't go sideways at all # out.roll = 0 out.jump = True else: out.jump = False elif state == self.State.KICKOFF: target_pos = Vec3(copysign(GOAL_CENTER_TO_POST*0.8, s.car.pos[0]), desired_car_y, 0) # corner boost # self.renderer.draw_rect_3d(target_pos, 100, 100, True, self.renderer.pink(), True) forward = True out.steer = get_steer_towards(s, target_pos) out.throttle = -(s.car.pos[TO_ORANGE] - .99 * desired_car_y) + .1 * s.car.vel[TO_ORANGE] elif state == self.State.IDLE: desired_car_x = 0.4 * ( game_tick_packet.game_ball.physics.location.x + .5 * game_tick_packet.game_ball.physics.velocity.x ) # Back and forth to go to goal time_ratio = (game_tick_packet.game_info.seconds_elapsed) % 2 is_close_x = abs(car.location.x - desired_car_x) < 50 if time_ratio < .5: desired_car_x *= -1 desired_car_x = avoid_others_x(desired_car_x) desired_car_x = clamp_into_goal(desired_car_x) out.throttle = forward_adjust(desired_car_x) # Try to stay aligned with the x axis. car_yaw = zero_centered_angle(car.rotation.yaw) to_desired_car_y = desired_car_y - car.location.y desired_yaw = min(pi/3, max(-pi/3, 0.004 * to_desired_car_y)) if desired_car_x < car.location.x: desired_yaw *= -1 car_yaw_vel = car.angular_velocity.z out.steer = ( # PD-controller 5.0 * (desired_yaw - car_yaw) + 0.1 * (0 - car_yaw_vel) ) if out.throttle < 1: out.steer *= -1 if not car_obj.has_wheel_contact: desired_forward = normalize(Vec3( 1, .001 * (desired_car_y - car.location.y), 0 )) out.pitch, out.yaw, out.roll = get_pitch_yaw_roll( s.car, desired_forward, ) # asign stuff just for rendering. seconds_until_intercept = 2.5 ball_intercept.physics.location.x = desired_car_x ball_intercept.physics.location.z = 100 elif state == self.State.HIGH_JUMP: z = car.location.z out.jump = not (110 < z < 120) if car_obj.double_jumped: out.pitch = min(1, max(-1, 5*(.9-car.rotation.pitch))) elif state == self.State.HIGH_JUMP_GROUND: if seconds_until_intercept == 0: desired_vel_x = 0 # avoid division by zero else: desired_vel_x = (ball_intercept.physics.location.x - car.location.x) / seconds_until_intercept desired_vel_x *= 1.1 out.throttle = (desired_vel_x - car.velocity.x) else: self.logger.warn(f'invalid state {state}') self.render_ball_intercept(ball_intercept, seconds_until_intercept, state) out.throttle = clamp(out.throttle, -1, 1) out.steer = clamp(out.steer, -1, 1) out.steer = clamp11(out.steer) out.throttle = clamp11(out.throttle) out.pitch = clamp11(out.pitch) out.yaw = clamp11(out.yaw) out.roll = clamp11(out.roll) out.jump = bool(out.jump) out.boost = bool(out.boost) out.handbrake = bool(out.handbrake) return out
def get_output(self, packet: GameTickPacket) -> SimpleControllerState: s = EasyGameState(packet, self.team, self.index) gravity_z = packet.game_info.world_gravity_z # trace(mag(s.car.vel)) # Re plan our trajectory. if not s.car.on_ground: self.last_time_in_air = s.time if s.time - self.last_time_in_air > self.min_time_on_ground and s.time - self.last_replan_time > self.min_replan_period and packet.game_info.is_round_active: self.last_replan_time = s.time def pick_offset_target(predicted_slice): # pos = ball_pos(predicted_slice) # self.target_pos = self.get_target_pos(s) # pos += + 50 * UP # return pos future_ball = Ball(predicted_slice.physics) target_ball_pos = s.enemy_goal_center to_goal_dir = normalize(z0(target_ball_pos - future_ball.pos)) # DONE: predict the ball by some small amount. # DONE: avoid ball when coming back # DONE: hit at an angle to change ball velocity desired_ball_speed = MAX_CAR_SPEED desired_ball_vel = MAX_CAR_SPEED * to_goal_dir desired_ball_vel_change = desired_ball_vel - future_ball.vel normal_dir = -normalize( z0(desired_ball_vel)) # center of ball to hit point # alignment = dot(normal_dir, normalize(z0(s.car.pos - future_ball.pos))) # hit_radius = (0.9 if alignment > 0.7 else 1.5) * BALL_RADIUS hit_radius = 0.9 * BALL_RADIUS ball_hit_offset = hit_radius * normal_dir # ball_hit_offset = -0.8 * BALL_RADIUS * to_goal_dir target_pos = future_ball.pos + ball_hit_offset # Avoid the ball when coming back if dist(s.car.pos, target_ball_pos) < dist( future_ball.pos, target_ball_pos): # TODO: make sure options are in bounds avoid_back_ball_radius = BALL_RADIUS * 5.0 options = [ future_ball.pos + avoid_back_ball_radius * normalize(Vec3(1, -s.enemy_goal_dir, 0)), future_ball.pos + avoid_back_ball_radius * normalize(Vec3(-1, -s.enemy_goal_dir, 0)), ] best_avoid_option = min(options, key=lambda avoid_ball_pos: dist( s.car.pos, avoid_ball_pos)) # TODO: factor in current velocity maybe target_pos = best_avoid_option target_pos += 10.0 * UP # fudge return target_pos time_offset = 0.1 predicted_slice = bisect_ball_prediction( self.get_ball_prediction_struct(), lambda slice: mag( get_cannon_vel(s.car.pos, pick_offset_target( slice), slice.game_seconds - s.time - time_offset, s. gravity_z)) > MAX_CAR_SPEED) vel = get_cannon_vel(s.car.pos, pick_offset_target(predicted_slice), predicted_slice.game_seconds - s.time, s.gravity_z) # if mag(vel) > MAX_CAR_SPEED: # print(f'cannon was not powerful enough: {mag(vel)} > {MAX_CAR_SPEED}') self.set_game_state( GameState(cars={ self.index: CarState(physics=Physics( location=Vector3(*s.car.pos), rotation=Rotator( acos(mag(z0(vel)) / mag(vel)), atan2(vel[1], vel[0]), 0, ), velocity=Vector3(*vel), angular_velocity=Vector3(*(vel)), ), boost_amount=100) }, )) out = self.controller_state forward = s.car.vel + Vec3(0, 0, 0.1 * gravity_z) time_to_touchdown = max(solve_quadratic(.5 * gravity_z, s.car.vel[TO_CEILING], s.car.pos[TO_CEILING]), default=0) is_landing = time_to_touchdown < self.prepare_to_land_period or ( s.car.pos[TO_CEILING] < 100 and not s.car.vel[TO_CEILING] > 100) wall_sign = (-1 if s.car.pos[TO_STATUE] < 0 else 1) wall = 4096 * wall_sign if s.car.vel[TO_STATUE] == 0: time_to_wall = 10000 else: time_to_wall = (wall - s.car.pos[TO_STATUE]) / s.car.vel[TO_STATUE] if time_to_wall < 0: time_to_wall = 10000 time_to_ceiling = min([ t for t in solve_quadratic(.5 * gravity_z, s.car.vel[TO_CEILING], s.car.pos[TO_CEILING] - 2000) if t > 0 ], default=10000) if time_to_ceiling < self.prepare_to_land_period or s.car.pos[ TO_CEILING] > 1900: out.pitch, out.yaw, out.roll = get_pitch_yaw_roll( s.car, z0(forward), -UP, ) elif time_to_wall < self.prepare_to_land_period: out.pitch, out.yaw, out.roll = get_pitch_yaw_roll( s.car, Vec3(0, forward[TO_ORANGE], forward[TO_CEILING]), Vec3(-wall_sign, 0, 0), ) elif is_landing: out.pitch, out.yaw, out.roll = get_pitch_yaw_roll( s.car, z0(forward), UP, ) else: out.pitch, out.yaw, out.roll = get_pitch_yaw_roll( s.car, forward, normalize(lerp(s.car.up, cross(forward, s.car.up), 0.1)), ) out.boost = False out.throttle = 0 out.throttle = 1 if is_landing else 0 # Sanitize our output out.steer = clamp11(out.steer) out.throttle = clamp11(out.throttle) out.pitch = clamp11(out.pitch) out.yaw = clamp11(out.yaw) out.roll = clamp11(out.roll) out.jump = bool(out.jump) out.boost = bool(out.boost) out.handbrake = bool(out.handbrake) return out