Example #1
0
def main(x, y, s, rotation_step, num_iter):
    enable_drawing = False

    start_x = x
    start_y = y
    s.update_pos(m2d.Vector2(start_x, start_y), rotation=0)

    # Only simulate kick_short -> robot_rotation == kick direction and we want to simulate the kick direction - how a kick in this direction is
    # executed doesnt matter.
    no_action = a.Action("none", 0, 0, 0, 0)
    kick_short = a.Action("kick_short", 1080, 0, 0,
                          0)  # Maybe set std again to 150mm

    action_list = [no_action, kick_short]

    repetitions = num_iter
    best_times = []
    best_rotations = []

    for reps in range(repetitions):
        best_rotation = 0

        # Container for all times and corresponding rotations for one position
        times_single_position = np.array([])
        single_position_rot = np.array([])

        for rot in range(0, 360, rotation_step):
            # print("Start Rotation: " + str(rot))
            s.update_pos(m2d.Vector2(x, y), rotation=rot)
            num_kicks = 0
            num_turn_degrees = 0
            goal_scored = False
            choosen_rotation = 'none'  # This is used for deciding the rotation direction once per none decision
            total_time = 0

            while not goal_scored:

                actions_consequences = []
                # Simulate Consequences
                for action in action_list:
                    single_consequence = a.ActionResults([])
                    # a.num_particles can be 1 since there is no noise at all
                    actions_consequences.append(
                        Sim.simulate_consequences(action, single_consequence,
                                                  s, 1))

                # Decide best action
                best_action = Sim.decide_smart(actions_consequences, s)

                # expected_ball_pos should be in local coordinates for rotation calculations
                expected_ball_pos = actions_consequences[
                    best_action].expected_ball_pos_mean

                # Check if expected_ball_pos inside opponent goal
                opp_goal_back_right = m2d.Vector2(
                    field.opponent_goalpost_right.x + field.goal_depth,
                    field.opponent_goalpost_right.y)
                opp_goal_box = m2d.Rect2d(opp_goal_back_right,
                                          field.opponent_goalpost_left)

                goal_scored = opp_goal_box.inside(s.pose * expected_ball_pos)
                inside_field = field.field_rect.inside(s.pose *
                                                       expected_ball_pos)
                if goal_scored:
                    # print("Goal " + str(total_time) + " " + str(math.degrees(s.pose.rotation)))

                    # Apparently when using this weird things happen
                    # rotation = np.arctan2(expected_ball_pos.y, expected_ball_pos.x)
                    # rotation_time = np.abs(math.degrees(rotation) / s.rotation_vel)
                    # distance = np.hypot(expected_ball_pos.x, expected_ball_pos.y)
                    # distance_time = distance / s.walking_vel
                    # total_time += distance_time  #  + rotation_time
                    break

                elif not inside_field and not goal_scored:
                    # Asserts that expected_ball_pos is inside field or inside opp goal
                    # print("Error: This position doesn't manage a goal")
                    total_time = float('nan')
                    break

                elif not action_list[best_action].name == "none":
                    if enable_drawing is True:
                        draw_robot_walk(s, s.pose * expected_ball_pos,
                                        action_list[best_action].name)
                    # calculate the time needed
                    rotation = np.arctan2(expected_ball_pos.y,
                                          expected_ball_pos.x)
                    rotation_time = np.abs(
                        math.degrees(rotation) / s.rotation_vel)
                    distance = np.hypot(expected_ball_pos.x,
                                        expected_ball_pos.y)
                    distance_time = distance / s.walking_vel
                    total_time += distance_time + rotation_time

                    # reset the rotation direction
                    choosen_rotation = 'none'

                    # update the robots position
                    s.update_pos(s.pose * expected_ball_pos,
                                 math.degrees(s.pose.rotation + rotation))

                    num_kicks += 1

                elif action_list[best_action].name == "none":
                    if enable_drawing is True:
                        draw_robot_walk(s, s.pose * expected_ball_pos,
                                        action_list[best_action].name)
                    # Calculate rotation time
                    total_time += np.abs(5 / s.rotation_vel)

                    attack_direction = attack_dir.get_attack_direction(s)
                    attack_direction = math.degrees((attack_direction.angle()))

                    if (attack_direction > 0 and choosen_rotation is 'none'
                        ) or choosen_rotation is 'left':
                        s.update_pos(s.pose.translation,
                                     math.degrees(s.pose.rotation) +
                                     5)  # Should turn right
                        choosen_rotation = 'left'
                    elif (attack_direction <= 0 and choosen_rotation is 'none'
                          ) or choosen_rotation is 'right':
                        s.update_pos(s.pose.translation,
                                     math.degrees(s.pose.rotation) -
                                     5)  # Should turn left
                        choosen_rotation = 'right'
                    else:
                        print("Error at: " + str(s.pose.translation.x) +
                              " - " + str(s.pose.translation.y) + " - " +
                              str(math.degrees(s.pose.rotation)))
                        break

                    num_turn_degrees += 1

                else:
                    sys.exit("There should not be other actions")

            if not np.isnan(total_time):  # Maybe this already works
                times_single_position = np.append(times_single_position,
                                                  total_time)
                single_position_rot = np.append(single_position_rot, [rot])

        # end while not goal scored
        # TODO FIX nan issue!!!
        if len(times_single_position
               ) is 0:  # This can happen for positions on the field borders
            print("Every rotation would shoot out")
            best_times.append(float('nan'))
            best_rotations.append(best_rotation)
        else:
            min_val = times_single_position.min()
            min_idx = np.where(times_single_position == min_val)
            # best_rotation = np.mean(single_position_rot[min_idx])

            best_rotation = np.arctan2(
                np.sum(np.sin(np.deg2rad(single_position_rot[min_idx]))),
                np.sum(np.cos(np.deg2rad(single_position_rot[min_idx]))))
            best_rotation = np.rad2deg(best_rotation)
            best_times.append(np.min(times_single_position))
            best_rotations.append(best_rotation)
    # end all rotations
    print("Shortest Time: " + str(np.nanmean(best_times)) +
          " with global Rotation of robot: " + str(np.mean(best_rotations)) +
          " StartX " + str(start_x) + " Y: " + str(start_y))

    return np.mean(best_times), np.mean(best_rotations)
