Skip to content

Referee integration#111

Open
isaac0804 wants to merge 19 commits intomainfrom
referee_integration
Open

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

Conversation

@isaac0804
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.
@isaac0804
Copy link
Copy Markdown
Contributor Author

Referee integration

  • Added full referee data plumbing into the game pipeline: complete SSL referee protobuf messages are parsed into RefereeData, recorded by RefereeRefiner, and exposed to strategies through the game state
  • RefereeData was converted to a @dataclass with custom equality so timestamps and non-behavioral fields do not cause noisy re-recording

Custom referee

  • Added an in-process CustomReferee with a GameStateMachine that works across rsim, grsim, and real
  • Added explicit referee source selection in StrategyRunner: none, official, or custom
  • Ported built-in profiles and renamed them to:
    • simulation for auto-progressing simulator / RL workflows
    • human for operator-controlled physical / testing workflows
  • Added configurable auto-advance behavior for queued restarts, including kickoff, direct free, penalty, and ball placement

Referee override behavior

  • Added the referee override tree and action nodes so strategies comply with referee commands like HALT, STOP, kickoff, penalty, direct free, and ball placement
  • Fixed key legality behaviors:
    • active clearance during STOP and opponent restarts
    • two-phase ball placement for our team
    • teammate clearance during our ball placement
    • clearance from both ball and designated target during opponent ball placement
    • kickoff and penalty positioning now scale correctly with field size and side assignment

Operator UX

  • Added a browser-based custom-referee GUI over HTTP/SSE for command control and state inspection
  • Live terminal status now shows richer referee context, including command, next command, referee source, and custom-referee profile
  • Custom-referee status messages are now preserved and shown to the operator so stoppages include a visible reason

Geometry / configurability

  • Removed hardcoded referee-action geometry where practical and moved shared distances into constants or live field-derived helpers
  • Kickoff and penalty formations now work on variable field sizes instead of assuming the standard field

Tests

  • Added focused unit/integration coverage for:
    • referee source selection in StrategyRunner
    • custom-referee profile loading and restart progression
    • referee action-node legality and field-scaling behavior
    • status-message propagation through the custom-referee path

isaac0804 and others added 6 commits April 3, 2026 11:43
- Add rsim integration tests for ball placement, direct free kick (ours
  and theirs), and kickoff positioning in test_referee_rsim.py
- Add 15 unit tests covering PrepareKickoffTheirsStep, DirectFreeOursStep,
  and DirectFreeTheirsStep action nodes in test_referee_unit.py
- Switch demo script control_scheme from dwa to pid

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace Field._UNDERSCORE_CONSTANTS with their public ClassProperty
equivalents in math_utils.py and geometry.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ument future work

- conftest.py: default --headless to True so tests don't open rsim window
- strategy_runner.py: skip ball teleport on STOP when next_command is
  BALL_PLACEMENT so the robot must physically carry the ball
- test_referee_rsim.py: replace broken full-sequence test with a comment
  documenting why it is deferred (ball placement carry mechanics not yet
  reliable in rsim)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Penalty and ball placement buttons are not in use; removing them keeps
the operator panel focused on the commands we actually use.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Ball placement phase before free kick per SSL rulebook
- BallPlacementOursStep carry mechanics investigation (two-robot kissing)
- GUI suggested next action to reduce operator cognitive load

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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