Skip to content

Real mapping - #123

Merged
energy-in-joles merged 113 commits into
mainfrom
real_mapping
Jun 3, 2026
Merged

energy-in-joles merged 113 commits into
mainfrom
real_mapping

Conversation

@energy-in-joles

Copy link
Copy Markdown
Member

add numerical mapping for real control when robot ids dont match with vision or when using one transmitter

isaac0804 and others added 30 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>
- 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>
- Delete dead RefereeStateMachine (never wired up, duplicated GameStateMachine
  with a broken _replace() call on a mutable class)
- Remove dead len(obs)==4 branch in _run_step; RSim always reads from ref_buffer
- Snapshot TeamInfo via copy.copy() in _generate_referee_data() to prevent
  score mutations retroactively corrupting stored RefereeRefiner records
- Update docs and stale docstrings accordingly
- Document TeamInfo frozen dataclass refactor as deferred follow-up work

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Fix CustomReferee.step() to reset rules only on actual command transition,
  not when the state machine ignores the violation due to the cooldown
- Remove dead STOP branch and unused my_team_is_yellow param from KeepOutRule
- Add last_status_message and last_next_command properties to RefereeRefiner;
  remove direct _referee_records[-1] access from StrategyRunner
- Fix designated_position type annotation: Tuple[float] → Tuple[float, float]
- Import BALL_KEEP_OUT_DISTANCE from referee_constants in test file instead
  of duplicating the literal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RefereeData.__eq__ excludes stage_time_left to avoid spurious
re-records, so the deduplication in add_new_referee_data() was
discarding every frame's updated countdown. The stage_time_left
property was reading from the stale cached record, causing the
terminal display to freeze (e.g. always showing 4:59).

Fix: track _latest_stage_time_left independently in refine(),
updated on every call before the dedup check, and return it
from the stage_time_left property.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ation loop

Action nodes were parking robots at 0.55 m from the ball while the
KeepOutRule fires at < 0.5 m. A small ball nudge during restart would
put defending robots inside the rule threshold, triggering another STOP
and looping indefinitely. Increasing the action node clearance to 0.8 m
gives enough buffer to absorb positioning inaccuracy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolved conflicts in main.py, field.py, game_frame.py, math_utils.py,
strategy_runner.py, and test_runner_misconfig.py. Updated referee branch
code to use the new FieldDimensions-based Field API (removed ClassProperty
constants, added field_dims param to Field.__init__), fixed Vector3D→Vector2D
issues in actions.py for fpp controller compatibility, and updated test
assertions to match the increased BALL_KEEP_OUT_DISTANCE (0.55→0.8).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace from_field_bounds with from_field_dims in RefereeGeometry so
  goal/defense dimensions scale with the actual field, not hardcoded to
  STANDARD_FIELD_DIMS
- Fix Vector3D→Vector2D conversions in actions.py for fpp controller
  compatibility (DirectFreeOursStep, BallPlacementOursStep)
- Rename get_min_bounding_zone→get_min_bounding_req in point_cycle_strategy
  and wandering_strategy to match abstract base class
- Update test fixtures to use new Field(field_dims=...) constructor and
  correct BALL_KEEP_OUT_DISTANCE (0.8 m)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_project_outside_circle was using a hardcoded (1, 0) fallback when a
robot sits exactly on the obstruction center (dist == 0), pushing it to
positive-x regardless of which half the team defends. Now _clear_to_legal_
positions passes an own-half-aware fallback so coincident robots are always
cleared toward their correct side.

Also fix test_all_robots_placed_on_own_half_* to track final target per
robot (dict keyed by robot_id) rather than a flat list, so the count
assertion is robust against the clearing pass re-issuing a move command.

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

Add CustomReferee.override_geometry() to replace geometry on both the
referee and its internal state machine. StrategyRunner now calls this
immediately after resolving field_bounds, so the custom referee always
uses the actual full_field_dims + field_bounds rather than the standard-
field values baked into the YAML profile.

The YAML geometry block is preserved as a fallback for standalone
CustomReferee use outside of StrategyRunner.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
from_field_dims(STANDARD_FIELD_DIMS) is the direct equivalent and makes
the standard-field case no more special than any other. Update the test
fixture accordingly.

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

Aligns with FieldDimensions.half_defense_area_depth. Updated geometry.py,
profile_loader.py, gui.py, and both YAML profiles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 7 out of 7 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (2)

