Skip to content

Strategy-branch guard, full-length round-robins, and session fixes - #141

Merged
isaac0804 merged 84 commits into
mainfrom
strategy-guard
Oct 10, 2026
Merged

isaac0804 merged 84 commits into
mainfrom
strategy-guard

Conversation

@isaac0804

@isaac0804 isaac0804 commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Completes the guard for strategy branches (agent or human), so a strategy/* pull request can't score better without playing better. CI runs tools/check_strategy_branch.py from main's copy, so a branch can't loosen it.

New checks (on top of the existing path allowlist):

  • Opponents stay on main's code: a branch either adds new strategy modules or changes exactly one existing strategy, never both; never pickers.py; never deletes a strategy; kernel_strategy.py only gains imports from the added modules (in any order, so isort is fine). This gives the guarantee a pinned main checkout of the opponents would, without a second checkout.
  • The candidate can't reach outside its module: no import-time effects or fingerprint hazards (environment, files, dynamic imports, module-level state), no imports of another strategy (relative ones included), no changing what it imports or an alias of it (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).
  • After Copilot's review: an added module's registry import may not rebind or rename an existing name (it could replace an opponent's factory); a type change (e.g. a strategy turned into a symlink) counts as changing a strategy; strategy/__init__.py is off limits.

Tried for real: strategy/zone-split-shape (#138) passes; a copy that rebinds tiki_taka's factory fails on both counts. Of today's 22 strategies only overload_flow fails the reach check: it keeps a global streak 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.

  • The side the replay doesn't record skips acceleration estimation: about 4% less CPU per match, identical replay, intentions and score.

Also: match_cache.put writes a per-process temp file, so round-robins on several branches sharing replays/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:

  • Round-robins play full 600 s matches. 65 s matches were 55% draws, and 16 of 40 full matches changed result after 180 s. A full round-robin is now about 107 min on 15 workers, plugged in (the final run took 4.7 h). There is no half-time: a match is 600 s of continuous play with config_a on the right taking the only kickoff (comments corrected after Copilot's review).
  • Pile-ups fixed in 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.
  • DecoyOverloadTactic lets go once the enemy has held the ball 1 s (it never released before, tripping the 10 s frozen watchdog). COMMITTED_FROZEN stalls now say who holds the ball.
  • Free kicks can't be held forever. A defender clamped 0.48 m from the ball by the defense-area margin fouled every 2 s, and each foul restarted the 10 s clock, freezing three matches to full time. Only the first foul of each free kick resets it now (cards never remove a robot here, which is what stops repeat fouls in the rulebook).
  • A ball over a goal line restarts in the corner. It used to be placed where it crossed and pushed 1 m off the box: a free kick 2 m in front of goal, against the rulebook's corner and goal kicks (§6.2.1–2). 521 such kicks gave 150 goals in the round-robin; without them split_shape's 11-point lead over counter_flow drops 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).
  • Robots clearing an opponent free kick go round the ball. Pushed straight out, a robot between the ball and the opponent box aimed inside the planner's box margin and stopped 0.48 m from the ball: a Defender Too Close foul on every such free kick. It now takes the nearest reachable point of the 0.8 m circle.
  • GiveAndGoTactic keeps 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.
  • In the sim, a no_progress restart 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_progress fired 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_progress stops 51 → 8, wedges 3 → 0, no stalls.
  • ShadowAndMarkTactic markers 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_swap targets 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_swap is no longer xfail.
  • docs/strategies.md: standings from a full 600 s round-robin on this PR's final code (tournament_20261005_170958). split_shape still 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_progress restarts 699 → 296, keep-out fouls 245 → 26, 10 stalled matches (was 9). Out-of-bounds restarts rose 2446 → 2938, not yet investigated.
  • STRATEGY_DEVELOPMENT.md notes strategy branches can run in parallel worktrees.
  • docs/signals.md: one catalog of every strategy signal (round-robin output and the offline metric_correlation.py ones), grouped by question (broken? creates chances? keeps the ball? defends? restarts?), each with its summary.json key, meaning, reference range from the final run and what an off value points to. Replaces the table in STRATEGY_DEVELOPMENT.md; AGENTS.md points 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_press was in 5 of the 10 stalled matches; three_slot kicks the ball out 20 times a match).
  • Round-robins measure chances, both sides (analysis/chances.py, a CHANCES table and strategies[*].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 --reuse keeps 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).
  • The scenario bench reports the strategy signals of each start next to its outcome: shots, open-goal-weighted shots, regains to shot, entries, real losses, shots faced, open shots faced and danger seconds, with a paired delta and t per signal, overall and per family. On bank_v7 with 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.
  • README is a front page; docs get an index. The README (196 → ~100 lines) has a quick start, a "what are you working on?" table, the corrected layout and the PR rules; docs/README.md indexes every doc; setup, contributing and the full tools list moved to docs/setup_external.md, docs/contributing.md and docs/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:

  • SwitchOfPlayTactic with one robot plays the ball instead of holding it. At its own corner free kicks overload_press's lone switch robot stood 0.13 m from the ball for 10 s until no_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) under replays/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, --where finds 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's candidate_kicking doesn't say who kicks off (30 of 77 such kickoffs are the opponent's); the restart tag 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_half and swaps it, StrategyRunner follows it (from our referee or a real game controller), replays store the side per tick, and analyses and scenario_from_replay read 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 fixes score_aware_counter_flow and score_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_slot gives the robot on the ball to its attack, not to press/mark, which cleared it blind out of the field (~24 a match); man_mark stands goal-side like ShadowAndMarkTactic, so two robots marking each other no longer walk off the pitch; a lone SwitchOfPlayTactic robot fetches the ball before playing it; an empty slot doesn't keep GiveAndGoTactic'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.md are from before these changes.

2026-10-09:

  • docs/roadmap.md 10c: 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 that MAX_VEL=1 on 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. OutOfBoundsRule keeps 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_flow counts its own possession streak. It was a module-level global, shared by two overload_flow teams in one process; it now lives in the picker's closure, and every existing strategy passes the strategy-branch reach check.

  • round_robin.py comments 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.md explains 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_flow changes rerun every cached match; the full round-robin and the standings in docs/strategies.md are still pending.

Full suite: 12576 passed, 87 xfailed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SACGSprm9GiBueqKdxsy2j

2026-10-10:

  • The test suite no longer leaks a spinning thread. referee_receiver_test mocked a network that returned None forever, 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.
  • Evaluation tools renamed and trimmed (repo cleanup merged in): tools/tournament/ → tools/evaluation/, tournament_lib.py → match.py; the old full-match tournament script and the Elo tools are removed.
  • Ten strategies retired. The bottom ten of the 2026-10-05 round-robin are left out of a round-robin or a ladder pool unless named (RETIRED in round_robin.py); their code stays. A full round-robin is now 66 matches instead of 231, about 30 min on 14 workers.
  • --reuse records spot-check mismatches in summary.json (reuse.mismatches).
  • Sim PvP refines each tick's vision once and mirrors it for the opponent, plus three refiner speed-ups: about 9% less time per match, every replay array, intention log, stats file and referee log byte-identical (checked on a pairing the change was not developed on).
  • Half-time no longer crashes on a robot past a line. Carried to the mirrored spot, a robot in the run-off at x = −4.594 went to +4.594, outside the field, and the sim refused it: clear_danger vs decoy_and_overload crashed at half-time. It is now placed on the line.
  • docs/strategies.md and docs/signal_report.md: the 2026-10-10 round-robin (tournament_20261010_090819, 12 strategies, 66 matches, no stalls, no reuse mismatches). split_shape won all 11; counter_flow second (2.09 points per match); clear_danger, fourth before, last.
  • A determinism check (identical matches under different hash seeds and 14 concurrent copies) and a measurement that blind kick_upfield clearances leave the pitch 17% of the time, not usually; the open-bug note claiming otherwise is removed.
  • Motion-planning benchmark: four crowd scenarios from real match situations (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_swap stays as a stress test. Both planners fail kickoff_reset often (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.

… 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
Copilot AI balanced review requested due to automatic review settings October 3, 2026 10:13

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

🟡 Changes recommended

The checker still permits several direct ways to import, mutate, or replace opponent strategy behavior.

Review effort: Balanced
Findings: 4 High severity

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.

Comment thread tools/check_strategy_branch.py
Comment thread tools/check_strategy_branch.py
Comment thread tools/check_strategy_branch.py Outdated
Comment thread tools/check_strategy_branch.py Outdated
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
Copilot AI balanced review requested due to automatic review settings October 3, 2026 10:35

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

🟡 Changes recommended

The strategy checker still permits registry aliasing, relative imports, shared-state mutation, and symlink-based opponent changes.

Review effort: Balanced
Findings: 5 High severity

Open (5)

Comment thread tools/check_strategy_branch.py Outdated
Copilot AI balanced review requested due to automatic review settings October 3, 2026 14:33

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.

Comment thread tools/check_strategy_branch.py
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
Copilot AI balanced review requested due to automatic review settings October 3, 2026 14:48

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.

…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
Copilot AI balanced review requested due to automatic review settings October 4, 2026 20:48

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.

Comment thread tools/check_strategy_branch.py
Comment thread tools/tournament/round_robin.py Outdated
Comment thread utama_core/tactics/give_and_go.py
Comment thread docs/STRATEGY_DEVELOPMENT.md Outdated
isaac0804 and others added 3 commits October 4, 2026 23:22
… 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
Copilot AI balanced review requested due to automatic review settings October 4, 2026 22:41

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.

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
Copilot AI balanced review requested due to automatic review settings October 4, 2026 22:48
@isaac0804 isaac0804 changed the title Strategy branches can't change their opponents or reach outside their module Strategy-branch guard, full-length round-robins, and session fixes Oct 4, 2026
isaac0804 and others added 30 commits October 9, 2026 16:53
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
_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
…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
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.

2 participants