Example #2
0
def simulate_consequences(action, categorized_ball_positions, state,
                          num_particles):

    categorized_ball_positions.reset()

    # calculate the own goal line
    own_goal_dir = (field.own_goalpost_right -
                    field.own_goalpost_left).normalize()

    own_left_endpoint = field.own_goalpost_left + own_goal_dir * (
        field.goalpost_radius + field.ball_radius)
    own_right_endpoint = field.own_goalpost_right - own_goal_dir * (
        field.goalpost_radius + field.ball_radius)

    own_goal_line_global = m2d.LineSegment(own_left_endpoint,
                                           own_right_endpoint)

    # calculate opponent goal lines and box
    opp_goal_back_left = m2d.Vector2(
        field.opponent_goalpost_left.x + field.goal_depth,
        field.opponent_goalpost_left.y)
    opp_goal_back_right = m2d.Vector2(
        field.opponent_goalpost_right.x + field.goal_depth,
        field.opponent_goalpost_right.y)

    # Maybe add list of goal backsides here
    goal_backsides = ([])
    goal_backsides.append(
        m2d.LineSegment(opp_goal_back_left, opp_goal_back_right))
    goal_backsides.append(
        m2d.LineSegment(field.opponent_goalpost_left, opp_goal_back_left))
    goal_backsides.append(
        m2d.LineSegment(field.opponent_goalpost_right, opp_goal_back_right))

    opp_goal_box = m2d.Rect2d(opp_goal_back_right,
                              field.opponent_goalpost_left)

    # current ball position
    global_ball_start_position = state.pose * state.ball_position

    # virtual ultrasound obstacle line
    obstacle_line = m2d.LineSegment(state.pose * m2d.Vector2(400, 200),
                                    state.pose * m2d.Vector2(400, -200))

    mean_test_list_x = []
    mean_test_list_y = []

    # now generate predictions and categorize
    for i in range(0, num_particles):
        # predict and calculate shoot line
        global_ball_end_position = state.pose * action.predict(
            state.ball_position, True)

        shootline = m2d.LineSegment(global_ball_start_position,
                                    global_ball_end_position)

        # check if collision detection with goal has to be performed
        # if the ball start and end positions are inside of the field, you don't need to check
        collision_with_goal = False
        t_min = 0  # dummy value
        if not field.field_rect.inside(
                global_ball_end_position) or not field.field_rect.inside(
                    global_ball_start_position):
            t_min = shootline.length
            for side in goal_backsides:
                t = shootline.line_intersection(side)
                if 0 <= t < t_min and side.intersect(shootline):
                    t_min = t
                    collision_with_goal = True

        # if there are collisions with the back goal lines, calculate where the ball will stop
        if collision_with_goal:
            global_ball_end_position = shootline.point(t_min -
                                                       field.ball_radius)
            shootline = m2d.LineSegment(global_ball_start_position,
                                        global_ball_end_position)

        # Obstacle Detection
        obstacle_collision = False
        # TODO: fix obstacle collision somehow
        '''
        for obstacle in state.obstacle_list:
            dist = math.sqrt((state.pose.translation.x-obstacle.x)**2 + (state.pose.translation.y-obstacle.y)**2)
            # check for distance and rotation
            # Todo it's wrong: Now if obstacle is near, then obstacle is in front of the robot
            if dist < 400 and shootline.intersect(obstacle_line):
                obstacle_collision = True
        '''
        if opp_goal_box.inside(global_ball_end_position):
            category = Category.OPPGOAL
        elif obstacle_collision and obstacle_line.intersect(
                shootline) and shootline.intersect(obstacle_line):
            category = Category.COLLISION
        elif (field.field_rect.inside(global_ball_end_position) or
              (global_ball_end_position.x <= field.opponent_goalpost_right.x
               and field.opponent_goalpost_left.y > global_ball_end_position.y
               > field.opponent_goalpost_right.y)):
            category = Category.INFIELD
        elif shootline.intersect(
                own_goal_line_global) and own_goal_line_global.intersect(
                    shootline):
            category = Category.OWNGOAL
        elif global_ball_end_position.x > field.x_opponent_groundline:
            category = Category.OPPOUT
        elif global_ball_end_position.x < field.x_opponent_groundline:
            category = Category.OWNOUT
        elif global_ball_end_position.y > field.y_left_sideline:
            category = Category.LEFTOUT
        elif global_ball_end_position.y < field.y_right_sideline:
            category = Category.RIGHTOUT
        else:
            category = Category.INFIELD

        local_test_pos = state.pose / global_ball_end_position
        mean_test_list_x.append(local_test_pos.x)
        mean_test_list_y.append(local_test_pos.y)

        categorized_ball_positions.add(state.pose / global_ball_end_position,
                                       category)

    # calculate the most likely ball position in a separate simulation run
    categorized_ball_positions.expected_ball_pos_mean = m2d.Vector2(
        np.mean(mean_test_list_x), np.mean(mean_test_list_y))
    categorized_ball_positions.expected_ball_pos_median = m2d.Vector2(
        np.median(mean_test_list_x), np.median(mean_test_list_y))
    return categorized_ball_positions
Example #3
0
x_own_goal = -x_opponent_goal
x_opponent_penalty_area = x_opponent_groundline - x_penalty_area_length
x_own_penalty_area = -x_opponent_penalty_area

y_left_goalpost = goal_width / 2.0
y_right_goalpost = -y_left_goalpost

y_left_penalty_area = y_penalty_area_length / 2.0
y_right_penalty_area = -y_left_penalty_area

y_left_sideline = y_length / 2.0
y_right_sideline = -y_left_sideline

opponent_goalpost_left = m2d.Vector2(x_opponent_goal+25, y_left_goalpost)
opponent_goalpost_right = m2d.Vector2(x_opponent_goal+25, y_right_goalpost)

own_goalpost_left = m2d.Vector2(x_own_goal-25, y_left_goalpost)
own_goalpost_right = m2d.Vector2(x_own_goal-25, y_right_goalpost)

# From Simulation.cpp
opp_goal_back_left  = m2d.Vector2(opponent_goalpost_left.x + goal_depth, opponent_goalpost_left.y)
opp_goal_back_right = m2d.Vector2(opponent_goalpost_right.x + goal_depth, opponent_goalpost_right.y)
opp_goal_box = m2d.Rect2d(opp_goal_back_right, opponent_goalpost_left)


own_goal_back_left  = m2d.Vector2(own_goalpost_left.x - goal_depth, own_goalpost_left.y)
own_goal_back_right = m2d.Vector2(own_goalpost_right.x - goal_depth, own_goalpost_right.y)
own_goal_box = m2d.Rect2d(own_goal_back_right, own_goalpost_left)

