Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
9720dc1
feat: referee support in rsim
isaac0804 Nov 26, 2025
4cff935
hotfix: self.name bug
isaac0804 Nov 26, 2025
ac7ffb0
Merge branch 'main' into referee_integration
isaac0804 Feb 18, 2026
3f6b0d9
feat: receive complete referee data and use it in strategy runner
isaac0804 Feb 18, 2026
27850b1
feat: referee command nodes and test
isaac0804 Feb 18, 2026
ec2558b
fix: make RefereeData.__eq__ actually override tuple equality; fix te…
isaac0804 Mar 13, 2026
08af954
merge: resolve conflicts from main into referee_integration
isaac0804 Mar 13, 2026
769a8a6
Add custom referee integration and profile rename
isaac0804 Mar 31, 2026
5c0b05a
Fix referee runner startup and keep-out behavior
isaac0804 Mar 31, 2026
626ef22
Scale referee actions with field geometry
isaac0804 Mar 31, 2026
d4adaeb
Fix referee restart progression and clearance
isaac0804 Mar 31, 2026
a0b120d
Fix custom referee demo wiring and status messages
isaac0804 Mar 31, 2026
2250a92
Show referee source details in live status
isaac0804 Mar 31, 2026
5d757c1
Add referee behaviour integration tests and unit test coverage
isaac0804 Apr 3, 2026
b683f39
Use public Field class properties instead of private constants
isaac0804 Apr 3, 2026
f6f1297
Default headless=True, skip ball teleport when placement pending, doc…
isaac0804 Apr 3, 2026
aa4974a
Document end-to-end ball placement test as future work
isaac0804 Apr 3, 2026
15aa391
Remove advanced controls (penalty & ball placement) from referee GUI
isaac0804 Apr 3, 2026
c148b34
Add three future work items to referee integration docs
isaac0804 Apr 3, 2026
cb6b176
Fix three bugs identified in Copilot review
isaac0804 Apr 7, 2026
5282cc2
Update utama_core/custom_referee/state_machine.py
isaac0804 Apr 7, 2026
cc1b187
Address second round of Copilot review comments
isaac0804 Apr 7, 2026
9d6b8ea
fix: stage_time_left now updates every frame in RefereeRefiner
isaac0804 Apr 14, 2026
6c265ab
fix: increase BALL_KEEP_OUT_DISTANCE to 0.8 m to prevent restart viol…
isaac0804 Apr 14, 2026
3125309
Merge origin/main into referee_integration
isaac0804 Apr 14, 2026
2040685
fix: adapt referee code to FieldDimensions API and arbitrary field sizes
isaac0804 Apr 14, 2026
3d170b8
fix: use own-half fallback direction when robot is coincident with ball
isaac0804 Apr 14, 2026
04288e5
feat: make StrategyRunner the single source of truth for referee geom…
isaac0804 Apr 14, 2026
fe1c490
refactor: remove RefereeGeometry.from_standard_div_b()
isaac0804 Apr 14, 2026
82051b9
refactor: rename half_defense_length → half_defense_depth in RefereeG…
isaac0804 Apr 14, 2026
4869ca9
fix: remove dead CLEARANCE_FALLBACK_DIRECTION and fix _clear_to_legal…
isaac0804 Apr 15, 2026
d6f4f11
fix: exclude PREPARE_KICKOFF/PENALTY from keep-out rule to prevent se…
isaac0804 Apr 15, 2026
b3a02bd
Revert "fix: exclude PREPARE_KICKOFF/PENALTY from keep-out rule to pr…
isaac0804 Apr 15, 2026
3bb31bc
fix: clear encroaching robots from current position, not formation ta…
isaac0804 Apr 15, 2026
ea33066
fix: use BALL_KEEP_OUT_DISTANCE in _ensure_outside_center_circle
isaac0804 Apr 15, 2026
386dd94
fix: address Copilot review issues in referee code and docs
isaac0804 Apr 16, 2026
ea8514b
fix: address Copilot review issues in referee actions and tests
isaac0804 Apr 16, 2026
530cd0b
fix: keep RefereeRefiner properties current when deduplication skips …
isaac0804 Apr 16, 2026
7707973
docs: rename half_defense_length → half_defense_depth in all docs
isaac0804 Apr 16, 2026
e5f381b
docs: remove non-existent exhibition.yaml from file structure
isaac0804 Apr 16, 2026
aceeb8e
Merge branch 'main' into referee_integration
energy-in-joles May 10, 2026
0c65a12
bug fix for pytest and add referee example in main
energy-in-joles May 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions start_test_env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,17 @@ if [ $? -ne 0 ]; then
fi

