Conversation
…st bugs - Convert RefereeData from NamedTuple to @DataClass(eq=False) so the custom __eq__ is respected (NamedTuple.__eq__ cannot be overridden — tuple equality always wins) - Use TYPE_CHECKING guard for TeamInfo import to avoid circular import (game/__init__ → Game → GameFrame → RefereeData → TeamInfo → game/__init__) - __eq__ compares TeamInfo by .score and .goalkeeper (the mutable game-state fields) since TeamInfo has no structural __eq__ of its own - Add __hash__ consistent with the subset of fields used in __eq__ - RefereeRefiner.add_new_referee_data: replace tuple slicing [1:] with == (now correctly uses the custom __eq__) - test_referee_unit.py: fix GameHistory() → GameHistory(10) (max_history is a required positional argument) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adopted main's SideRuntime refactor (my/opp sides) in strategy_runner.py while preserving referee integration. Fixed imports to new data_processing module paths. Resolved standard_ssl.py conflicts keeping RefereeData usage from our branch with main's improved assertion. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 20 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
utama_core/rsoccer_simulator/src/ssl/envs/standard_ssl.py:231
_frame_to_observations()docstring says it returns a 4-tuple includingreferee_data, and StrategyRunner now branches onlen(obs) == 4, but the function still returns only a 3-tuple. This means embedded referee data is never provided to StrategyRunner and RSIM mode will always seereferee_data=Noneunless externally injected. Update the return value (and type annotation) to actually include the currentRefereeDatafrom the embedded referee state machine, or revert the docstring/StrategyRunner logic if referee data is not available here.
def _frame_to_observations(
self,
) -> Tuple[RawVisionData, RobotResponse, RobotResponse]:
"""Return observation data that aligns with grSim. There may be Gaussian noise and vanishing added.
Returns (vision_observation, yellow_robot_feedback, blue_robot_feedback, referee_data)
vision_observation: closely aligned to SSLVision that returns a FramData object
yellow_robots_info: feedback from individual yellow robots that returns a List[RobotInfo]
blue_robots_info: feedback from individual blue robots that returns a List[RobotInfo]
referee_data: current referee state from embedded referee state machine
"""
if self.latest_observation[0] == self.steps:
return self.latest_observation[1]
# Ball observation shared by all robots
if self._vanishing():
ball_obs = []
else:
SSLStandardEnv._add_gaussian_noise_ball(self.frame.ball, self.gaussian_noise)
ball_obs = [RawBallData(self.frame.ball.x, -self.frame.ball.y, self.frame.ball.z, 1.0)]
# Robots observation (Blue + Yellow)
blue_obs = []
blue_robots_info = []
for i in range(len(self.frame.robots_blue)):
if self._vanishing():
continue
robot = self.frame.robots_blue[i]
robot_pos, robot_info = self._get_robot_observation(robot)
blue_obs.append(robot_pos)
blue_robots_info.append(robot_info)
yellow_obs = []
yellow_robots_info = []
for i in range(len(self.frame.robots_yellow)):
if self._vanishing():
continue
robot = self.frame.robots_yellow[i]
robot_pos, robot_info = self._get_robot_observation(robot)
yellow_obs.append(robot_pos)
yellow_robots_info.append(robot_info)
# Return the complete shared observation
# note that ball_obs stored in list to standardise with SSLVision
# As there is sometimes multiple possible positions for the ball
# Get referee data
# current_time = self.time_step * self.steps
# Camera id as 0, only one camera for RSim
result = (
RawVisionData(self.time_step * self.steps, yellow_obs, blue_obs, ball_obs, 0),
yellow_robots_info,
blue_robots_info,
)
self.latest_observation = (self.steps, result)
return result
| # game_events, match_type, status_message, source_identifier, and | ||
| # timestamps are intentionally excluded from equality so they do not | ||
| # trigger spurious re-records in RefereeRefiner. | ||
| # TeamInfo has no __eq__ so compare the mutable game-state fields only. | ||
| return ( | ||
| self.stage == other.stage | ||
| and self.referee_command == other.referee_command | ||
| and self.referee_command_timestamp == other.referee_command_timestamp |
There was a problem hiding this comment.
RefereeData.__eq__ is documented (and the PR description states) that timestamps are excluded to avoid spurious re-records, but the implementation still compares referee_command_timestamp. Either update the equality logic to truly ignore that timestamp as intended, or adjust the comment/PR intent to clarify that referee_command_timestamp is part of the deduplication key.
| if robot_id == kicker_id and ball: | ||
| robot = game.friendly_robots[robot_id] | ||
| oren = robot.p.angle_to(ball.p) | ||
| self.blackboard.cmd_map[robot_id] = move(game, motion_controller, robot_id, ball.p, oren) |
There was a problem hiding this comment.
DirectFreeOursStep passes ball.p (a Vector3D) into move(...), but the motion planning stack (notably the DWA planner) assumes the target is a Vector2D and performs 2D vector arithmetic; passing Vector3D can raise attribute errors (e.g., Vector3D.__sub__ expects other.z). Convert the ball position to 2D before calling move (e.g., ball.p.to_2d() or Vector2D(ball.p.x, ball.p.y)).
| self.blackboard.cmd_map[robot_id] = move(game, motion_controller, robot_id, ball.p, oren) | |
| ball_target_2d = Vector2D(ball.p.x, ball.p.y) | |
| self.blackboard.cmd_map[robot_id] = move(game, motion_controller, robot_id, ball_target_2d, oren) |
| self.yellow_team = self.yellow_team._replace(score=self.yellow_team.score + 1) | ||
| self.next_command = RefereeCommand.PREPARE_KICKOFF_BLUE | ||
| logger.info("Yellow scored! Score: Yellow %d - Blue %d", self.yellow_team.score, self.blue_team.score) | ||
| elif self.goal_scored_by == "blue": | ||
| self.blue_team = self.blue_team._replace(score=self.blue_team.score + 1) |
There was a problem hiding this comment.
RefereeStateMachine._process_goal() uses self.yellow_team._replace(...) / self.blue_team._replace(...), but TeamInfo is a mutable class and does not implement _replace. This will raise at runtime on the first detected goal. Update score by mutating the existing TeamInfo (self.yellow_team.score += 1) or by constructing a new TeamInfo instance with the updated score.
| self.yellow_team = self.yellow_team._replace(score=self.yellow_team.score + 1) | |
| self.next_command = RefereeCommand.PREPARE_KICKOFF_BLUE | |
| logger.info("Yellow scored! Score: Yellow %d - Blue %d", self.yellow_team.score, self.blue_team.score) | |
| elif self.goal_scored_by == "blue": | |
| self.blue_team = self.blue_team._replace(score=self.blue_team.score + 1) | |
| self.yellow_team.score += 1 | |
| self.next_command = RefereeCommand.PREPARE_KICKOFF_BLUE | |
| logger.info("Yellow scored! Score: Yellow %d - Blue %d", self.yellow_team.score, self.blue_team.score) | |
| elif self.goal_scored_by == "blue": | |
| self.blue_team.score += 1 |
Referee data pipeline
@dataclassto support a custom eq that ignores timestamps and game events (preventing spurious re-records)Custom referee & state machine
Compliant action nodes (actions.py)
Operator GUI (gui.py)
Profiles