field_rect = m2d.Rect2d(m2d.Vector2(-x_length*0.5, -y_length*0.5), m2d.Vector2(x_length*0.5, y_length*0.5))
Example #4
0
def main(x, y, rot, s, num_iter):
    s.update_pos(m2d.Vector2(x, y), rotation=rot)

    no_action = a.Action("none", 0, 0, 0, 0)
    kick_short = a.Action("kick_short", 1280, 0, 0, 0)
    sidekick_left = a.Action("sidekick_left", 750, 0, 90, 0)
    sidekick_right = a.Action("sidekick_right", 750, 0, -90, 0)

    action_list = [no_action, kick_short, sidekick_left, sidekick_right]

    # Do several decision cycles not just one to get rid of accidental decisions
    timings = []
    n_kicks = []
    n_turns = []
    for idx in range(num_iter):
        num_kicks = 0
        num_turn_degrees = 0
        goal_scored = False
        total_time = 0
        choosen_rotation = 'none'  # This is used for deciding the rotation direction once per none decision
        s.update_pos(m2d.Vector2(x, y), rotation=rot)
        while not goal_scored:
            actions_consequences = []
            # Simulate Consequences
            for action in action_list:
                single_consequence = a.ActionResults([])
                actions_consequences.append(
                    Sim.simulate_consequences(action, single_consequence, s,
                                              a.num_particles))

            # Decide best action
            best_action = Sim.decide_smart(actions_consequences, s)

            # expected_ball_pos should be in local coordinates for rotation calculations
            expected_ball_pos = actions_consequences[
                best_action].expected_ball_pos_mean

            # Check if expected_ball_pos inside opponent goal
            opp_goal_back_right = m2d.Vector2(
                field.opponent_goalpost_right.x + field.goal_depth,
                field.opponent_goalpost_right.y)
            opp_goal_box = m2d.Rect2d(opp_goal_back_right,
                                      field.opponent_goalpost_left)

            goal_scored = opp_goal_box.inside(s.pose * expected_ball_pos)
            inside_field = field.field_rect.inside(s.pose * expected_ball_pos)
            if goal_scored:
                num_kicks += 1
                # print("Goal " + str(total_time) + " " + str(math.degrees(s.pose.rotation)))
                break

            elif not inside_field and not goal_scored:
                # Asserts that expected_ball_pos is inside field or inside opp goal
                # print("Error: This position doesn't manage a goal")
                total_time = float('nan')
                break

            elif not action_list[best_action].name == "none":

                # draw_robot_walk(actions_consequences, state, state.pose * expected_ball_pos, action_list[best_action].name)

                # calculate the time needed
                rotation = np.arctan2(expected_ball_pos.y, expected_ball_pos.x)
                rotation_time = np.abs(m.degrees(rotation) / s.rotation_vel)
                distance = np.hypot(expected_ball_pos.x, expected_ball_pos.y)
                distance_time = distance / s.walking_vel
                total_time += distance_time + rotation_time

                # update total turns
                num_turn_degrees += np.abs(m.degrees(rotation))

                # reset the rotation direction
                choosen_rotation = 'none'

                # update the robots position
                s.update_pos(s.pose * expected_ball_pos,
                             m.degrees(s.pose.rotation + rotation))
                num_kicks += 1

            elif action_list[best_action].name == "none":
                # print(str(state.pose * expected_ball_pos) + " Decision: " + str(action_list[best_action].name))
                # draw_robot_walk(actions_consequences, state, state.pose * expected_ball_pos, action_list[best_action].name)
                turn_rotation_step = 5
                # Calculate rotation time
                total_time += np.abs(turn_rotation_step / s.rotation_vel)

                attack_direction = attack_dir.get_attack_direction(s)

                if (attack_direction > 0 and choosen_rotation
                    ) is 'none' or choosen_rotation is 'left':
                    s.update_pos(s.pose.translation,
                                 m.degrees(s.pose.rotation) +
                                 turn_rotation_step)  # Should turn right
                    choosen_rotation = 'left'
                elif (attack_direction <= 0 and choosen_rotation is 'none'
                      ) or choosen_rotation is 'right':
                    s.update_pos(s.pose.translation,
                                 m.degrees(s.pose.rotation) -
                                 turn_rotation_step)  # Should turn left
                    choosen_rotation = 'right'

                num_turn_degrees += turn_rotation_step
            else:
                sys.exit("There should not be other actions")
        # print("Total time to goal: " + str(total_time))
        # print("Num Kicks: " + str(num_kicks))
        # print("Num Turns: " + str(num_turn_degrees))

        timings.append(total_time)
        n_kicks.append(num_kicks)
        n_turns.append(num_turn_degrees)

    return np.nanmean(timings), np.nanmean(n_kicks), np.nanmean(n_turns)
