Repository navigation
Strategy-branch guard, full-length round-robins, and session fixes - #141
Conversation
… module check_strategy_branch.py now also checks, for a strategy/* branch: - opponents run main's code: add new strategy modules or change exactly one existing strategy, never both; never pickers.py, never delete a strategy; kernel_strategy.py only gains imports from the added modules; - a new or changed strategy module reaches nothing outside itself: no import-time effects or fingerprint hazards (environment, files, dynamic imports, module-level state), no imports of another strategy, no assigning attributes of what it imports, no setattr/delattr. Tried on strategy/zone-split-shape (passes) and a copy that rebinds tiki_taka's factory (fails on both counts). Of today's strategies only overload_flow fails the reach check (a global streak both teams share). match_cache.put uses a per-process temp file, so round-robins on several branches sharing the cache can't tear a record or fail a rename. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01SACGSprm9GiBueqKdxsy2j
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The checker still permits several direct ways to import, mutate, or replace opponent strategy behavior.
Review effort: Balanced
Findings: 4
Open (4)
What changed in this PR
Strengthens strategy-branch isolation and prevents concurrent match-cache writes from sharing temporary files.
Changes:
- Adds opponent and strategy reach checks.
- Uses process-specific cache temporary files.
- Expands regression tests and documentation.
| File | Description |
|---|---|
.github/workflows/ci.yml |
Clarifies strategy-branch CI enforcement. |
docs/STRATEGY_DEVELOPMENT.md |
Documents new isolation rules. |
tools/check_strategy_branch.py |
Implements AST-based branch checks. |
utama_core/replay/match_cache.py |
Adds PID-specific temporary files. |
utama_core/tests/engine/test_check_strategy_branch.py |
Tests branch restrictions. |
utama_core/tests/replay/test_match_cache.py |
Tests distinct process temporary files. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
A strategy/* branch may add modules under tactics/ (reach-checked like a strategy module) but not change or delete an existing tactic: the opponents run those. To improve a tactic an agent copies it into a new module; making the improvement everyone's stays a human change. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01SACGSprm9GiBueqKdxsy2j
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01SACGSprm9GiBueqKdxsy2j
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The checker still permits several paths that can alter or replace opponent strategy code.
Review effort: Balanced
Findings: 6
Open (6)
Reject modifications to the shared strategy package initializer · New Treat symlink type changes as modified strategy files Track imported-object aliases before allowing state mutations Resolve relative imports before applying the strategy allowlist Detect mutations of module-owned class and object state Restrict registry imports to the module's exact factory
A first-touch carrier with no open teammate held still with the ball for _MAX_HOP_TICKS (4 s), fell through to the shoot/strafe branch for a single tick, then held for another 4 s, until first_touch_stuck at 8 s of ticks_held. A repartition or referee reset renews mem before that (overload_flow's picker every ~6 s via the overload slot's release, and the referee's own 10 s no_progress restart), so the carrier froze 15-20 s with the ball while both teams' other robots milled around it (tournament_20261003_102921: clear_danger_vs_overload_flow_RK t=171-192 s, counter_flow_vs_overload_flow_Lk t=394-411 s). The hold now runs once per mem; afterwards the carrier keeps searching for a receiver each tick but strafes or shoots instead of holding again. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01SACGSprm9GiBueqKdxsy2j
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
The strategy checker still permits registry shadowing and several bypasses that can affect opponent code.
Review effort: Balanced
Findings: 6
Open (6)
Reject modifications to the shared strategy package initializer Treat symlink type changes as modified strategy files Track imported-object aliases before allowing state mutations Resolve relative imports before applying the strategy allowlist Detect mutations of module-owned class and object state Restrict registry imports to the module's exact factory
…00 s matches GiveAndGo support targets were offset from each robot's own y with the farthest candidate preferred, so a robot that arrived got a new target 2.2 m to the other side and support robots circled each other. Offsets now come from the ball and the nearest candidate wins ties; regression test fails before, passes after. Round-robins now play 600 s: of 40 full matches, 16 changed result after 180 s and the top of the table reordered; 65 s matches were 55% draws. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01SACGSprm9GiBueqKdxsy2j
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The checker has opponent-isolation bypasses, and the 600-second fixture does not implement the claimed two-half match.
Review effort: Balanced
Findings: 8
Open (10)
Detect mutations through imported shared-state bindings · New Implement halftime orchestration for full-match duration · New Reject modifications to the shared strategy package initializer Treat symlink type changes as modified strategy files Track imported-object aliases before allowing state mutations Resolve relative imports before applying the strategy allowlist Detect mutations of module-owned class and object state Restrict registry imports to the module's exact factory Separate tactic and evaluation changes from guard/cache work · New Update documentation for the new tournament duration · New
… say why DecoyOverloadTactic had no way out of either phase while the enemy held the ball, and is_committed() kept its decoy chasing until the 10 s stall watchdog fired (overload_flow_vs_tiki_taka_plus_LK, t=125-136 s). It now releases its robots after the enemy has held the ball for 1 s. COMMITTED_FROZEN stalls carried no diagnosis; they now say who holds the ball and how far each committed robot is from it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01SACGSprm9GiBueqKdxsy2j
…benchmark scenario The mirror_swap benchmark cell and test_mirror_swap put four targets at (±3.5, ±0.75), on the opponent's defense-area edge. Planners keep outfield robots OPPONENT_DEFENSE_AREA_KEEP_DISTANCE plus clearance away from it, so those robots stopped at |x| = 2.98, 0.52 m short. The test xfailed it as a planner local minimum. Back-row targets are now at |x| = 2.9; the test passes (xfail removed) and the benchmark has fpp and trajsample at 19/19. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01SACGSprm9GiBueqKdxsy2j
Each Defender Too Close foul restarted the 10 s free-kick clock, and keep_out re-raises every 2 s. The rulebook relies on cards to stop repeat fouls, but our cards never take a robot off, so a defender stuck 0.48 m from the ball (clamped there by the defense-area margin) froze three 600 s round-robin matches for 60-245 s, to full time. Only the first foul of each free kick now resets the clock. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01SACGSprm9GiBueqKdxsy2j
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
The strategy guard has multiple bypasses, and the 600-second tournament remains one extended first half rather than two halves.
Review effort: Balanced
Findings: 8
Open (10)
Implement halftime orchestration for full-match duration Detect mutations through imported shared-state bindings Reject modifications to the shared strategy package initializer Treat symlink type changes as modified strategy files Track imported-object aliases before allowing state mutations Resolve relative imports before applying the strategy allowlist Detect mutations of module-owned class and object state Restrict registry imports to the module's exact factory Separate tactic and evaluation changes from guard/cache work Update documentation for the new tournament duration
Replaces the 65s table. The run predates the decoy release and free-kick clock fixes in this PR; the doc names the matches they touch. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01SACGSprm9GiBueqKdxsy2j
The foul log charged an out-of-bounds exit to that team's robot nearest the ball as it crossed the line. A full-power clearance rolls ~17 m, so that was usually a robot near the touch line that never played the ball, and per-tactic out-of-bounds counts named the wrong tactic. OutOfBoundsRule now records the last robot seen touching the ball in this spell of play and names it in offending_robots; with no touch seen since the restart the log still falls back to the nearest robot. docs/strategies.md: the out-of-bounds rise (2446 -> 2938) is the goal-kick ping-pong, now met by the aimless-kick rule; the per-tactic counts of the last run used the old attribution. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… blog - start_test_env.sh: unused; its three commands are in docs/setup_external.md. - utama_core/run/vision_stream_data.py: a stale copy of the commentary constants that live in StrategyRunner; nothing imports it. - scenario bench banks v3 and v4: nothing references them (v5-v7 stay). - docs/blog/: the post belongs with the blog, not the code. Git history keeps all of them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…s gitignored benchmark_results/ mixed three kinds of output (five motion-planning runs, correlation studies, turnover reports), and "benchmark" in the glossary means only the motion-planning benchmark. The run motion_planning_comparison.md cites is now docs/motion_planning_results.md, like signal_report.md; the tool still writes to benchmark_results/, which is gitignored. The roadmap cites the old correlation and turnover reports by commit. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…tches/ demo_clips/ was a second root folder for media next to assets/; the rSim patch diffs sat under docs/ though only vendor/rSim/FORK_NOTES.md cites them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Our fork of RoboCIn's rSoccer keeps only SSLStandardEnv. Removed: the Ornstein-Uhlenbeck action noise (an RL training helper), a kd-tree test pytest never collects (it imports `kdtree` as a top-level module), the VSS and SSL render images nothing loads, and src/ssl/README.md, which documented upstream environments that don't exist here (its GIF links were broken). LICENSE and the README's credit to RoboCIn stay. Co-Authored-By: Claude Opus 5.5 <[email protected]>
main.py was the 2026 Exhibition Road festival demo (one GiveAndGo robot over grSim on the 4 m x 3 m field); examples/demo_exhibition_road.py keeps the festival demo. `pixi run main [yellow] [blue]` now plays two strategies from docs/strategies.md against each other in rsim with the simulation referee profile, a kickoff ceremony and the dashboard (default split_shape vs high_press). Co-Authored-By: Claude Opus 5.5 <[email protected]>
The direction agreed 2026-10-09: a ladder against a reference pool instead of more round-robins, realistic sampled worlds (vision noise and dropouts, kick spread, command delay, later per-robot profiles) instead of restart fuzzing as the source of variation, one match runner with the schedule as an option, and which changes rerun the match cache, so renames ride along with the next forced rerun. Co-Authored-By: Claude Opus 5.5 <[email protected]>
- debug_utils/real_controller_test.py imports modules that no longer exist (robot_control.src.skills, vision.vision_receiver), so it cannot start. - debug_utils/real_debugger.py sends an old 8-byte serial packet the current RealRobotController framing (0xAA ... 0x55) replaced; telop_gui.py (pixi run debug-robots) is the teleoperation tool. - debug_utils/real_vision_test.py printed raw vision frames, which the dashboard already shows. - data_processing/refiners/filters/fir.py (#81) was never wired in: the position refiner smooths and imputes with the Kalman filter. Its test and the two 0.5 MB CSVs only that test read go with it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… no longer skew time - RefereeMessageReceiver.pull_referee_data held its non-reentrant lock while _update_data took it again, so the thread hung on the first packet: since 2024-12, no game-controller message ever reached StrategyRunner in grSim or real mode (rsim uses CustomReferee in process and was unaffected). The lock guarded only a deque.append, which is atomic, so it is gone. time_received is now stamped after the receive rather than before it blocks. - VisionReceiver buffered `detection` even for SSL-Vision/grSim's geometry-only packets: an empty camera-0 frame captured at t=0, which CameraCombiner then averaged into the frame time (1.7e9 s became 8.5e8 s), sending the Kalman prediction across a huge time step. Only packets with a detection are buffered now. Regression tests drive each receive path with a fake socket or a serialized packet. Also removed what nothing called: the referee receiver's string maps, command history, wait/poll helpers and per-loop debug print (and its stale manual script, which called methods that no longer exist), the vision receiver's FPS printout and debug helpers, and PositionRefiner's AngleSmoother (alpha=1, a no-op). Co-Authored-By: Claude Opus 5.5 <[email protected]>
Position-only Kalman with finite-difference velocity, lost objects predicted forever, confidence ignored, noise settings from rsim. Waits on logged real-field vision data. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The match cache was written only after the whole run, so a crash lost every match played: WSL ran out of memory (7.4 GB) twice on 2026-10-09, at 139/231 and early in the rerun. Each finished match now goes into the cache with its restart and ball-loss analyses (about 7 s in the main process), unless it is a spot-check, which _store_played still compares at the end, or its key changed since the run started. round_robin.py is outside the match key, so stored results stay valid. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_019xiBYH9evM56f9477FZGJ4
…p 1) A candidate plays the top 5 of the newest complete round-robin (or --pool) in mirrored pairs over side and kickoff, at most 8 matches per opponent while rsim is deterministic, stopping a pairing once it is decided: at least 4 matches and a win-loss margin of 3, or a margin the remaining matches can't overturn. Reports W-D-L, points per match and goal difference per opponent and in total, next to each opponent's round-robin points per match. Additions only: it calls run_match and stores records in the round-robin's match-cache format, so each reuses the other's matches and no stored result is invalidated. Not built: Elo anchoring (points per match against the pool is the read for now) and spot-checks of reused records. Not yet checked against a real round-robin: every stored result is invalid after today's referee changes, so it is validated with the round-robin that closes the evaluation-v2 batch. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_019xiBYH9evM56f9477FZGJ4
The fake network returned None forever once its one packet was delivered, so pull_referee_data's loop spun in a daemon thread for the rest of the session and mock.Mock recorded every call: about 65 MB a second, which ran WSL out of memory in full-suite runs. The fake now blocks like a socket with no traffic. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_019xiBYH9evM56f9477FZGJ4
…d ones - tools/tournament/ is now tools/evaluation/; tournament_lib.py is now match.py. round_robin.py and the replays/tournament_<id>/ names stay. - Removed full_match_tournament.py, tools/elo.py and tools/plot_elo.py: the round-robin and ladder.py replace them. - round_robin.py analyses each match once, when it finishes; the end-of-run step reuses those analyses instead of re-reading every replay. Changes the match closure, so the next round-robin reruns every match. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_019xiBYH9evM56f9477FZGJ4
…abels Status labels had stopped matching results: the top three of the 2026-10-05 round-robin were a 'baseline' and an 'experimental' config, and four 'competitive' ones finished 19th-22nd. docs/strategies.md now lists the twelve a round-robin plays, in standings order, and the ten retired ones. round_robin.RETIRED leaves those ten out of a round-robin with no strategies named and out of the ladder's default pool; naming one still plays it, and their code stays (tiki_taka_plus, decoy_and_overload and tests build on them). RETIRED lives in round_robin.py, outside the match cache key: this reruns nothing. 231 matches a full run becomes 66. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_019xiBYH9evM56f9477FZGJ4
…t tables - match.py no longer calls tiki_taka the de facto baseline (it is retired). - Usage examples in scenario_bench, repro_from_replay and debug_match name playing strategies instead of retired ones. - docs/strategies.md drops the 2026-09 full-match tables; the 2026-10-05 round-robin supersedes them. Changes match.py, so the next round-robin reruns every match (owed anyway). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_019xiBYH9evM56f9477FZGJ4
Both teams refined the same vision with their own position and velocity refiners, and the two results were the same frame seen from opposite sides. _run_step now refines it once, for our side, and the opponent's frame is that one mirrored (friendly and enemy swapped; coordinates are absolute, so no float arithmetic). The robot-info and referee steps stay per side. In sim PVP our velocity refiner now always computes acceleration, since its frame feeds both sides; real mode is unchanged. Matches play identically: tiki_taka vs high_press (60 s) and counter_flow vs split_shape (700 s, through half-time) give bit-identical replays (all 23 .npz arrays), intention logs, stats and referee logs. A 60 s match takes 15.68 s instead of 16.30 s (-0.62 s, about 4%; three alternating runs each). test_frame_once.py pins the equivalence tick by tick, Kalman, a vanished robot and acceleration included. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_019xiBYH9evM56f9477FZGJ4
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_019xiBYH9evM56f9477FZGJ4 # Conflicts: # docs/STRATEGY_DEVELOPMENT.md # docs/roadmap.md
A spot-check mismatch was only printed, so a determinism regression left no record. summary.json's reuse block now lists the matches that differed. Roadmap 10a: today's checks (hash seeds, 14 concurrent copies, no wall clock or unseeded randomness in the rsim path) did not reproduce the September nondeterminism. round_robin.py is outside the match cache key. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_019xiBYH9evM56f9477FZGJ4
Every frame of both teams built one (keys plus an n x n numpy distance matrix), 7,202 per 60 s match, but only 1,904 are ever queried. The lookup now keeps its inputs and builds on the first closest_to_ball/closest_to_robot. Frames are never mutated after they are made, so a late build sees the same robots. Same matches: both test matches' replays (23 .npz arrays) and intention logs are bit-identical. Frame setup per 60 s match: 0.394 s -> 0.127 s, about 0.27 s (1.6%) saved. Not touched by the planned vision-filter rework (roadmap 10d). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_019xiBYH9evM56f9477FZGJ4
…ut-of-bounds source 8 matches of 300 s between round-robin strategies: 83 kick_upfield kicks, 14 (17%) followed by the ball leaving the pitch within 4 s; at most 14 of 58 balls out. The note said such a kick usually crosses the far goal line. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_019xiBYH9evM56f9477FZGJ4
Neither reproduces: rsim replayed a match identically across hash seeds and 14 concurrent copies, and blind kick_upfield clearances account for at most 14 of 58 balls out (9a26f17). The determinism check lives on as summary.json's reuse.mismatches, documented in docs/tools.md. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_019xiBYH9evM56f9477FZGJ4
Storing a frame went through three nested helpers per value, each looking the object's key up again (an ObjectKey of two enums, whose hash is a Python-level call). It now looks each object up once and creates its deques on first use, as before; the three helpers had no other callers and are removed. test_game_history.py pins what is stored, including that a missing vector stores nothing and a non-int robot id is skipped (it passes on the old code too). Same matches: both test matches' replays (23 .npz arrays) and intention logs are bit-identical. Per 60 s match: 0.348 s -> 0.203 s, about 0.15 s (0.9%) saved. Not touched by the planned vision-filter rework (roadmap 10d). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_019xiBYH9evM56f9477FZGJ4
Each robot's acceleration copied its last 15 velocity entries into two lists, then averaged them through the general n-dimensional helper. Robots (2-D) now average the stored entries directly, with the same additions in the same order, so the same floats; the ball keeps the general helper. Checked bit for bit (repr, signed zeros included) on 20,000 random histories with zero and tiny time steps, and pinned by test_robot_acceleration_fast_path_is_the_general_helper_bit_for_bit. Same matches: both test matches' replays (23 .npz arrays) and intention logs are bit-identical. Velocity refine per 60 s match: 0.940 s -> 0.702 s, about 0.24 s (1.5%) saved. Likely replaced by the planned vision-filter rework (roadmap 10d), if that estimates velocity and acceleration in the filter. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_019xiBYH9evM56f9477FZGJ4
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_019xiBYH9evM56f9477FZGJ4
_turned mirrored a robot about the centre spot without regard to the field: one in the run-off at x=-4.594 was sent to x=+4.594, the sim refused the teleport, and clear_danger vs decoy_and_overload crashed at half-time in the round-robin. The turned position is now kept within the field bounds the sim checks. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_019xiBYH9evM56f9477FZGJ4
… kept strategies Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_019xiBYH9evM56f9477FZGJ4
…tions mirror_swap (twelve robots head-on at full speed) was the only crowded scenario, and a stress test rather than a match situation. In the 2026-10-10 round-robin a moving robot had 3+ robots within 1 m 40% of the time, 66% of those within 1.5 m of the ball, the robot at ~0.8 m/s and its neighbours at ~0.5 m/s. New: ball_scrum and recovery_run (runs past robots circling the ball), wing_switch (teammates crossing ahead of the one on the ball) and kickoff_reset (ten robots from a corner scramble to the kick-off formation, positions from a replay). The doc also notes that trajsample's fixed seed makes repeats one draw. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_019xiBYH9evM56f9477FZGJ4



Completes the guard for strategy branches (agent or human), so a
strategy/*pull request can't score better without playing better. CI runstools/check_strategy_branch.pyfrommain's copy, so a branch can't loosen it.New checks (on top of the existing path allowlist):
main's code: a branch either adds new strategy modules or changes exactly one existing strategy, never both; neverpickers.py; never deletes a strategy;kernel_strategy.pyonly gains imports from the added modules (in any order, so isort is fine). This gives the guarantee a pinnedmaincheckout of the opponents would, without a second checkout.SomeTactic.x = ...,t = SomeTactic; t.x = ...,module.TABLE.clear(),setattr/delattr), and no changing its own module-level classes or variables from inside a function (both teams share them).strategy/__init__.pyis off limits.Tried for real:
strategy/zone-split-shape(#138) passes; a copy that rebindstiki_taka's factory fails on both counts. Of today's 22 strategies onlyoverload_flowfails the reach check: it keeps aglobalstreak that both teams would share, so changing it on a strategy branch means moving that into the factory's closure. A static check, not a sandbox: the reviewer still reads the diff.Also:
match_cache.putwrites a per-process temp file, so round-robins on several branches sharingreplays/match_cache/can't tear a record or fail a rename.STRATEGY_DEVELOPMENT.md's strategy-branch section states the new rules.Tests: 30 checker tests (incl. a tmp git repo end to end), a cache regression test that fails without the fix; full suite 12455 passed.
New tactics (added on request): a strategy branch may add modules under
tactics/(reach-checked like strategy modules), but not change or delete an existing tactic, since the opponents run those. To improve a tactic, copy it into a new module and change the copy. Tests: 42 checker tests.Session work (2026-10-04), each with a regression test that fails before its fix:
GiveAndGoTactic: the carrier with no receiver re-armed its 4 s hold every time its slot was reshuffled, freezing for 15–20 s while others crowded in; it now holds once per possession. Support runs also kept flipping between targets on either side; they now keep a target once reached.DecoyOverloadTacticlets go once the enemy has held the ball 1 s (it never released before, tripping the 10 s frozen watchdog).COMMITTED_FROZENstalls now say who holds the ball.split_shape's 11-point lead overcounter_flowdrops to 1. Corner kicks sit 0.5 m from both lines, goal kicks 1 m from the goal line (0.5 m, not 0.2 m, from the touch line, keeping the corner-loop margin).GiveAndGoTactickeeps its memory when robots join or leave its slot (new opt-in,keeps_mem_on_robot_change; only GiveAndGo sets it). A/B on 15 matches with the wedge fix: frozen give-and-go carriers 79 → 50, repeat holds in one possession 3 → 0. The kernel wiped it on every robot-set change, so six pickers that shuffle robots with the overload duet re-armed the 4 s no-receiver hold every few seconds, up to the 10 s no_progress stop. Design doc §11 notes the exception.go_to_ball: removed the no-op "dribbler on the back" flip. The dribbler is on the front, so facing the ball was already right; behaviour unchanged, now pinned by a test.no_progressrestart waits for robots to clear the ball. The runner forced FORCE_START on the STOP's first tick, with the ball teleported to where it already lay, so two robots locked on the ball never separated:no_progressfired every 10 s for up to 230 s (19% of matches in the 600 s round-robin had such a wedge). The state machine now advances once every robot is 0.5 m clear (about 1 s). Same 15 matches:no_progressstops 51 → 8, wedges 3 → 0, no stalls.ShadowAndMarkTacticmarkers stand goal-side of their opponent. The sign was reversed, so markers (in about ten strategies) stood on the far side, leaving the opponent a clear run at goal. A related chase (two markers targeting each other walk off the pitch) is listed under Known open bugs, not fixed.mirror_swaptargets moved off the defense-area edge (the planner was never deadlocked; the spots were unreachable). fpp and trajsample pass all 19 benchmark scenarios;test_mirror_swapis no longer xfail.docs/strategies.md: standings from a full 600 s round-robin on this PR's final code (tournament_20261005_170958).split_shapestill leads (19-1-1, 2.76 points per match) but its margin over second (high_press, 2.48) fell from 0.52 to 0.28. Against the run before the fixes:no_progressrestarts 699 → 296, keep-out fouls 245 → 26, 10 stalled matches (was 9). Out-of-bounds restarts rose 2446 → 2938, not yet investigated.STRATEGY_DEVELOPMENT.mdnotes strategy branches can run in parallel worktrees.docs/signals.md: one catalog of every strategy signal (round-robin output and the offlinemetric_correlation.pyones), grouped by question (broken? creates chances? keeps the ball? defends? restarts?), each with itssummary.jsonkey, meaning, reference range from the final run and what an off value points to. Replaces the table inSTRATEGY_DEVELOPMENT.md;AGENTS.mdpoints to it.docs/signal_report.md: the main signals of the final round-robin in figures (tools/signal_report.py <run_dir>regenerates them): a strategy × signal heatmap, attack vs defense scatters, goals per shot by distance and open goal mouth, ball losses by kind, fouls and stalls, with notes on what stands out (e.g.overload_presswas in 5 of the 10 stalled matches;three_slotkicks the ball out 20 times a match).analysis/chances.py, a CHANCES table andstrategies[*].chances): shot distance, angle and open goal mouth; conversion; save rate and how open the shots faced were; open-play regains shot from within 10 s; seconds the enemy held the ball in a side's defensive third; free kicks shot from within 10 s; metres gained per completed pass. Fed from the replay pass the ball-loss breakdown already makes. On the final run, against points per match: regain to shot +0.88, danger conceded −0.76, free kick to shot +0.64 (rank correlation); conversion, save rate and pass progress near 0. Diagnostics, not objectives. The 231 cached records were refreshed so--reusekeeps them. After Copilot's review, a stoppage ends a possession spell, so a shot from a restart is no longer credited to the regain before it (regains fell slightly for 22 strategies; split_shape's regain to shot 27% → 28%; records refreshed again).bank_v7with passes aimed 10° off, the outcome gave t −2.84 while shots and open shots gave t −6.1 and entries −3.6; losses, danger and shots faced didn't move, which says where the change acts.docs/README.mdindexes every doc; setup, contributing and the full tools list moved todocs/setup_external.md,docs/contributing.mdanddocs/tools.md.docs/pipeline_method.md's text matches the current code (refiner names, referee sources, one tick); the diagram is unchanged.2026-10-06:
SwitchOfPlayTacticwith one robot plays the ball instead of holding it. At its own corner free kicksoverload_press's lone switch robot stood 0.13 m from the ball for 10 s untilno_progress, with the overload slot committed and waiting: 2 of its stalls in the final run. It now shoots when a lane is open and otherwise carries the ball toward one. Both matches replayed with the fix: no stalls. Used by five strategies, so their cached matches rerun next time.scenario_bench.py --play: put a strategy into one start and watch it. Prints the outcome, signals and a timeline (referee, ball, nearest robot of each side, every tactic change); keeps the replay, match log and a picture (--render png|mp4) underreplays/scenario_play/. About 12 s a start. For testing a change in a situation the round-robin rarely reaches; a trial of a new strategy on the bench found the A/B outcome too coarse and asked for exactly this.Starts are tagged by situation,
--wherefinds them: third, lane, whose restart, who has the ball, robots near the ball (e.g.--where third=defensive ball=theirs: 64 starts in bank_v7). Tagging showed the id'scandidate_kickingdoesn't say who kicks off (30 of 77 such kickoffs are the opponent's); therestarttag reads it from the referee command.Hand-authored starts are played from the candidate's side. They were written with friendly on the left and never mirrored, so the candidate's keeper started in the goal it attacks. Fixed in code; bank_v7's stored copies are left so its results stay comparable.
docs/pitch_zones.md: one set of names for the pitch (thirds, wings, centre, cells, the box, danger, entry) with a diagram, linked from the docs index and the AGENTS.md glossary.Replay sidecar ~100× smaller. The referee's stage clock stopped at 0 (and, after the change below, stands still in stoppages), but the writer predicted it counting down, so the "sparse" referee sidecar stored a full message on about half of all ticks: 979 MB for the final run. One match rewritten: 17783 stored → 163, 4.46 MB → 0.04 MB, every tick identical.
The referee keeps match time by the rulebook. The clock counts playing time only (paused in STOP, HALT, kick-off and penalty preparation, ball placement); when a half's playing time is up, a 0 s half-time and a second half kicked off by the other team, then full time. Round-robins play the two halves to full time (sim-time cap 1200 s): a match is about 785 s of sim time instead of 600 s. At half-time the teams change ends: the referee fills
blue_team_on_positive_halfand swaps it,StrategyRunnerfollows it (from our referee or a real game controller), replays store the side per tick, and analyses andscenario_from_replayread it per frame. In the sim the robots are carried to their new half (as people do in the break), and a kick-off now waits for every robot to be in its own half (at most 10 s). This also fixesscore_aware_counter_flowandscore_aware_zone_flow, which were in their last-60-s mode from 240 s to the end of every round-robin match.Tactic fixes:
three_slotgives the robot on the ball to its attack, not to press/mark, which cleared it blind out of the field (~24 a match);man_markstands goal-side likeShadowAndMarkTactic, so two robots marking each other no longer walk off the pitch; a loneSwitchOfPlayTacticrobot fetches the ball before playing it; an empty slot doesn't keepGiveAndGoTactic's memory. Blind full-power clearances in other strategies are recorded as a known weakness (a fixed 4.7 m/s kick rolls ~17 m).Strategy-branch guard: classmethods, wildcard imports, parameter defaults and the remaining in-place methods no longer get past the reach check (Copilot).
Referee, replay and shared-tactic changes rerun every cached match on the next round-robin; standings in
docs/strategies.mdare from before these changes.2026-10-09:
docs/roadmap.md10c: real-robot profiles and calibration plan. What exists (the vision-to-command roster, trusted ball sensors), what can differ per robot, what is agreed (correct in the controller, expose only the rest, tactics ask about abilities not IDs), and the calibration routine to write before measuring. Records that the kicker stays fixed power and thatMAX_VEL=1on real robots is a safety cap.The aimless kick rule (§6.2.3, Division B). A ball touched by a robot that then crosses the halfway line and the opponent's goal line outside the goal, touching no other robot, is now a free kick for the opponent from where it was kicked, not a goal kick in the opponent's corner. Blind clearances from our own half cost nothing before: in the final run 388 of 1377 goal kicks led within seconds to one at the other end.
OutOfBoundsRulekeeps where the ball was last touched in the current spell of play (last_touch.touching_team); a touch on the halfway line itself (a kick-off) is not aimless.out_of_bounds.aimless_kick, on in both profiles.Every goal kick is placed at the same spot (
RefereeGeometry.goal_kick_position: 1 m from the goal line, 0.5 m from the touch line). A disallowed goal and a penalty out of time put it 0.2 m from the touch line.overload_flowcounts its own possession streak. It was a module-levelglobal, shared by twooverload_flowteams in one process; it now lives in the picker's closure, and every existing strategy passes the strategy-branch reach check.round_robin.pycomments say the teams change ends at half-time.A ball out of bounds is charged to the robot that last touched it, not to that team's robot nearest the line (a 17 m clearance usually left the kicker far away, so per-tactic out-of-bounds counts named the wrong tactic).
docs/strategies.mdexplains the out-of-bounds rise (2446 → 2938) as the goal-kick ping-pong the aimless-kick rule now meets.Merged
main(v2.0.3 release bump).The referee and
overload_flowchanges rerun every cached match; the full round-robin and the standings indocs/strategies.mdare still pending.Full suite: 12576 passed, 87 xfailed.
🤖 Generated with Claude Code
https://claude.ai/code/session_01SACGSprm9GiBueqKdxsy2j
2026-10-10:
referee_receiver_testmocked a network that returnedNoneforever, so its receiver thread kept a core busy and memory growing for the rest of the session (WSL ran out of memory). It now delivers one packet and then blocks like a quiet socket.tools/tournament/→tools/evaluation/,tournament_lib.py→match.py; the old full-match tournament script and the Elo tools are removed.RETIREDinround_robin.py); their code stays. A full round-robin is now 66 matches instead of 231, about 30 min on 14 workers.--reuserecords spot-check mismatches insummary.json(reuse.mismatches).clear_dangervsdecoy_and_overloadcrashed at half-time. It is now placed on the line.docs/strategies.mdanddocs/signal_report.md: the 2026-10-10 round-robin (tournament_20261010_090819, 12 strategies, 66 matches, no stalls, no reuse mismatches).split_shapewon all 11;counter_flowsecond (2.09 points per match);clear_danger, fourth before, last.kick_upfieldclearances leave the pitch 17% of the time, not usually; the open-bug note claiming otherwise is removed.ball_scrum,recovery_run,wing_switch,kickoff_reset, the last from a real goal in the round-robin). In the round-robin a moving robot has 3+ robots within 1 m 40% of the time, mostly at the ball;mirror_swapstays as a stress test. Both planners failkickoff_resetoften (trajsample about half of 40 sampler seeds, fpp at the default seed): the next planner target. Trajectory-sampling planner work (Sumatra-style sampling and fallback) is on its own branches for a later PR.Tests: full suite 12603 passed, 87 xfailed; each fix has a regression test that fails without it.