utama_core/team_controller/src/controllers/real/real_robot_controller.py:248

  • _generate_command_buffer() now silently masks robot_id with & 0xFF. If a caller ever passes an out-of-range (e.g. negative or >255) ID, it will wrap and send a command to the wrong robot instead of failing fast. Add an explicit range check and use the validated robot_id byte directly.
    def _generate_command_buffer(self, robot_id: int, c_command: RobotCommand) -> bytes:
        """Generates the command buffer to be sent to the robot."""
        # endianness: little endian
        packet = bytearray(
            [
                robot_id & 0xFF,  # Robot ID
                c_command.local_forward_vel & 0xFF,  # Forward velocity low byte

utama_core/team_controller/src/controllers/real/real_robot_controller.py:248

  • _generate_command_buffer() now silently masks robot_id with & 0xFF. If a caller ever passes an out-of-range (e.g. negative or >255) ID, it will wrap and send a command to the wrong robot instead of failing fast. Add an explicit range check and use the validated robot_id byte directly.
    def _generate_command_buffer(self, robot_id: int, c_command: RobotCommand) -> bytes:
        """Generates the command buffer to be sent to the robot."""
        # endianness: little endian
        packet = bytearray(
            [
                robot_id & 0xFF,  # Robot ID
                c_command.local_forward_vel & 0xFF,  # Forward velocity low byte

Comment thread utama_core/run/strategy_runner.py Outdated
Comment thread utama_core/run/strategy_runner.py
Comment thread utama_core/run/strategy_runner.py Outdated
Comment thread utama_core/run/strategy_runner.py
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 30, 2026 22:09
@energy-in-joles
energy-in-joles requested a review from isaac0804 May 30, 2026 22:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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.

isaac0804
isaac0804 previously approved these changes May 30, 2026
Add yellow_trusted_ir_robots / blue_trusted_ir_robots params to
StrategyRunner (colour-keyed, matching vision_to_cmd_mapping convention).
Robots not in the trusted set have has_ball inferred from vision proximity
(~0.13 m) instead of the IR sensor — handles broken sensors without
disabling IR globally.

Default None trusts all sensors (backwards-compatible). Remove the args
once sensors are stable hardware-wide.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents the dribbler motor from overheating by capping continuous use at
30 s. The bucket fills 1 step/step while dribbling and drains at the same
rate while off — so 20 s on + 10 s off leaves 10 s on the clock, matching
real thermal recovery behaviour. Dribbler is forced off and a warning is
emitted when the bucket is full; it resumes automatically once drained.

Sim controllers are unaffected. Limit defined by DRIBBLER_MAX_ON_STEPS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 31, 2026 15:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

isaac0804 and others added 2 commits May 31, 2026 16:16
…conds

Uses time.monotonic() so the 30s budget is real elapsed time regardless
of control-loop frequency. Replaces DRIBBLER_MAX_ON_STEPS with
DRIBBLER_MAX_ON_SECONDS = 30.0 and _dribbler_steps with _dribbler_seconds
+ _dribbler_last_tick per robot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Warn only once per limit event (not every tick) using _dribbler_limit_warned
- Add hysteresis: once throttled, dribbler stays off until bucket drains to
  DRIBBLER_RESUME_SECONDS (15s = 50%) — prevents 1s-on/1s-off oscillation
  at the limit boundary
- Bucket continues draining even while strategy requests dribble (throttled)
- telop_gui: add heat: XX% label per robot, colour-coded muted→amber→red
  at 0-50% / 50-80% / 80-100% bucket fill

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 31, 2026 15:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@fred-huang122 fred-huang122 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review notes from the branch diff against main, focused on real-mode ID mapping correctness and typing.

Comment thread utama_core/run/strategy_runner.py
Comment thread utama_core/data_processing/refiners/robot_info.py
Comment thread utama_core/team_controller/src/controllers/real/real_robot_controller.py Outdated
isaac0804 and others added 2 commits May 31, 2026 17:43
…tests

Brings in the single-robot hardware test suite:
  - demo_dribbler_test.py       — fetch ball, carry forward with dribbler
  - demo_dribbler_test2.py      — dribbler sequence (forward/left/right/back)
  - demo_kicker_test.py         — fetch, align, kick, repeat
  - demo_one_robot_placement.py — 3x3 grid waypoint traversal

  - DribblerTestStrategy        — single-robot dribble carry
  - DribblerSequenceStrategy    — multi-leg dribble sequence
  - KickerTestStrategy          — fetch/align/kick loop
  - RobotPlacementStep          — updated to 3x3 snake-pattern grid (was simple oscillation)

demo_kicker_test and demo_one_robot_placement updated from the old
robot_id_map={} API to yellow_vision_to_cmd_mapping={} (real_mapping API).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. robot_info.py: untrusted robots now always get vision-proximity
   inference each frame, even when no RobotResponse arrives (dropped
   packet). Previously has_ball would stay frozen at the prior value.

2. strategy_runner.py: vision_to_cmd_mapping validation now checks that
   mapping keys cover all expected vision IDs (0..exp_n-1), not just
   the count. A mapping with the right length but wrong keys would
   previously pass and silently command the wrong physical robot.

3. real_robot_controller.py: annotate _dribbler_limit_warned and
   _dribbler_throttled as set[int] instead of bare set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 31, 2026 16:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@fred-huang122 fred-huang122 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Follow-up review on latest PR head f2915f5, focused on the new fixes and migrated demo strategy commits.

Comment thread utama_core/run/strategy_runner.py Outdated
Comment thread utama_core/strategy/examples/one_robot_placement_strategy.py Outdated
isaac0804 and others added 2 commits May 31, 2026 18:20
- strategy_runner: replace contiguous-ID assumption (set(range(n))) with
  a count check at init; add _validate_mapping_covers_game_frame() called
  after _load_game() to verify keys match real observed vision IDs —
  fixes rejection of valid non-contiguous deployments like {5:0, 6:1, 7:2}
- one_robot_placement_strategy: remove stale `target = Vector2D(0, 0)`
  override that overwrote the waypoint index selection every frame
- tests: update misconfig tests to match new error messages and add
  coverage for _validate_mapping_covers_game_frame()

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

- one_robot_placement_strategy: raise _ARRIVE_TOL from 0.1 to 0.15 m so
  the rsim motion controller reliably triggers waypoint advance (0.1 m
  caused the robot to hover just outside and freeze)
- test_placement_coords: rewrite tests to match 3x3 grid behaviour —
  verify the robot visits any two distinct grid waypoints instead of
  checking the old two-point oscillation targets; use same _ARRIVE_TOL
  as the strategy so test and strategy agree on when a waypoint is reached

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@fred-huang122 fred-huang122 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Left one comment about making the mapping invariant explicit.

robot_id (int): The ID of the robot.
command (RobotCommand): A named tuple containing the robot command with keys: 'local_forward_vel', 'local_left_vel', 'angular_vel', 'kick', 'chip', 'dribble'.
"""
if robot_id in self._vision_to_cmd_mapping:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

When vision_to_cmd_mapping is non-empty, missing entries should be treated as a configuration error. The controller cannot know that an unmapped vision ID is safe to send as a command ID; falling back to the raw ID partially restores identity mapping and can command the wrong physical robot. Please validate mapping coverage before real-mode startup, or raise/drop the command when a mapped controller receives an unmapped robot ID.

@fred-huang122 fred-huang122 Jun 1, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@energy-in-joles should this be implemented as I assume when we use mapping that we are mapping all robots no matter if the vision id and firmware id are the same? This is mostly an issue when mapping robots when a single strategy is running

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

don't know. don't care.

@energy-in-joles
energy-in-joles requested a review from isaac0804 June 3, 2026 19:58

@isaac0804 isaac0804 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@energy-in-joles
energy-in-joles merged commit 88516f3 into main Jun 3, 2026
2 checks passed
@energy-in-joles
energy-in-joles deleted the real_mapping branch June 3, 2026 20:00
energy-in-joles added a commit that referenced this pull request Jun 4, 2026
* feat: referee support in rsim

* hotfix: self.name bug

* feat: receive complete referee data and use it in strategy runner

* feat: referee command nodes and test

* fix: make RefereeData.__eq__ actually override tuple equality; fix test 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>

* improved some defence interactions + added new tests

* Add custom referee integration and profile rename

* Fix referee runner startup and keep-out behavior

* Scale referee actions with field geometry

* Fix referee restart progression and clearance

* Fix custom referee demo wiring and status messages

* Show referee source details in live status

* Add referee behaviour integration tests and unit test coverage

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

* Use public Field class properties instead of private constants

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>

* Default headless=True, skip ball teleport when placement pending, document 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>

* Document end-to-end ball placement test as future work

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Remove advanced controls (penalty & ball placement) from referee GUI

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>

* Add three future work items to referee integration docs

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

* Fix three bugs identified in Copilot review

- Delete dead RefereeStateMachine (never wired up, duplicated GameStateMachine
  with a broken _replace() call on a mutable class)
- Remove dead len(obs)==4 branch in _run_step; RSim always reads from ref_buffer
- Snapshot TeamInfo via copy.copy() in _generate_referee_data() to prevent
  score mutations retroactively corrupting stored RefereeRefiner records
- Update docs and stale docstrings accordingly
- Document TeamInfo frozen dataclass refactor as deferred follow-up work

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update utama_core/custom_referee/state_machine.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Address second round of Copilot review comments

- Fix CustomReferee.step() to reset rules only on actual command transition,
  not when the state machine ignores the violation due to the cooldown
- Remove dead STOP branch and unused my_team_is_yellow param from KeepOutRule
- Add last_status_message and last_next_command properties to RefereeRefiner;
  remove direct _referee_records[-1] access from StrategyRunner
- Fix designated_position type annotation: Tuple[float] → Tuple[float, float]
- Import BALL_KEEP_OUT_DISTANCE from referee_constants in test file instead
  of duplicating the literal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: cleanup

* fix: stage_time_left now updates every frame in RefereeRefiner

RefereeData.__eq__ excludes stage_time_left to avoid spurious
re-records, so the deduplication in add_new_referee_data() was
discarding every frame's updated countdown. The stage_time_left
property was reading from the stale cached record, causing the
terminal display to freeze (e.g. always showing 4:59).

Fix: track _latest_stage_time_left independently in refine(),
updated on every call before the dedup check, and return it
from the stage_time_left property.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: increase BALL_KEEP_OUT_DISTANCE to 0.8 m to prevent restart violation loop

Action nodes were parking robots at 0.55 m from the ball while the
KeepOutRule fires at < 0.5 m. A small ball nudge during restart would
put defending robots inside the rule threshold, triggering another STOP
and looping indefinitely. Increasing the action node clearance to 0.8 m
gives enough buffer to absorb positioning inaccuracy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: adapt referee code to FieldDimensions API and arbitrary field sizes

- Replace from_field_bounds with from_field_dims in RefereeGeometry so
  goal/defense dimensions scale with the actual field, not hardcoded to
  STANDARD_FIELD_DIMS
- Fix Vector3D→Vector2D conversions in actions.py for fpp controller
  compatibility (DirectFreeOursStep, BallPlacementOursStep)
- Rename get_min_bounding_zone→get_min_bounding_req in point_cycle_strategy
  and wandering_strategy to match abstract base class
- Update test fixtures to use new Field(field_dims=...) constructor and
  correct BALL_KEEP_OUT_DISTANCE (0.8 m)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: use own-half fallback direction when robot is coincident with ball

_project_outside_circle was using a hardcoded (1, 0) fallback when a
robot sits exactly on the obstruction center (dist == 0), pushing it to
positive-x regardless of which half the team defends. Now _clear_to_legal_
positions passes an own-half-aware fallback so coincident robots are always
cleared toward their correct side.

Also fix test_all_robots_placed_on_own_half_* to track final target per
robot (dict keyed by robot_id) rather than a flat list, so the count
assertion is robust against the clearing pass re-issuing a move command.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: make StrategyRunner the single source of truth for referee geometry

Add CustomReferee.override_geometry() to replace geometry on both the
referee and its internal state machine. StrategyRunner now calls this
immediately after resolving field_bounds, so the custom referee always
uses the actual full_field_dims + field_bounds rather than the standard-
field values baked into the YAML profile.

The YAML geometry block is preserved as a fallback for standalone
CustomReferee use outside of StrategyRunner.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: remove RefereeGeometry.from_standard_div_b()

from_field_dims(STANDARD_FIELD_DIMS) is the direct equivalent and makes
the standard-field case no more special than any other. Update the test
fixture accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: rename half_defense_length → half_defense_depth in RefereeGeometry

Aligns with FieldDimensions.half_defense_area_depth. Updated geometry.py,
profile_loader.py, gui.py, and both YAML profiles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: remove dead CLEARANCE_FALLBACK_DIRECTION and fix _clear_to_legal_positions

Three issues addressed:

1. Remove CLEARANCE_FALLBACK_DIRECTION from referee_constants — it was the
   default parameter of _project_outside_circle but no caller used the
   default; the fallback is now always passed explicitly.

2. _clear_to_legal_positions now accepts an optional intended_targets dict.
   When provided, clearance starts from the intended formation target rather
   than robot.p, so the clearing pass refines rather than discards the
   formation intent. PrepareKickoffTheirsStep now passes intended_targets
   instead of pre-issuing move commands and relying on overwrite.

3. Use own-half direction as the coincidence fallback for both ball and
   designated-position clearance — consistent with the invariant that
   friendly robots belong on their own half during all restart states.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: exclude PREPARE_KICKOFF/PENALTY from keep-out rule to prevent sequence resets

During PREPARE_KICKOFF_* and PREPARE_PENALTY_*, robots are actively moving
to their formation positions. The keep-out rule was firing after 30 frames
of encroachment and resetting the sequence to STOP → DIRECT_FREE, causing
a violation loop.

These states are now excluded from _STOPPAGE_COMMANDS — the state machine
already gates progression via _kicker_in_centre_circle and
_penalty_kicker_ready, so keep-out violations here are spurious. The rule
remains active for DIRECT_FREE_* where defending robots must genuinely stay
clear while the free kick is set up.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Revert "fix: exclude PREPARE_KICKOFF/PENALTY from keep-out rule to prevent sequence resets"

This reverts commit d6f4f11.

* fix: clear encroaching robots from current position, not formation target

If a robot is currently inside a keep-out zone, sending it toward a
distant formation target makes it traverse the exclusion zone for multiple
frames, triggering the keep-out violation counter. Instead, when a robot
is encroaching, project it out from its current position immediately.
Only use the intended formation target when the robot is already clear of
all exclusion zones.

This prevents keep-out rule violations during PREPARE_KICKOFF without
modifying the referee state machine.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: use BALL_KEEP_OUT_DISTANCE in _ensure_outside_center_circle

The function previously used a hardcoded 0.5 m (centre-circle radius),
meaning formation positions could be placed at exactly 0.5 m from the
ball. _clear_to_legal_positions would then push them out to 0.8 m, but
the robot still had to travel through the keep-out zone to reach its
target, accumulating violation frames.

Using BALL_KEEP_OUT_DISTANCE (0.8 m) ensures formation positions are
always placed at or beyond our enforced clearance distance, so robots
never need to traverse the keep-out zone to reach their assigned position.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address Copilot review issues in referee code and docs

- Update docs/custom_referee.md: replace stale from_standard_div_b() /
  from_field_bounds() references with the actual from_field_dims() API;
  fix half_defense_length → half_defense_depth in the dataclass snippet
- Fix misleading __eq__ comment in RefereeData: clarify that time_sent
  and time_received are excluded, not all timestamps
- Fix GameFrame.is_ball_in_goal: accept a Field parameter (self.field
  does not exist on GameFrame); add ball None guard; simplify with abs()
- Fix demo_custom_referee.py: replace non-existent from_standard_div_b()
  with from_field_dims(STANDARD_FIELD_DIMS); fix half_defense_length →
  half_defense_depth crash

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address Copilot review issues in referee actions and tests

- Fix PreparePenaltyTheirsStep: behind_line_x used +sign which placed
  non-keeper robots toward our goal instead of toward the centre line;
  change to -sign so robots are correctly positioned behind the penalty
  mark facing midfield (mirrors the existing PreparePenaltyOursStep logic)
- Rename misleading test: test_resolve_referee_system_defaults_to_none_even_when_custom_referee_is_provided
  → test_resolve_referee_system_rejects_custom_referee_without_explicit_system
- Fix demo_referee_gui_rsim.py docstring: run command pointed to
  utama_core/tests/referee/ path instead of the actual root-level file

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: keep RefereeRefiner properties current when deduplication skips a record

Properties like last_command, blue_team, status_message, etc. were all
reading from _referee_records[-1], which is not updated when the new
RefereeData compares equal (e.g. only status_message or time_sent changes).
This could leave live properties stale between meaningful state changes.

Add _latest_referee_data, updated unconditionally on every refine() call
(alongside the existing _latest_stage_time_left), and point all live
properties at it. _referee_records still grows only on meaningful state
changes, preserving its role as a deduplicated history.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: rename half_defense_length → half_defense_depth in all docs

The field was renamed in code (RefereeGeometry, YAML profiles) but three
doc files still used the old name in YAML examples and the field table.
Updated docs/custom_referee.md, docs/referee_integration.md, and
docs/custom_referee_gui.md to match the actual API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: remove non-existent exhibition.yaml from file structure

Only simulation.yaml and human.yaml exist under profiles/.
exhibition.yaml was listed in the directory tree but was never created.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* bug fix for pytest and add referee example in main

* fix(defense): make keeper and defender geometry radius-aware

Share shadow-geometry helpers between goalkeeping and defender positioning.
Use keeper-line and post limits adjusted for robot radius, account for ball and
robot radius in defender shadows, and dynamically choose the single defender's
coverage side.

Update goalkeep and defend_parameter tests for standard and custom field geometry.

* Fix graceful SIGINT shutdown under pixi

Use setsid for pixi main tasks so Ctrl+C is not delivered twice to the Python child. Update StrategyRunner SIGINT handling so the first interrupt requests graceful shutdown, stops the Rich FPS display, and lets cleanup send stop commands; a later interrupt still force-quits.

Add regression coverage for SIGINT behavior and REAL-mode zero-velocity stop commands during shutdown.

* Update Pixi environment configuration

* Fix FPP no-render path and point cycle API

* Fix FPP nullable RSim env typing

* fix: use game.ts as referee timebase instead of wall clock

GameStateMachine.stage_start_time is now seeded lazily from the first
current_time passed to step(), so the timebase matches the caller's
clock. StrategyRunner now passes game_frame.ts instead of time.time(),
making referee cooldowns and stage durations operate in sim-time for
RSIM (which starts near 0) rather than wall-clock time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(#107): suppress Kalman ghost tracking during robot substitution

GameStateMachine now sets bot_substitution_allowed=False by default
and flips it to True only when the stage transitions to a halftime/break
stage (where SSL rules permit substitution). PositionRefiner reads this
flag from game_frame.referee and skips adding None vanishing entries for
robots that disappear during an open substitution window, preventing the
Kalman filter from ghost-predicting their positions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Revert "fix(#107): suppress Kalman ghost tracking during robot substitution"

This reverts commit 69b96e0.

* fix: address Copilot review issues in main.py and referee tree dispatchers

- Move CustomReferee construction inside main() to eliminate import-time side effects
- Store _is_opp_strat on AbstractBehaviour so setup_() can propagate it to inner nodes
- Fix dispatcher setup_() calls in tree.py to forward is_opp_strat instead of hardcoding False

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: make DefenseAreaRule color-aware for symmetric enforcement

Previously the rule only checked the "friendly" team's defense area, missing
violations in the opponent's box. Now derives yellow/blue sides from
my_team_is_right XOR my_team_is_yellow and checks both areas in one pass.

Also fix two stale doc issues:
- Remove STOP from KeepOutRule active-commands table (it was always excluded)
- Update design-decisions section 4 to reflect that reset() is already called
  on command transitions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: cache last RefereeData so GameFrame.referee stays populated between packets

When using referee_system='official', the GC sends packets at ~10 Hz while
the main loop runs at 60 Hz. popleft() returned None on most ticks, causing
GameFrame.referee to drop out intermittently and disabling the referee layer.

Now the last successfully received RefereeData is cached and reused on ticks
where the buffer is empty. The RSIM/custom path is unaffected (data is always
produced on the same tick it is consumed).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address Copilot review issues in referee tree dispatchers and penalty docstrings

Remove redundant child.blackboard assignments in all dispatcher update() methods
since each child already attaches to the same global blackboard namespace during setup().
Clarify penalty positioning docstrings to reflect current behaviour and flag them
as the place for strategy team tuning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: prevent keep-out violations and defense-area infinite loop

- PrepareKickoffOursStep: route support robots through
  _clear_to_legal_positions with intended_targets so their paths
  never cross the 0.5m keep-out circle, even when starting on the
  enemy half after a STOP clearance. Kicker approaches from own-half
  side offset (ROBOT_RADIUS + 0.03m) instead of exactly (0,0).

- StopStep: add own-defense-area eviction so excess defenders are
  pushed outside the front edge during STOP. Prevents the infinite
  loop where two robots sit in their own area, STOP ends, the
  DefenseAreaRule fires immediately, STOP restarts, and no robot
  ever moves out.

  New helpers in actions.py:
    _is_in_own_defense_area(game, x, y)
    _own_defense_area_exit_x(game)
  New params on _clear_to_legal_positions:
    clear_own_defense_area: bool = False
    max_own_defenders: int = 1

- formations.py: add START_2V2 formation for 2v2 exhibition matches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: seed _prepare_entered_time in set_command() for direct PREPARE_* commands

When set_command() is called with a PREPARE_KICKOFF_* or PREPARE_PENALTY_*
command while already in STOP/HALT (bypassing the insert-STOP branch),
_prepare_entered_time was never updated from -inf, causing the auto-advance
prepare-duration check to fire immediately. Seed it (and _stop_entered_time
for STOP/HALT) in the fallthrough path so the delay is always respected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: replace referee_system+custom_referee args with typed RefereeSource union

Replaces the two coupled StrategyRunner arguments (`referee_system: str` and
`custom_referee: CustomReferee`) with a single `referee: RefereeSource` parameter
that accepts `None`, `OfficialReferee()`, or a `CustomReferee` instance — a proper
discriminated union with no string magic.

Also fixes referee clock initialisation: StrategyRunner now calls
`CustomReferee.seed_clock()` after `_load_game()` so all internal timers are
anchored to the first real `game_frame.ts` rather than a hardcoded 0.0.  This
makes the referee work correctly when reusing a long-running grsim instance
(wall-clock ts) or rsim (sim-time ts).  The initial command is automatically
chosen by mode — FORCE_START for rsim/grsim, HALT for real — so developers no
longer need to call `set_command()` at startup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add center_circle_radius to FieldDimensions

Moves the hardcoded 0.5 m center circle radius out of RefereeGeometry and
into FieldDimensions, the single source of truth for all field measurements.
RefereeGeometry.from_field_dims() now reads it from the field dims instead
of hardcoding it.  GREAT_EXHIBITION_FIELD_DIMS uses 0.3 m to reflect its
smaller scale.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: replace hardcoded geometry fallbacks with STANDARD_FIELD_DIMS references

profile_loader.py: default geometry values in _parse_profile() now read from
STANDARD_FIELD_DIMS instead of bare magic numbers, so they stay consistent
if standard field constants ever change.

state_machine.py: the 4.5 m half_length fallback in _penalty_kicker_ready()
replaced with STANDARD_FIELD_DIMS.full_field_half_length for the same reason.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: remove dead initial_command and initial_time params from GameStateMachine

Both were made obsolete by seed_clock(), which is now the single place where
the initial command and clock are applied after the first real game frame is
available.  The constructor always starts in HALT with stage_start_time=None.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: remove GameTracer from referee_integration branch

GameTracer belongs to agentic-harness; its import was causing CI
collection errors on this branch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address Copilot review issues — timebase mismatch and center_circle_radius validation

- test_referee_rsim: replace time.time() with game.ts in set_command() calls so
  timer comparisons use sim-time consistently
- field_params: validate center_circle_radius > 0 and <= half_width in __post_init__

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: snapshot TeamInfo per packet to prevent retroactive history mutation

RefereeMessageReceiver was passing the same mutable blue_info/yellow_info
instances into every RefereeData record, so a score or goalkeeper update
would silently mutate all previously recorded referee states. Added
TeamInfo.snapshot() and call it in the receiver so each RefereeData owns
independent TeamInfo values — consistent with how GameStateMachine already
used copy.copy() on its own team instances.

Also fixes the Optional[tuple[float]] → Optional[tuple[float, float]]
type annotation on RefereeRefiner.designated_position.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* update center circle render for rsim

* fix crash when rsim not included for rendering fpp

* refactor: make StrategyRunner.full_field_dims the single source of field geometry

- Remove geometry block from RefereeProfile, profile_loader.py, and
  built-in YAML profiles (human, simulation); CustomReferee now
  initialises _geometry from STANDARD_FIELD_DIMS as a placeholder
- StrategyRunner calls override_geometry() at startup so the referee
  always uses full_field_dims (or its field_bounds projection)
- For gRSim/Real, VisionReceiver fires a one-shot on_geometry callback
  that validates the vision geometry packet against full_field_dims and
  raises RuntimeError on mismatch
- GUI /config endpoint reads referee.geometry live so it reflects the
  overridden geometry, not stale profile values
- Update docs to document the geometry authority chain

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add Exhibition Road festival demo (2v2, compact field, human-operator referee)

* refactor: use WanderingStrategy instead of external Strategy_2v2 import

* feat: scale WanderingStrategy waypoints to field_dims; pass exhibition dims in demo

* fix: propagate geometry mismatch error from vision thread to main thread

* fix: initialise _stop_event before _setup_vision_and_referee in __init__

* fix: guard draw_line calls when env is None (grsim/real mode)

* fix: re-raise parked thread exception after stop_event exits the run loop

* feat: redesign GUI to 2x2 quadrant layout with field filling top-left

* feat: muted colour palette and grouped ghost-style buttons in GUI

* Revert "feat: muted colour palette and grouped ghost-style buttons in GUI"

This reverts commit c5caa3459ea2fa9e9da262e47d6729dbbb0eb46b.

* Revert "feat: redesign GUI to 2x2 quadrant layout with field filling top-left"

This reverts commit d922a78d20ee7a969c37105e33497936986cd5df.

* feat: restore 2x2 quadrant layout with original colour scheme

* fix: pass waypoints directly to WanderingStep instead of via blackboard

* fix: set _waypoints before super().__init__() in WanderingStrategy

* config: disable defense area and keep-out rules for exhibition road

* feat: add goal_depth to FieldDimensions and propagate to RefereeGeometry and RSim renderer

* docs: fix stale referee_gui.py references and outdated StrategyRunner API in docs

* fix: add _vision_receiver=None to mock_runner fixture to match StrategyRunner.__init__

* referee should be using FieldDimensions only

* tighten docs

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* update docs

* implement mapping

* remove automatic id increment

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* check for duplication of cmd ids

* prevent double saving duplicaterobot responses

* fix mapping logic

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix robot response pulling

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix unbound reference

* move self.opp

* migrate mapping logic to strat runner for get_robot_responses

* change erro to warning

* fix: address 6 open Copilot inline review comments in real mapping

- Fix None mapping TypeError in _validate_vision_to_cmd_mapping for REAL+PVP friendly team
- Remap RobotResponse IDs from cmd_id to vision_id in _split_robot_responses_by_team
- Fix duplicate cmd ID error message to report specific conflicting ID
- Fix stale comment: clarify cmd->vision remap happens in _split_robot_responses_by_team
- Fix misleading warning message in _add_robot_command on shared transmitter
- Fix serial port log to distinguish opened vs shared connections

* fix: follow-up review fixes in real mapping

- Fix _step_game real_responses type annotation and fallback from dict to List
- Skip friendly mapping length check when mapping was implicitly defaulted to {}
- Only pass real_responses kwarg to _step_game in REAL mode

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* add test cases for new checks

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* feat: per-robot IR sensor trust list with vision-proximity fallback

Add yellow_trusted_ir_robots / blue_trusted_ir_robots params to
StrategyRunner (colour-keyed, matching vision_to_cmd_mapping convention).
Robots not in the trusted set have has_ball inferred from vision proximity
(~0.13 m) instead of the IR sensor — handles broken sensors without
disabling IR globally.

Default None trusts all sensors (backwards-compatible). Remove the args
once sensors are stable hardware-wide.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: leaky-bucket dribbler thermal limiter in RealRobotController

Prevents the dribbler motor from overheating by capping continuous use at
30 s. The bucket fills 1 step/step while dribbling and drains at the same
rate while off — so 20 s on + 10 s off leaves 10 s on the clock, matching
real thermal recovery behaviour. Dribbler is forced off and a warning is
emitted when the bucket is full; it resumes automatically once drained.

Sim controllers are unaffected. Limit defined by DRIBBLER_MAX_ON_STEPS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: switch dribbler thermal limiter from steps to wall-clock seconds

Uses time.monotonic() so the 30s budget is real elapsed time regardless
of control-loop frequency. Replaces DRIBBLER_MAX_ON_STEPS with
DRIBBLER_MAX_ON_SECONDS = 30.0 and _dribbler_steps with _dribbler_seconds
+ _dribbler_last_tick per robot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: warning spam, hysteresis, and heat telemetry for dribbler limiter

- Warn only once per limit event (not every tick) using _dribbler_limit_warned
- Add hysteresis: once throttled, dribbler stays off until bucket drains to
  DRIBBLER_RESUME_SECONDS (15s = 50%) — prevents 1s-on/1s-off oscillation
  at the limit boundary
- Bucket continues draining even while strategy requests dribble (throttled)
- telop_gui: add heat: XX% label per robot, colour-coded muted→amber→red
  at 0-50% / 50-80% / 80-100% bucket fill

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: migrate hardware test demos and strategies from robot-hardware-tests

Brings in the single-robot hardware test suite:
  - demo_dribbler_test.py       — fetch ball, carry forward with dribbler
  - demo_dribbler_test2.py      — dribbler sequence (forward/left/right/back)
  - demo_kicker_test.py         — fetch, align, kick, repeat
  - demo_one_robot_placement.py — 3x3 grid waypoint traversal

  - DribblerTestStrategy        — single-robot dribble carry
  - DribblerSequenceStrategy    — multi-leg dribble sequence
  - KickerTestStrategy          — fetch/align/kick loop
  - RobotPlacementStep          — updated to 3x3 snake-pattern grid (was simple oscillation)

demo_kicker_test and demo_one_robot_placement updated from the old
robot_id_map={} API to yellow_vision_to_cmd_mapping={} (real_mapping API).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address Fred's review comments on PR #123

1. robot_info.py: untrusted robots now always get vision-proximity
   inference each frame, even when no RobotResponse arrives (dropped
   packet). Previously has_ball would stay frozen at the prior value.

2. strategy_runner.py: vision_to_cmd_mapping validation now checks that
   mapping keys cover all expected vision IDs (0..exp_n-1), not just
   the count. A mapping with the right length but wrong keys would
   previously pass and silently command the wrong physical robot.

3. real_robot_controller.py: annotate _dribbler_limit_warned and
   _dribbler_throttled as set[int] instead of bare set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address Fred's second-round review comments

- strategy_runner: replace contiguous-ID assumption (set(range(n))) with
  a count check at init; add _validate_mapping_covers_game_frame() called
  after _load_game() to verify keys match real observed vision IDs —
  fixes rejection of valid non-contiguous deployments like {5:0, 6:1, 7:2}
- one_robot_placement_strategy: remove stale `target = Vector2D(0, 0)`
  override that overwrote the waypoint index selection every frame
- tests: update misconfig tests to match new error messages and add
  coverage for _validate_mapping_covers_game_frame()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: update placement strategy tests for 3x3 grid and fix arrival tolerance

- one_robot_placement_strategy: raise _ARRIVE_TOL from 0.1 to 0.15 m so
  the rsim motion controller reliably triggers waypoint advance (0.1 m
  caused the robot to hover just outside and freeze)
- test_placement_coords: rewrite tests to match 3x3 grid behaviour —
  verify the robot visits any two distinct grid waypoints instead of
  checking the old two-point oscillation targets; use same _ARRIVE_TOL
  as the strategy so test and strategy agree on when a waypoint is reached

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add public rSim scenario hooks

* Enforce blackboard contracts in behaviour setup

* fix: track ball y with lone goalkeeper fallback

* Reset strategy on referee stoppages

* Make out-of-bounds restarts playable

* Use startup strategy in core demo

* feat: muted colour palette and grouped ghost-style buttons in GUI

* Revert "feat: muted colour palette and grouped ghost-style buttons in GUI"

This reverts commit c5caa3459ea2fa9e9da262e47d6729dbbb0eb46b.

* Revert "feat: redesign GUI to 2x2 quadrant layout with field filling top-left"

This reverts commit d922a78d20ee7a969c37105e33497936986cd5df.

* feat: restore 2x2 quadrant layout with original colour scheme

* feat: add goal_depth to FieldDimensions and propagate to RefereeGeometry and RSim renderer

* feat: add ball placement entry point, strategy skeleton, and tests

- demo_ball_placement.py: colleague entry point wired with the Exhibition
  Road field (GREAT_EXHIBITION_FIELD_DIMS, 4 m × 3 m), CustomReferee
  pre-configured for out-of-bounds → ball placement cycles, and GUI on
  port 8080. Zero referee config work needed by the strategy developer.

- utama_core/strategy/examples/ball_placement_strategy.py: BallPlacementStep
  skeleton with detailed docstring guide (approach → capture → carry flow,
  relevant API pointers, dribbler usage). BallPlacementStrategy wires it
  into the referee override tree for 2v2 exhibition play.

- utama_core/tests/strategy_runner/test_ball_placement_rsim.py: three rsim
  integration tests covering the full placement contract — placer approaches
  ball, placer progresses toward designated_position, non-placer robots clear
  and hold outside BALL_KEEP_OUT_DISTANCE.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: finish the ball_placement

* init demp

* take the free kick

* success sow the demo

* finish the ball placement

* test: tighten ball placement rsim tests and add placer selection coverage

- Test 1: robot 0 now starts 1.5 m from the ball (was already within
  tolerance), checks distance decrease ≥ 0.8 m instead of threshold crossing
- Test 3: hardcode placer_id=0 from reset_field instead of re-deriving it
  with the same min(distance_to_ball) logic as the implementation (tautological);
  move ball to centre so robot 1 has room to reach 0.8 m clearance
- Test 4 (new): robot 1 starts closer to the ball than robot 0; asserts
  robot 1 approaches the ball and robot 0 does not — exercises placer
  selection with robot 0 as non-placer, which original tests never covered
- Module docstring: add Known Gap section explaining why has_ball carry
  phase transition is not tested end-to-end

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add god mode to referee GUI for manual ball placement testing

- Right-click field canvas (with God Mode active) to issue
  BALL_PLACEMENT_YELLOW/BLUE directly to any target point
- Uses force_command() on the state machine to bypass the STOP-first
  guard, so the command takes effect immediately without operator
  needing to advance through STOP manually
- Add demo_ball_placement_real.py: single-robot rsim/real entry point
  with all auto-rules off, operator-driven via GUI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: update direct free kick tests broken by ball-placement-first routing

DirectFreeOursStep unit tests were asserting the robot targets the exact
ball position, but the new implementation targets an approach point behind
the ball (_APPROACH_OFFSET). Update assertions to check the target is
closer to the ball than the robot's start position.

The two rsim integration tests were relying on OutOfBoundsRule triggering
DIRECT_FREE via OOB detection, but the state machine now routes through
BALL_PLACEMENT first before DIRECT_FREE. Switch reset_field to inject the
command directly via force_command() so each test stays focused on its
subject (kicker approach / keep-out clearance) rather than OOB detection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: pivot around ball using turn_on_spot during ball placement carry

When the placer has the ball and needs to reorient toward the target,
use turn_on_spot (dribbling=True) instead of move — this offsets lateral
velocity so the robot orbits the ball contact point rather than spinning
around its own centre, reducing the chance the dribbler loses the ball.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address 4 P2 review comments from fred

- state_machine: manual STOP→BALL_PLACEMENT_* via set_command now uses
  _post_ball_placement_command (like auto-advance 1) instead of
  unconditionally setting next_command=NORMAL_START, so foul-restart is
  preserved through placement.
- state_machine: force_command sets next_command=NORMAL_START when
  forcing a BALL_PLACEMENT_* command so auto-advance 4 can fire.
- KickAfterDirectFreeStep: replace seen-first-restart guard with
  prev_command tracking; only kick on NORMAL_START that followed
  a DIRECT_FREE_* command, so the post-placement restart is never skipped.
- demo_ball_placement_real.py: mode="real" (was "rsim").

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: test_placer_moves_toward_designated_position robot start position

Robot 0 was teleported onto the ball expecting has_ball to fire, but rsim
infrared doesn't trigger reliably. Start robot 0 at (-1.3, 0) instead —
behind the ball relative to the target — so the approach motion reduces
distance to target, making progress measurable without requiring has_ball.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address 2 new P2 review comments from fred

- ball_placement_and_kick_strategy: KickAfterDirectFreeStep was never
  ticked during DIRECT_FREE_* (the RefereeOverride subtree handles it),
  so _prev_command never reached a DIRECT_FREE value and the post-
  placement kick was always skipped.  Introduced _CommandTracker, a
  mutable container updated by BallPlacementAndKickStrategy.step()
  before every tree tick, so the transition is observable even when the
  strategy subtree is overridden.

- robot_info: when trusted_ir_robots is set, untrusted robots that did
  not appear in robot_responses (non-blocking real-mode polling can omit
  robots) were keeping their stale has_ball value.  Refiner now applies
  vision-proximity inference for all untrusted robots up-front, then
  overlays IR readings only for trusted robots that responded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add vision-to-firmware ID mapping to demo_ball_placement_real

* fix: approach ball with back facing it for back-mounted kicker

go_to_ball was setting target_oren = robot.angle_to(ball), meaning
the robot approached with its front toward the ball. Since the
kicker and IR dribbler sensor are on the back, the robot now
approaches with back toward ball so the IR sensor triggers correctly.

* Filter vision detections to roster defined by vision→cmd mapping

PositionRefiner now accepts allowed_yellow_ids and allowed_blue_ids.
Any robot ID reported by SSL-Vision that is not in the allowlist is
dropped before it enters the game state, preventing stray detections
from permanently accumulating in friendly_robots/enemy_robots.

StrategyRunner derives the allowlists from yellow/blue vision_to_cmd_mapping
keys so the roster is implicitly defined by what is configured in gefr.py
(or any other runner) — no extra config needed.

* fix: remove circular coverage check and guard blue allowlist in single-team mode

_validate_mapping_covers_game_frame was called after wait_until_game_valid
but observed_ids were already allowlist-filtered, making the check circular
and causing empty observed IDs in output. Removed the dead post-loop calls.

Blue allowlist is now only derived when opp_strategy is present; in
single-team mode blue robots must remain visible as enemies regardless of
whether a blue mapping was provided.

---------

Co-authored-by: isaac0804 <isaacyong0804@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Isaac Yong <72080621+isaac0804@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joel <nzmjoel@gmail.com>
Co-authored-by: Joel <42647510+energy-in-joles@users.noreply.github.com>
Co-authored-by: SSL_Linux_Laptop <firstorderrobotics@gmail.com>
Co-authored-by: Ningchaun <1311856651@qq.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.

4 participants