Example #5
0
x_own_groundline = -x_opponent_groundline
x_opponent_goal = x_opponent_groundline

x_own_goal = -x_opponent_goal
x_opponent_penalty_area = x_opponent_groundline - x_penalty_area_length
x_own_penalty_area = -x_opponent_penalty_area

y_left_goalpost = goal_width / 2.0
y_right_goalpost = -y_left_goalpost

y_left_penalty_area = y_penalty_area_length / 2.0
y_right_penalty_area = -y_left_penalty_area

y_left_sideline = y_length / 2.0
y_right_sideline = -y_left_sideline

opponent_goalpost_left = m2d.Vector2(x_opponent_goal + 25, y_left_goalpost)
opponent_goalpost_right = m2d.Vector2(x_opponent_goal + 25, y_right_goalpost)

own_goalpost_left = m2d.Vector2(x_own_goal - 25, y_left_goalpost)
own_goalpost_right = m2d.Vector2(x_own_goal - 25, y_right_goalpost)

# From Simulation.cpp
opp_goal_back_left = m2d.Vector2(opponent_goalpost_left.x + goal_depth,
                                 opponent_goalpost_left.y)
opp_goal_back_right = m2d.Vector2(opponent_goalpost_right.x + goal_depth,
                                  opponent_goalpost_right.y)

field_rect = m2d.Rect2d(m2d.Vector2(-x_length * 0.5, -y_length * 0.5),
                        m2d.Vector2(x_length * 0.5, y_length * 0.5))
def simulate_goal_cycle(variation=False):
    state = State()
    sim_data = [copy.deepcopy(state)]

    no_action = a.Action("none", 0, 0, 0, 0)
    kick_short = a.Action("kick_short", 780, 150, 8.454482265522328, 6.992268841997358)
    sidekick_left = a.Action("sidekick_left", 750, 150, 86.170795364136380, 10.669170653645670)
    sidekick_right = a.Action("sidekick_right", 750, 150, -89.657943335302260, 10.553726275058064)

    action_list = [no_action, kick_short, sidekick_left, sidekick_right]

    # Todo: Maybe do several decision cycles not just one to get rid of accidental decisions
    num_kicks = 0
    num_turn_degrees = 0
    goal_scored = False

    while not goal_scored:

        actions_consequences = []
        # Simulate Consequences
        for action in action_list:
            single_consequence = a.ActionResults([])
            actions_consequences.append(Sim.simulate_consequences(action, single_consequence, state, a.num_particles))

        # Decide best action
        best_action = Sim.decide_smart(actions_consequences, state)

        # state.next_action = best_action #not next but last action
        sim_data[len(sim_data)-1].next_action = best_action

        # expected_ball_pos should be in local coordinates for rotation calculations
        expected_ball_pos = actions_consequences[best_action].expected_ball_pos_mean

        if variation is True:
            expected_ball_pos = var_kick(best_action, expected_ball_pos)

        # Check if expected_ball_pos inside opponent goal
        opp_goal_back_right = m2d.Vector2(field.opponent_goalpost_right.x + field.goal_depth,
                                          field.opponent_goalpost_right.y)
        opp_goal_box = m2d.Rect2d(opp_goal_back_right, field.opponent_goalpost_left)

        goal_scored = opp_goal_box.inside(state.pose * expected_ball_pos)
        inside_field = field.field_rect.inside(state.pose * expected_ball_pos)

        # Assert that expected_ball_pos is inside field or inside opp goal
        if not inside_field and not goal_scored:
            sim_data.append(copy.deepcopy(state))
            print("Error")
            # For histogram -> note the this position doesnt manage a goal
            break

        if not action_list[best_action].name == "none":

            print(str(state.pose * expected_ball_pos) + " Decision: " + str(action_list[best_action].name))

            # update the robots position
            rotation = np.arctan2(expected_ball_pos.y, expected_ball_pos.x)
            print(math.degrees(rotation))
            state.update_pos(state.pose * expected_ball_pos, state.pose.rotation + rotation)
            sim_data.append(copy.deepcopy(state))

            num_kicks += 1

        elif action_list[best_action].name == "none":
            print(str(state.pose * expected_ball_pos) + " Decision: " + str(action_list[best_action].name))

            attack_direction = attack_dir.get_attack_direction(state)
            # Todo: can run in a deadlock for some reason
            if attack_direction > 0:
                state.update_pos(state.pose.translation, state.pose.rotation + math.radians(10))  # Should be turn right
                # state.pose.rotation += math.radians(10)  # Should be turn right
                print("Robot turns right - global rotation turns left")

            else:
                state.update_pos(state.pose.translation, state.pose.rotation - math.radians(10))  # Should be turn left
                # state.pose.rotation -= math.radians(10)  # Should be turn left
                print("Robot turns left - global rotation turns right")

            sim_data.append(copy.deepcopy(state))

            num_turn_degrees += 1

    # print("Num Kicks: " + str(num_kicks))
    # print("Num Turns: " + str(num_turn_degrees))

    return sim_data