# Change to the ssl-game-controller directory and run the game controller, suppressing output
echo "Starting game controller..."
cd ssl-game-controller/
./ssl-game-controller > /dev/null 2>&1 &
GAME_CONTROLLER_PID=$!
cd ..
# echo "Starting game controller..."
# cd ssl-game-controller/
# ./ssl-game-controller > /dev/null 2>&1 &
# GAME_CONTROLLER_PID=$!
# cd ..

# Check if the game controller started successfully
if [ $? -ne 0 ]; then
echo "Failed to start game controller. Exiting."
cleanup
fi
# if [ $? -ne 0 ]; then
# echo "Failed to start game controller. Exiting."
# cleanup
# fi

# Change to the AutoReferee directory and run the run.sh script, suppressing output
echo "Starting AutoReferee..."
Expand Down
3 changes: 3 additions & 0 deletions utama_core/data_processing/receivers/referee_receiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ def _update_data(self, referee_packet: Referee) -> None:
if referee_packet.HasField("current_action_time_remaining")
else None
),
game_events=list(referee_packet.game_events),
match_type=referee_packet.match_type,
status_message=referee_packet.status_message if referee_packet.status_message else None,
)

# add to referee buffer
Expand Down
28 changes: 22 additions & 6 deletions utama_core/data_processing/refiners/referee.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dataclasses
from typing import Optional

from utama_core.data_processing.refiners.base_refiner import BaseRefiner
Expand All @@ -8,15 +9,30 @@


class RefereeRefiner(BaseRefiner):
def refine(self, game, data):
return game

def __init__(self):
self._referee_records = []

def refine(self, game_frame, data: Optional[RefereeData]):
"""Process referee data and update the game frame.

Args:
game_frame: Current GameFrame object
data: Referee data to process (None if no referee)

Returns:
Updated GameFrame with referee data attached, or the original frame if data is None
"""
if data is None:
return game_frame

# Add to history
self.add_new_referee_data(data)

# Return a new GameFrame with referee data injected
return dataclasses.replace(game_frame, referee=data)

def add_new_referee_data(self, referee_data: RefereeData) -> None:
if not self._referee_records:
self._referee_records.append(referee_data)
elif referee_data[1:] != self._referee_records[-1][1:]:
if not self._referee_records or referee_data != self._referee_records[-1]:
self._referee_records.append(referee_data)

def source_identifier(self) -> Optional[str]:
Expand Down
48 changes: 42 additions & 6 deletions utama_core/entities/data/referee.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
from typing import NamedTuple, Optional, Tuple
from __future__ import annotations

from dataclasses import dataclass, field
from typing import TYPE_CHECKING, List, Optional, Tuple

from utama_core.entities.game.team_info import TeamInfo
from utama_core.entities.referee.referee_command import RefereeCommand
from utama_core.entities.referee.stage import Stage

if TYPE_CHECKING:
from utama_core.entities.game.team_info import TeamInfo


class RefereeData(NamedTuple):
"""Namedtuple for referee data."""
@dataclass(eq=False)
class RefereeData:
"""Dataclass for referee data."""

source_identifier: Optional[str]
time_sent: float
Expand Down Expand Up @@ -36,17 +42,47 @@ class RefereeData(NamedTuple):
# * ball placement
current_action_time_remaining: Optional[int] = None

# All game events detected since the last RUNNING state (e.g. foul type, ball-out side).
# Stored as raw protobuf GameEvent objects. Cleared when the game resumes.
# Useful for logging and future decision-making; not required for basic compliance.
game_events: List = field(default_factory=list)

