Files
Apollo3D_SE/agent/agent.py

295 lines
10 KiB
Python
Raw Normal View History

2026-03-10 09:35:27 -04:00
from dataclasses import Field
import logging
from typing import Mapping
import numpy as np
from utils.math_ops import MathOps
2026-03-20 02:33:44 -04:00
from world.commons.field import FIFAField, HLAdultField, Soccer7vs7Field
2026-03-10 09:35:27 -04:00
from world.commons.play_mode import PlayModeEnum, PlayModeGroupEnum
logger = logging.getLogger()
class Agent:
"""
Responsible for deciding what the agent should do at each moment.
This class is called every simulation step to update the agent's behavior
based on the current state of the world and game conditions.
"""
BEAM_POSES: Mapping[type[Field], Mapping[int, tuple[float, float, float]]] ={
FIFAField: {
1: (2.1, 0, 0),
2: (22.0, 12.0, 0),
3: (22.0, 4.0, 0),
4: (22.0, -4.0, 0),
5: (22.0, -12.0, 0),
6: (15.0, 0.0, 0),
7: (4.0, 16.0, 0),
8: (11.0, 6.0, 0),
9: (11.0, -6.0, 0),
10: (4.0, -16.0, 0),
11: (7.0, 0.0, 0),
},
HLAdultField: {
1: (7.0, 0.0, 0),
2: (2.0, -1.5, 0),
3: (2.0, 1.5, 0),
2026-03-20 02:33:44 -04:00
},
Soccer7vs7Field: {
1: (2.1, 0, 0),
2: (22.0, 12.0, 0),
3: (22.0, 4.0, 0),
4: (22.0, -4.0, 0),
5: (22.0, -12.0, 0),
6: (15.0, 0.0, 0),
7: (4.0, 16.0, 0)
2026-03-10 09:35:27 -04:00
}
}
TEAMMATE_INFO_MAX_AGE: float = 0.8
SUPPORT_DISTANCE_FROM_BALL: float = 1.2
2026-03-10 09:35:27 -04:00
def __init__(self, agent):
"""
Creates a new DecisionMaker linked to the given agent.
Args:
agent: The main agent that owns this DecisionMaker.
"""
from agent.base_agent import Base_Agent # type hinting
self.agent: Base_Agent = agent
self.is_getting_up: bool = False
self.is_kicking: bool = False
self.current_shot_target_point: np.ndarray | None = None
self.target_point: np.ndarray = np.array([0.0, 0.0])
self.my_pos: np.ndarray = self.agent.world.global_position[:2]
2026-03-10 09:35:27 -04:00
def update_current_behavior(self) -> None:
"""
Chooses what the agent should do in the current step.
This function checks the game state and decides which behavior
or skill should be executed next.
"""
if self.agent.world.playmode is PlayModeEnum.GAME_OVER:
return
if self.agent.world.playmode_group in (
PlayModeGroupEnum.ACTIVE_BEAM,
PlayModeGroupEnum.PASSIVE_BEAM,
):
self.agent.server.commit_beam(
pos2d=self.BEAM_POSES[type(self.agent.world.field)][self.agent.world.number][:2],
rotation=self.BEAM_POSES[type(self.agent.world.field)][self.agent.world.number][2],
)
if self.is_getting_up or self.agent.skills_manager.is_ready(skill_name="GetUp"):
self.is_getting_up = not self.agent.skills_manager.execute(skill_name="GetUp")
elif self.agent.world.playmode is PlayModeEnum.PLAY_ON:
active_player_number = self.get_active_player_number()
if active_player_number == self.agent.world.number:
# self.carry_ball()
self.target_point = self.my_pos + np.array([4.0, 0.0])
self.kick_ball()
else:
# self.execute_support_behavior()
self.agent.skills_manager.execute("Neutral")
2026-03-10 09:35:27 -04:00
elif self.agent.world.playmode in (PlayModeEnum.BEFORE_KICK_OFF, PlayModeEnum.THEIR_GOAL, PlayModeEnum.OUR_GOAL):
self.agent.skills_manager.execute("Neutral")
else:
# self.execute_support_behavior()
2026-04-19 21:16:41 +08:00
self.kick_ball()
2026-03-10 09:35:27 -04:00
self.agent.robot.commit_motor_targets_pd()
def carry_ball(self):
"""
Basic example of a behavior: moves the robot toward the goal while handling the ball.
"""
target_point = self.target_point
2026-03-10 09:35:27 -04:00
ball_pos = self.agent.world.ball_pos[:2]
my_pos = self.agent.world.global_position[:2]
if self.kick_ball(target_point=target_point):
2026-03-10 09:35:27 -04:00
return
ball_to_target = target_point - ball_pos
bt_norm = np.linalg.norm(ball_to_target)
if bt_norm == 0:
return
ball_to_target_dir = ball_to_target / bt_norm
2026-03-10 09:35:27 -04:00
dist_from_ball_to_start_carrying = 0.30
carry_ball_pos = ball_pos - ball_to_target_dir * dist_from_ball_to_start_carrying
2026-03-10 09:35:27 -04:00
my_to_ball = ball_pos - my_pos
my_to_ball_norm = np.linalg.norm(my_to_ball)
if my_to_ball_norm == 0:
my_to_ball_dir = np.zeros(2)
else:
my_to_ball_dir = my_to_ball / my_to_ball_norm
cosang = np.dot(my_to_ball_dir, ball_to_target_dir)
2026-03-10 09:35:27 -04:00
cosang = np.clip(cosang, -1.0, 1.0)
angle_diff = np.arccos(cosang)
ANGLE_TOL = np.deg2rad(7.5)
aligned = (my_to_ball_norm > 1e-6) and (angle_diff <= ANGLE_TOL)
behind_ball = np.dot(my_pos - ball_pos, ball_to_target_dir) < 0
desired_orientation = MathOps.vector_angle(ball_to_target)
2026-03-10 09:35:27 -04:00
if not aligned or not behind_ball:
self.agent.skills_manager.execute(
"Walk",
target_2d=carry_ball_pos,
is_target_absolute=True,
orientation=None if np.linalg.norm(my_pos - carry_ball_pos) > 2 else desired_orientation
)
else:
self.agent.skills_manager.execute(
"Walk",
target_2d=target_point,
2026-03-10 09:35:27 -04:00
is_target_absolute=True,
orientation=desired_orientation
)
def kick_ball(self, target_point=None):
if target_point is None:
target_point = self.target_point
target_point = np.asarray(target_point, dtype=float)[:2]
ball_pos = self.agent.world.ball_pos[:2]
my_pos = self.agent.world.global_position[:2]
ball_to_target = target_point - ball_pos
ball_to_target_dist = np.linalg.norm(ball_to_target)
if ball_to_target_dist <= 1e-6:
return False
if self.is_kicking:
if self.current_shot_target_point is None:
self.current_shot_target_point = target_point
active_target = self.current_shot_target_point
active_distance = np.linalg.norm(active_target - ball_pos)
self.is_kicking = not self.agent.skills_manager.execute(
"Kick", distance=active_distance
)
if not self.is_kicking:
self.current_shot_target_point = None
return True
my_to_ball = ball_pos - my_pos
my_to_ball_dist = np.linalg.norm(my_to_ball)
ball_to_target_dir = ball_to_target / ball_to_target_dist
behind_ball = np.dot(my_pos - ball_pos, ball_to_target_dir) < 0
me_to_ball_dir = ball_pos - my_pos
me_to_ball_dir_norm = np.linalg.norm(me_to_ball_dir)
desired_orientation = MathOps.vector_angle(me_to_ball_dir)
current_orientation = self.agent.robot.global_orientation_euler[2]
orientation_error = abs(
MathOps.normalize_deg(desired_orientation - current_orientation)
)
lateral_error_to_shot_line, _ = MathOps.distance_point_to_line(
p=my_pos,
a=ball_pos,
b=target_point,
)
2026-04-19 21:16:41 +08:00
kick_offset = MathOps.rotate_2d_vec(np.array([0.0, -0.1]), desired_orientation)
close_to_ball = my_to_ball_dist <= 0.24
2026-04-19 21:16:41 +08:00
good_orientation = orientation_error <= 8.0
good_lateral_offset = lateral_error_to_shot_line <= 0.16
can_shoot = (
close_to_ball
and behind_ball
and good_orientation
and good_lateral_offset
)
if not can_shoot:
2026-04-19 21:16:41 +08:00
prepare_offset = 0.1
prepare_pos = ball_pos - ball_to_target_dir * prepare_offset + kick_offset
self.agent.skills_manager.execute(
"Walk",
target_2d=prepare_pos,
is_target_absolute=True,
orientation=desired_orientation,
)
return True
self.current_shot_target_point = target_point
self.is_kicking = not self.agent.skills_manager.execute(
"Kick", distance=ball_to_target_dist
)
if not self.is_kicking:
self.current_shot_target_point = None
return True
def get_active_player_number(self) -> int:
world = self.agent.world
ball_pos = world.ball_pos[:2]
server_time = world.server_time
my_num = world.number
my_pos = world.global_position[:2]
best_player_number = my_num
best_distance = np.linalg.norm(my_pos - ball_pos)
for teammate_number, teammate in enumerate(world.our_team_players, start=1):
if teammate_number == my_num:
continue
if teammate.last_seen_time is None or server_time is None:
continue
info_age = server_time - teammate.last_seen_time
if info_age > self.TEAMMATE_INFO_MAX_AGE:
continue
teammate_pos = teammate.position[:2]
teammate_distance = np.linalg.norm(teammate_pos - ball_pos)
if teammate_distance + 1e-6 < best_distance:
best_distance = teammate_distance
best_player_number = teammate_number
elif abs(teammate_distance - best_distance) <= 1e-6:
best_player_number = min(best_player_number, teammate_number)
return best_player_number
def execute_support_behavior(self) -> None:
world = self.agent.world
my_pos = world.global_position[:2]
ball_pos = world.ball_pos[:2]
target_point = self.target_point
ball_to_target = target_point - ball_pos
bt_norm = np.linalg.norm(ball_to_target)
if bt_norm <= 1e-6:
ball_to_target_dir = np.array([1.0, 0.0])
else:
ball_to_target_dir = ball_to_target / bt_norm
support_pos = ball_pos - ball_to_target_dir * self.SUPPORT_DISTANCE_FROM_BALL
support_orientation = MathOps.vector_angle(ball_pos - my_pos)
self.agent.skills_manager.execute(
"Walk",
target_2d=support_pos,
is_target_absolute=True,
orientation=support_orientation,
)