Example #7
0
def main(x, y, rot, s, num_iter):
    no_action = a.Action("none", 0, 0, 0, 0)
    kick_short = a.Action("kick_short", 1280, 0, 0, 0)
    sidekick_left = a.Action("sidekick_left", 750, 0, 90, 0)
    sidekick_right = a.Action("sidekick_right", 750, 0, -90, 0)
    action_list = [no_action, kick_short, sidekick_left, sidekick_right]

    s.update_pos(m2d.Vector2(x, y), rotation=rot)  # rot is in degrees

    # Do several decision cycles not just one to get rid of accidental decisions
    timings = []
    n_kicks = []
    n_turns = []
    for idx in range(num_iter):
        num_kicks = 0
        num_turn_degrees = 0
        goal_scored = False
        total_time = 0
        s.update_pos(m2d.Vector2(x, y), rotation=rot)
        while not goal_scored:
            # Change Angle of all actions according to the particle filter
            # best_dir is the global rotation for that kick
            best_dir = 360
            best_action = 0
            for ix, action in enumerate(action_list):
                if action.name is "none":
                    continue
                tmp, _ = calculate_best_direction(s, action_list[ix], False, iterations=20)
                # print("Best dir: " + str(math.degrees(tmp)) + " for action: " + action_list[idx].name)
                if np.abs(tmp) < np.abs(best_dir):
                    best_dir = tmp
                    best_action = ix
            # print("Best dir: " + str(math.degrees(best_dir)) + " for action: " + action_list[best_action].name)

            # Rotate the robot so that the shooting angle == best_dir
            s.pose.rotation = s.pose.rotation + best_dir
            # only model turning when it's significant
            if np.abs(best_dir) > 5:
                total_time += np.abs(math.degrees(best_dir) / s.rotation_vel)
                num_turn_degrees += np.abs(math.degrees(best_dir))

            # after turning evaluate the best action again to calculate the expected ball position
            actions_consequences = []
            single_consequence = a.ActionResults([])
            actions_consequences.append(Sim.simulate_consequences(action_list[best_action], single_consequence, s, a.num_particles))

            # expected_ball_pos should be in local coordinates for rotation calculations
            expected_ball_pos = actions_consequences[0].expected_ball_pos_mean

            # Check if expected_ball_pos inside opponent goal
            opp_goal_back_right = m2d.Vector2(field.opponent_goalpost_right.x + field.goal_depth,
                                              field.opponent_goalpost_right.y)
            opp_goal_box = m2d.Rect2d(opp_goal_back_right, field.opponent_goalpost_left)

            goal_scored = opp_goal_box.inside(s.pose * expected_ball_pos)
            inside_field = field.field_rect.inside(s.pose * expected_ball_pos)

            # Assert that expected_ball_pos is inside field or inside opp goal
            if not inside_field and not goal_scored:
                # print("Error: This position doesn't manage a goal")
                # total_time = float('nan')
                # Maybe still treat it as goal since it's some weird issue with particle not inside the goal which screws up the mean
                break

            # calculate the time needed
            rotation = np.arctan2(expected_ball_pos.y, expected_ball_pos.x)
            rotation_time = np.abs(math.degrees(rotation) / s.rotation_vel)
            distance = np.hypot(expected_ball_pos.x, expected_ball_pos.y)
            distance_time = distance / s.walking_vel

            total_time += distance_time + rotation_time

            # update the robots position
            s.update_pos(s.pose * expected_ball_pos, math.degrees(s.pose.rotation + rotation))
            num_turn_degrees += np.abs(math.degrees(rotation))
            num_kicks += 1

        timings.append(total_time)
        n_kicks.append(num_kicks)
        n_turns.append(num_turn_degrees)

    return np.nanmin(timings), np.nanmean(n_kicks), np.nanmean(n_turns)