# Meta information about the match type:
# 0 = UNKNOWN_MATCH, 1 = GROUP_PHASE, 2 = ELIMINATION_PHASE, 3 = FRIENDLY
match_type: int = 0

# Human-readable message from the referee UI (e.g. reason for a stoppage).
status_message: Optional[str] = None

def __eq__(self, other):
if not isinstance(other, RefereeData):
return NotImplemented
# 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
Comment thread
isaac0804 marked this conversation as resolved.
Outdated
and self.yellow_team == other.yellow_team
and self.blue_team == other.blue_team
and self.yellow_team.score == other.yellow_team.score
Comment thread
isaac0804 marked this conversation as resolved.
Outdated
and self.yellow_team.goalkeeper == other.yellow_team.goalkeeper
and self.blue_team.score == other.blue_team.score
and self.blue_team.goalkeeper == other.blue_team.goalkeeper
and self.designated_position == other.designated_position
Comment thread
isaac0804 marked this conversation as resolved.
and self.blue_team_on_positive_half == other.blue_team_on_positive_half
Comment thread
isaac0804 marked this conversation as resolved.
and self.next_command == other.next_command
and self.current_action_time_remaining == other.current_action_time_remaining
)
Comment thread
isaac0804 marked this conversation as resolved.

def __hash__(self):
return hash(
(
self.stage,
self.referee_command,
self.referee_command_timestamp,
self.designated_position,
self.blue_team_on_positive_half,
self.next_command,
)
)
1 change: 1 addition & 0 deletions utama_core/entities/game/current_game_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def __init__(self, game: GameFrame):
object.__setattr__(self, "friendly_robots", game.friendly_robots)
object.__setattr__(self, "enemy_robots", game.enemy_robots)
object.__setattr__(self, "ball", game.ball)
object.__setattr__(self, "referee", game.referee)
object.__setattr__(self, "robot_with_ball", self._set_robot_with_ball(game))
object.__setattr__(self, "proximity_lookup", self._init_proximity_lookup(game))

Expand Down
4 changes: 4 additions & 0 deletions utama_core/entities/game/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,7 @@ def robot_with_ball(self):
@property
def proximity_lookup(self):
return self.current.proximity_lookup

@property
def referee(self):
return self.current.referee
2 changes: 2 additions & 0 deletions utama_core/entities/game/game_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from dataclasses import dataclass
from typing import Dict, Optional

from utama_core.entities.data.referee import RefereeData
from utama_core.entities.game.ball import Ball
from utama_core.entities.game.field import Field
from utama_core.entities.game.robot import Robot
Expand All @@ -18,6 +19,7 @@ class GameFrame:
friendly_robots: Dict[int, Robot]
enemy_robots: Dict[int, Robot]
ball: Optional[Ball]
referee: Optional[RefereeData] = None

def is_ball_in_goal(self, right_goal: bool) -> bool:
ball_pos = self.ball.p
Expand Down
4 changes: 0 additions & 4 deletions utama_core/entities/referee/stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@ def from_id(stage_id: int):
except ValueError:
raise ValueError(f"Invalid stage ID: {stage_id}")

@property
def name(self):
return self.name

@property
def stage_id(self):
return self.value
7 changes: 6 additions & 1 deletion utama_core/rsoccer_simulator/src/ssl/envs/standard_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
)
from utama_core.entities.data.command import RobotResponse
from utama_core.entities.data.raw_vision import RawBallData, RawRobotData, RawVisionData
from utama_core.entities.data.referee import RefereeData
from utama_core.global_utils.math_utils import (
deg_to_rad,
normalise_heading_deg,
Expand Down Expand Up @@ -173,10 +174,11 @@ def _frame_to_observations(
) -> 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)
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
Comment thread
isaac0804 marked this conversation as resolved.
Outdated
"""

if self.latest_observation[0] == self.steps:
Expand Down Expand Up @@ -216,6 +218,9 @@ def _frame_to_observations(
# 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),
Expand Down
Loading
Loading