Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
113 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
82e80a2
fix: use game.ts as referee timebase instead of wall clock
isaac0804 May 15, 2026
69b96e0
fix(#107): suppress Kalman ghost tracking during robot substitution
isaac0804 May 15, 2026
3cc2ee8
Revert "fix(#107): suppress Kalman ghost tracking during robot substi…
isaac0804 May 15, 2026
d89a3af
fix: address Copilot review issues in main.py and referee tree dispat…
isaac0804 May 15, 2026
2e65361
fix: make DefenseAreaRule color-aware for symmetric enforcement
isaac0804 May 15, 2026
6db9d49
fix: cache last RefereeData so GameFrame.referee stays populated betw…
isaac0804 May 15, 2026
0a1e912
fix: address Copilot review issues in referee tree dispatchers and pe…
isaac0804 May 16, 2026
60b7c52
fix: prevent keep-out violations and defense-area infinite loop
isaac0804 May 17, 2026
b317811
fix: seed _prepare_entered_time in set_command() for direct PREPARE_*…
isaac0804 May 17, 2026
56b20a9
refactor: replace referee_system+custom_referee args with typed Refer…
isaac0804 May 18, 2026
4104b95
feat: add center_circle_radius to FieldDimensions
isaac0804 May 18, 2026
58fd7e1
refactor: replace hardcoded geometry fallbacks with STANDARD_FIELD_DI…
isaac0804 May 18, 2026
bdbe130
refactor: remove dead initial_command and initial_time params from Ga…
isaac0804 May 18, 2026
7502472
refactor: remove GameTracer from referee_integration branch
isaac0804 May 18, 2026
7b95ae8
fix: address Copilot review issues — timebase mismatch and center_cir…
isaac0804 May 18, 2026
d689b5d
fix: snapshot TeamInfo per packet to prevent retroactive history muta…
isaac0804 May 18, 2026
180d00a
update center circle render for rsim
energy-in-joles May 19, 2026
d0fb462
fix crash when rsim not included for rendering fpp
energy-in-joles May 19, 2026
9193f38
refactor: make StrategyRunner.full_field_dims the single source of fi…
isaac0804 May 19, 2026
eac4eb8
feat: add Exhibition Road festival demo (2v2, compact field, human-op…
isaac0804 May 19, 2026
3799bae
refactor: use WanderingStrategy instead of external Strategy_2v2 import
isaac0804 May 19, 2026
b8a259d
feat: scale WanderingStrategy waypoints to field_dims; pass exhibitio…
isaac0804 May 19, 2026
7b19c92
fix: propagate geometry mismatch error from vision thread to main thread
isaac0804 May 19, 2026
a4107af
fix: initialise _stop_event before _setup_vision_and_referee in __init__
isaac0804 May 19, 2026
2a9237b
fix: guard draw_line calls when env is None (grsim/real mode)
isaac0804 May 19, 2026
f5f30f0
fix: re-raise parked thread exception after stop_event exits the run …
isaac0804 May 19, 2026
a0f2bc6
feat: redesign GUI to 2x2 quadrant layout with field filling top-left
isaac0804 May 19, 2026
a35829c
feat: muted colour palette and grouped ghost-style buttons in GUI
isaac0804 May 19, 2026
788c637
Revert "feat: muted colour palette and grouped ghost-style buttons in…
isaac0804 May 19, 2026
0395d80
Revert "feat: redesign GUI to 2x2 quadrant layout with field filling …
isaac0804 May 19, 2026
7d16076
feat: restore 2x2 quadrant layout with original colour scheme
isaac0804 May 19, 2026
d380365
fix: pass waypoints directly to WanderingStep instead of via blackboard
isaac0804 May 19, 2026
a427963
fix: set _waypoints before super().__init__() in WanderingStrategy
isaac0804 May 19, 2026
3c5a653
config: disable defense area and keep-out rules for exhibition road
isaac0804 May 19, 2026
7d2c565
feat: add goal_depth to FieldDimensions and propagate to RefereeGeome…
isaac0804 May 19, 2026
a885932
docs: fix stale referee_gui.py references and outdated StrategyRunner…
isaac0804 May 19, 2026
b73ba24
fix: add _vision_receiver=None to mock_runner fixture to match Strate…
isaac0804 May 19, 2026
98e41ee
referee should be using FieldDimensions only
energy-in-joles May 20, 2026
7aae73d
tighten docs
energy-in-joles May 20, 2026
b64e4db
Potential fix for pull request finding
energy-in-joles May 20, 2026
25d660d
Potential fix for pull request finding
energy-in-joles May 20, 2026
f7b9204
update docs
energy-in-joles May 20, 2026
e2ce95f
implement mapping
energy-in-joles May 23, 2026
e446948
Merge branch 'main' into real_mapping
energy-in-joles May 23, 2026
2c5551b
remove automatic id increment
energy-in-joles May 23, 2026
7d2bebf
Potential fix for pull request finding
energy-in-joles May 23, 2026
f07f01f
check for duplication of cmd ids
energy-in-joles May 23, 2026
278c940
prevent double saving duplicaterobot responses
energy-in-joles May 23, 2026
e206914
fix mapping logic
energy-in-joles May 23, 2026
dbb13c9
Potential fix for pull request finding
energy-in-joles May 23, 2026
b267ce2
fix robot response pulling
energy-in-joles May 23, 2026
4c39664
Merge branch 'real_mapping' of https://github.com/First-Order-RoboCup…
energy-in-joles May 23, 2026
2ed3a53
Potential fix for pull request finding
energy-in-joles May 23, 2026
7be21ba
fix unbound reference
energy-in-joles May 23, 2026
c804a28
move self.opp
energy-in-joles May 23, 2026
ebf7d4e
migrate mapping logic to strat runner for get_robot_responses
energy-in-joles May 23, 2026
8374db1
change erro to warning
May 23, 2026
5e8ca10
fix: address 6 open Copilot inline review comments in real mapping
isaac0804 May 30, 2026
fedda8a
fix: follow-up review fixes in real mapping
isaac0804 May 30, 2026
900581b
Potential fix for pull request finding
energy-in-joles May 30, 2026
ccf061f
add test cases for new checks
energy-in-joles May 30, 2026
a57f386
Potential fix for pull request finding
energy-in-joles May 30, 2026
bcd1c10
Potential fix for pull request finding
energy-in-joles May 30, 2026
fe2ddc0
feat: per-robot IR sensor trust list with vision-proximity fallback
isaac0804 May 31, 2026
bdae22b
feat: leaky-bucket dribbler thermal limiter in RealRobotController
isaac0804 May 31, 2026
f81c274
refactor: switch dribbler thermal limiter from steps to wall-clock se…
isaac0804 May 31, 2026
f7c19d5
fix: warning spam, hysteresis, and heat telemetry for dribbler limiter
isaac0804 May 31, 2026
9913e45
feat: migrate hardware test demos and strategies from robot-hardware-…
isaac0804 May 31, 2026
f2915f5
fix: address Fred's review comments on PR #123
isaac0804 May 31, 2026
55aef7b
fix: address Fred's second-round review comments
isaac0804 May 31, 2026
1edcad1
fix: update placement strategy tests for 3x3 grid and fix arrival tol…
isaac0804 May 31, 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
39 changes: 39 additions & 0 deletions demo_dribbler_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""demo_dribbler_test.py — Test the dribbler with one robot on the Exhibition Road field.

Run:
pixi run python demo_dribbler_test.py

What this does
--------------
- 1 yellow robot, 0 opponents, Exhibition Road field (3 m × 2.25 m)
- DribblerTestStrategy: robot fetches the ball, carries it forward with the
dribbler on, then stops
- Blue target point drawn in RSim during carry
"""

from utama_core.config.field_params import GREAT_EXHIBITION_FIELD_DIMS
from utama_core.run import StrategyRunner
from utama_core.strategy.examples.dribbler_test_strategy import DribblerTestStrategy

N_FRIENDLY = 1
MY_TEAM_IS_YELLOW = True
MY_TEAM_IS_RIGHT = True


def main() -> None:
runner = StrategyRunner(
strategy=DribblerTestStrategy(robot_id=0),
my_team_is_yellow=MY_TEAM_IS_YELLOW,
my_team_is_right=MY_TEAM_IS_RIGHT,
mode="rsim",
exp_friendly=N_FRIENDLY,
exp_enemy=0,
full_field_dims=GREAT_EXHIBITION_FIELD_DIMS,
show_live_status=True,
)

runner.run()


if __name__ == "__main__":
main()
34 changes: 34 additions & 0 deletions demo_dribbler_test2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""demo_dribbler_test2.py — Dribbler sequence test: forward → left → right → back → stop.

Run:
pixi run python demo_dribbler_test2.py
"""

from utama_core.config.field_params import GREAT_EXHIBITION_FIELD_DIMS
from utama_core.run import StrategyRunner
from utama_core.strategy.examples.dribbler_test_strategy2 import (
DribblerSequenceStrategy,
)

N_FRIENDLY = 1
MY_TEAM_IS_YELLOW = True
MY_TEAM_IS_RIGHT = True


def main() -> None:
runner = StrategyRunner(
strategy=DribblerSequenceStrategy(robot_id=0),
my_team_is_yellow=MY_TEAM_IS_YELLOW,
my_team_is_right=MY_TEAM_IS_RIGHT,
mode="rsim",
exp_friendly=N_FRIENDLY,
exp_enemy=0,
full_field_dims=GREAT_EXHIBITION_FIELD_DIMS,
show_live_status=True,
)

runner.run()


if __name__ == "__main__":
main()
48 changes: 48 additions & 0 deletions demo_kicker_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""demo_kicker_test.py — Test the kicker with one robot on the Exhibition Road field.

Run:
pixi run python demo_kicker_test.py

What this does
--------------
- 1 yellow robot, 0 opponents, Exhibition Road field (3 m × 2.25 m)
- KickerTestStrategy: robot fetches ball, aligns to face enemy goal, kicks,
waits, then repeats
- Red goal center drawn in RSim
"""

from utama_core.config.field_params import GREAT_EXHIBITION_FIELD_DIMS
from utama_core.run import StrategyRunner
from utama_core.strategy.examples.kicker_test_strategy import KickerTestStrategy

N_FRIENDLY = 1
MY_TEAM_IS_YELLOW = True
MY_TEAM_IS_RIGHT = True


# Vision ID → firmware command ID mapping.
# Update before running on real hardware: vision ID is the sticker on the robot,
# command ID is baked into the robot's firmware.
YELLOW_VISION_TO_CMD = {
0: 0, # vision ID 0 → cmd ID 0 ← update with actual firmware ID
}


def main() -> None:
runner = StrategyRunner(
strategy=KickerTestStrategy(robot_id=0),
my_team_is_yellow=MY_TEAM_IS_YELLOW,
my_team_is_right=MY_TEAM_IS_RIGHT,
mode="real",
exp_friendly=N_FRIENDLY,
exp_enemy=0,
full_field_dims=GREAT_EXHIBITION_FIELD_DIMS,
show_live_status=True,
yellow_vision_to_cmd_mapping=YELLOW_VISION_TO_CMD,
)

runner.run()


if __name__ == "__main__":
main()
47 changes: 47 additions & 0 deletions demo_one_robot_placement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""demo_one_robot_placement.py — Entry point for testing RobotPlacementStrategy with one robot.

Run:
pixi run python demo_one_robot_placement.py

What this sets up
-----------------
- Exhibition Road field (3 m × 2.25 m, ``GREAT_EXHIBITION_FIELD_DIMS``)
- 1 yellow robot, 0 opponents
- RobotPlacementStrategy: robot oscillates vertically around the field center
while facing the ball
"""

from utama_core.config.field_params import GREAT_EXHIBITION_FIELD_DIMS
from utama_core.run import StrategyRunner
from utama_core.strategy.examples import RobotPlacementStrategy

N_FRIENDLY = 1
MY_TEAM_IS_YELLOW = True
MY_TEAM_IS_RIGHT = True

# Vision ID → firmware command ID mapping.
# Update before running on real hardware: vision ID is the sticker on the robot,
# command ID is baked into the robot's firmware.
YELLOW_VISION_TO_CMD = {
0: 0, # vision ID 0 → cmd ID 0 ← update with actual firmware ID
}


def main() -> None:
runner = StrategyRunner(
strategy=RobotPlacementStrategy(robot_id=0),
my_team_is_yellow=MY_TEAM_IS_YELLOW,
my_team_is_right=MY_TEAM_IS_RIGHT,
mode="real",
exp_friendly=N_FRIENDLY,
exp_enemy=0,
full_field_dims=GREAT_EXHIBITION_FIELD_DIMS,
show_live_status=True,
yellow_vision_to_cmd_mapping=YELLOW_VISION_TO_CMD,
)

runner.run()


if __name__ == "__main__":
main()
10 changes: 5 additions & 5 deletions utama_core/config/field_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,10 @@ def __post_init__(self):
)

GREAT_EXHIBITION_FIELD_DIMS = FieldDimensions(
full_field_half_length=2.0,
full_field_half_width=1.5,
half_defense_area_depth=0.4,
half_defense_area_width=0.8,
half_goal_width=0.5,
full_field_half_length=1.5,
full_field_half_width=1.125,
half_defense_area_depth=0.25,
half_defense_area_width=0.5,
half_goal_width=0.4,
Comment thread
energy-in-joles marked this conversation as resolved.
center_circle_radius=0.3,
)
1 change: 1 addition & 0 deletions utama_core/config/physical_constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
ROBOT_RADIUS = 0.09
MAX_ROBOTS = 6
MAX_ROBOT_ID = 15 # valid IDs from ssl small ruleset
BALL_RADIUS = 0.0215
2 changes: 1 addition & 1 deletion utama_core/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

### REAL CONTROLLER SETTINGS ###
BAUD_RATE = 115200
PORT = "/dev/ttyUSB0"
PORT = "/dev/ttyACM0"
Comment thread
energy-in-joles marked this conversation as resolved.
TIMEOUT = 0.1
KICKER_COOLDOWN_TIME = 10 # in seconds to prevent kicker from being actuated too frequently
KICKER_COOLDOWN_TIMESTEPS = int(KICKER_COOLDOWN_TIME * CONTROL_FREQUENCY) # in timesteps
Expand Down
65 changes: 53 additions & 12 deletions utama_core/data_processing/refiners/robot_info.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,67 @@
import warnings
from dataclasses import replace
from typing import List
from typing import FrozenSet, List, Optional

from utama_core.config.physical_constants import ROBOT_RADIUS
from utama_core.data_processing.refiners.base_refiner import BaseRefiner
from utama_core.entities.data.command import RobotResponse
from utama_core.entities.game.game_frame import GameFrame

# TODO: current doesn't handle has_ball for enemy robots. In future, implement using vision data

# Distance threshold for vision-based has_ball inference: robot centre + small buffer.
_BALL_CAPTURE_DIST = ROBOT_RADIUS + 0.04 # ~0.13 m


class RobotInfoRefiner(BaseRefiner):
"""Merges IR-sensor robot responses into the game frame.

Args:
trusted_ir_robots: Set of robot IDs (vision IDs) whose IR sensor is known to be
working. Robots in this set use the raw sensor reading directly. Any robot
ID **not** in this set has its has_ball inferred from vision proximity
(~0.13 m threshold) instead. Pass ``None`` (default) to trust every
robot's IR sensor — this is the normal stable-hardware behaviour and can
be restored by simply removing the argument.
"""

def __init__(self, trusted_ir_robots: Optional[FrozenSet[int]] = None):
self._trusted_ir_robots = trusted_ir_robots

def refine(self, game_frame: GameFrame, robot_responses: List[RobotResponse]):
Comment thread
isaac0804 marked this conversation as resolved.
if robot_responses is None or len(robot_responses) == 0:
friendly_robots = game_frame.friendly_robots.copy()

# When an allowlist is active, first infer has_ball for every untrusted
# robot from vision proximity. This covers frames where the robot drops
# its serial response entirely — without this pass, has_ball would stay
# frozen at the previous frame's value instead of being inferred.
if self._trusted_ir_robots is not None:
for robot_id, robot in friendly_robots.items():
if robot_id not in self._trusted_ir_robots:
friendly_robots[robot_id] = replace(robot, has_ball=self._infer_has_ball(game_frame, robot))

# Then overlay IR sensor readings for robots that sent a response.
if robot_responses:
for robot_response in robot_responses:
rid = robot_response.id
if rid not in friendly_robots:
warnings.warn(f"Robot ID {rid} in robot responses not found in friendly robots. ")
continue

robot = friendly_robots[rid]
if self._trusted_ir_robots is None or rid in self._trusted_ir_robots:
# Trusted (or trust-all mode): use raw IR reading
friendly_robots[rid] = replace(robot, has_ball=robot_response.has_ball)
# Untrusted robots were already handled by the vision-proximity pass above

if friendly_robots == game_frame.friendly_robots:
return game_frame
return replace(game_frame, friendly_robots=friendly_robots)

friendly_robots = game_frame.friendly_robots.copy()
for robot_response in robot_responses:
id = robot_response.id
if id in friendly_robots:
robot = friendly_robots[id]
friendly_robots[id] = replace(robot, has_ball=robot_response.has_ball)
else:
warnings.warn(f"Robot ID {id} in robot responses not found in friendly robots. ")
new_game_frame = replace(game_frame, friendly_robots=friendly_robots)
return new_game_frame
@staticmethod
def _infer_has_ball(game_frame: GameFrame, robot) -> bool:
"""Proximity-based has_ball: True when robot is within capture distance of ball."""
if game_frame.ball is None:
return False
ball_2d = game_frame.ball.p.to_2d()
return robot.p.distance_to(ball_2d) < _BALL_CAPTURE_DIST
17 changes: 17 additions & 0 deletions utama_core/global_utils/mapping_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ def map_friendly_enemy_to_colors(my_team_is_yellow: bool, friendly_item: T, enem
return yellow_item, blue_item


# NB: actually map_friendly_enemy_to_colors and map_colors_to_friendly_enemy have the same logic


def map_colors_to_friendly_enemy(my_team_is_yellow: bool, yellow_item: T, blue_item: T) -> Tuple[T, T]:
"""Map yellow and blue items to their respective friendly and enemy items based on my team color.

Args:
my_team_is_yellow (bool): True if the team is yellow, False if blue.
yellow_item (T): Any item from the yellow team (int, list, etc.)
blue_item (T): Any item from the blue team (int, list, etc.)

Returns:
Tuple[T, T]: A tuple of (friendly_item, enemy_item).
"""
return map_friendly_enemy_to_colors(my_team_is_yellow, yellow_item, blue_item)


def map_left_right_to_colors(my_team_is_yellow: bool, my_team_is_right: bool, right_item: T, left_item: T):
"""Map left and right items to their respective colors based on my team color and position.

Expand Down
Loading
Loading