Skip to content

feat: ball placement entry point#125

Closed
isaac0804 wants to merge 89 commits into
mainfrom
ball_placement_entry_point
Closed

feat: ball placement entry point#125
isaac0804 wants to merge 89 commits into
mainfrom
ball_placement_entry_point

Conversation

@isaac0804

@isaac0804 isaac0804 commented May 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds BallPlacementOursStep behaviour: robot approaches ball with dribbler on, then carries it to designated_position; uses turn_on_spot to pivot around the ball (not the robot center) when reorienting mid-carry
  • Adds force_command() to CustomReferee and GameStateMachine for bypassing the STOP-first guard — used by god mode and test injection
  • Adds God Mode to the referee GUI: toggle button + right-click context menu on the field canvas to issue BALL_PLACEMENT_YELLOW/BLUE to any target position
  • Adds demo_ball_placement.py (rsim, 2v2, full auto-advance cycle) and demo_ball_placement_real.py (single real robot, operator-driven)
  • Adds test_ball_placement_rsim.py — rsim integration tests for placer approach, carry, and non-placer clearance
  • Fixes 5 pre-existing test failures caused by ball-placement-first routing in _handle_foul and the new approach-behind-ball logic in DirectFreeOursStep

Test plan

  • 409/410 tests passing (test_random_movement_same_team is a pre-existing unrelated failure)
  • demo_ball_placement.py — rsim window opens, god mode right-click issues placement command, robot approaches designated position
  • demo_ball_placement_real.py — switch mode="rsim"mode="real" when hardware available; single robot responds to GUI commands

Known gap

End-to-end ball carry (has_ball → pivot → drive to target) is not rsim-testable — the motion controller stops at ball centre so the IR sensor never fires in simulation. Validated at the logic/unit level; physical validation requires real hardware.

🤖 Generated with Claude Code

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>
isaac0804 and others added 25 commits May 20, 2026 00:41
… GUI"

This reverts commit c5caa3459ea2fa9e9da262e47d6729dbbb0eb46b.
…top-left"

This reverts commit d922a78d20ee7a969c37105e33497936986cd5df.
- 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>
…rage

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

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

Copy link
Copy Markdown
Contributor Author

Superseded by #126 (conflict-free rebase onto main).

@isaac0804 isaac0804 closed this May 31, 2026
on_geometry = self._make_geometry_validation_callback()
vision_receiver = VisionReceiver(vision_buffers, on_geometry=on_geometry)
if isinstance(self.referee, OfficialReferee):
self.start_threads(vision_receiver, RefereeMessageReceiver(ref_buffer))

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.

[P2] This path starts RefereeMessageReceiver.pull_referee_data(), but that method holds self.lock while calling _update_data(), and _update_data() takes the same non-reentrant lock again before appending to ref_buffer. On the first referee packet the thread deadlocks before publishing any RefereeData, so OfficialReferee() leaves strategies with no referee commands. Remove one lock boundary or use a reentrant lock before wiring this source.

self.command = self.next_command
self.command_counter += 1
self.command_timestamp = timestamp
self.next_command = RefereeCommand.NORMAL_START

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.

[P2] When a violation queues BALL_PLACEMENT_*, _post_ball_placement_command stores the real restart, such as DIRECT_FREE_YELLOW. This manual NORMAL_START path overwrites next_command with NORMAL_START unconditionally, so after placement the state machine skips the foul restart and goes straight to live play. Mirror the automatic STOP-to-next branch for ball-placement commands.

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