Skip to content

Referee integration#111

Open
isaac0804 wants to merge 7 commits intomainfrom
referee_integration
Open

Referee integration#111
isaac0804 wants to merge 7 commits intomainfrom
referee_integration

Conversation

@isaac0804
Copy link
Contributor

Referee data pipeline

  • Receive and parse complete SSL referee protobuf messages into RefereeData
  • RefereeRefiner records referee state changes
  • RefereeData converted from NamedTuple to @dataclass to support a custom eq that ignores timestamps and game events (preventing spurious re-records)

Custom referee & state machine

  • GameStateMachine tracks referee commands, stages, and transitions
  • 5 configurable auto-advance transitions (HALT→STOP, STOP→kickoff, kickoff→NORMAL, direct free→NORMAL, ball placement→next command)
  • 2-second sustained-readiness delay on game-starting advances for safety
  • All auto-advances disabled by default in physical environment profiles

Compliant action nodes (actions.py)

  • BallPlacementTheirsStep: active radial clearance — robots push out of the 0.55 m keep-out zone instead of freezing
  • PrepareKickoffTheirsStep: robots clamped to own half before kickoff
  • BallPlacementOursStep: two-phase placer motion (drive to ball → carry to target) with non-placer radial clearance and completion detection
  • PreparePenaltyOursStep / PreparePenaltyTheirsStep: non-kicker/non-keeper robots placed at touch-line boundary (SSL rule approximation)

Operator GUI (gui.py)

  • Browser-based panel served over HTTP + SSE at ~30 Hz
  • Displays live referee state, game score, stage, and command history
  • Config panel exposes all 5 auto-advance flags as toggleable ON/OFF pills
  • Integrates with the custom referee via attach_gui(referee, profile, port)

Profiles

  • arcade.yaml and strict_ai.yaml with explicit auto_advance blocks
  • AutoAdvanceConfig dataclass parsed from YAML game.auto_advance section

isaac0804 and others added 7 commits November 26, 2025 15:50
…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>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 including referee_data, and StrategyRunner now branches on len(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 see referee_data=None unless externally injected. Update the return value (and type annotation) to actually include the current RefereeData from 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

Comment on lines +60 to 67
# 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
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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)
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)).

Suggested change
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)

Copilot uses AI. Check for mistakes.
Comment on lines +193 to +197
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)
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants