Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 on lines +60 to 67
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.
and self.yellow_team == other.yellow_team
and self.blue_team == other.blue_team
and self.yellow_team.score == other.yellow_team.score
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
and self.blue_team_on_positive_half == other.blue_team_on_positive_half
and self.next_command == other.next_command
and self.current_action_time_remaining == other.current_action_time_remaining
)

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
"""

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