Repository navigation
Real SSL logs as replays, goal ledger, situation bench, and referee fixes - #143
Merged
Merged
Conversation
…dules The official game logs carry the autorefs' tracked frames (positions and velocities in metres), which the log converter reads. Generated with the pinned protoc 3.20.2; imports made package-relative like the other generated modules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
A replay frame carries the referee's decision on the frame before it, so the frame nearest the match log's time for a free kick still showed ball placement: every harvested free kick (bank_v7: 240 of 242) was stored, and replayed, in BALL_PLACEMENT_*. `scenario_from_replay(showing=)` takes the first frame from that time on that shows the command, and the harvester uses it for every restart. Kickoffs and penalties are now harvested where they are awarded (-> PREPARE_KICKOFF_*/PREPARE_PENALTY_*), like free kicks, not at the NORMAL_START after their preparation, and their perspective follows the colour the command names (it was candidate_kicking whoever kicked). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
A start's command was set over the bench runner's PREPARE_KICKOFF_YELLOW, and the referee puts a STOP of its own before a restart set outside STOP, held until every robot (the kicker too) is 0.5 m from the ball. NORMAL_START was not set at all. So penalties and kickoffs were replayed late or as a yellow kickoff, free kicks started in a STOP, and every open-play NORMAL_START start (bank_v7: 496 of 860) was played inside the seeded kickoff for up to 10 s. A restart is now set from STOP, as the match awarded it, so its preparation runs from the first tick; any other command, NORMAL_START included, is set as it is. The kickoff anchor is a PREPARE_KICKOFF_YELLOW. Bench results recorded before this commit are not comparable with later ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
…play utama_core/analysis/goal_ledger.py ports the scratchpad ledger validated this session. A goal goes to the latest restart going live or ball won in open play less than 10 s before it, else "sustained possession". Per strategy, goals per match = sum over situation types of situations per match x P(goal), for goals scored and conceded. It reads only the .npz and .sparse_referee.pkl, so it also runs on converted official logs: - a free kick from the official game controller is itself live play (it goes straight to STOP), while ours waits for NORMAL_START; - a goal is timed when play stopped for it; - a lowered score withdraws a goal. On tournament_20261011_230119 it reproduces the reference exactly: 1458/1458 goal attributions, all 648 table values. Over all five run dirs, goals match the recorded scores in 299/299 matches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
Adds where goals come from (situation frequency x conversion), with split_shape reference values from tournament_20261011_230119, the CLI to the tool index, and "situation / goal ledger" to the AGENTS.md glossary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
A restart start is the moment the referee awarded it and is played as that restart; every robot starts at rest because rsim cannot place a moving one. bank_v7's restart starts were taken one tick early, and bench results recorded before b7ea54c are not comparable with later ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
utama_core/replay/ssl_log.py reads a RoboCup SSL game log (.log.gz) and writes it through ColumnarReplayWriter, so load_columnar_replay, render_window, scenario_from_replay and the analyses read a real match like one of ours. Yellow is the friendly team; side_is_right per tick from blue_team_on_positive_half (yellow's goalkeeper for pre-2019 logs). Robots and ball come from the autoref tracker with the most frames, falling back to merged camera detections for logs without one. Each robot id gets its own column, so substitutions lose no robot. Referee messages are kept where something but the clocks changes, with stage_time_left in seconds and designated_position in metres; a free kick reads NORMAL_START once its ball has moved 0.05 m, as in our referee. A <stem>.source.json sidecar records teams, score, tracker source and field geometry. Tests on synthetic logs pin the record reader, the referee dedupe, the id and side mapping, units, and the free-kick boundary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
…the published corpus Converts given logs or directories, or with --corpus YEARS lists the league's public share (WebDAV PROPFIND) for one division and, per log, downloads it, converts it into OUT/<year>/ and deletes the download, so at most --jobs raw logs are on disk. Existing replays are skipped, so an interrupted run resumes; OUT/corpus.json records each log's outcome. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
… taken free kick A converted tick's referee was the last kept message with its clocks run on from the first tick after it, and switching a taken free kick to NORMAL_START rebuilt the message from scratch: the stage clock jumped back by however long the free kick had waited (2-3 s in a 2025 match). Clocks now run on from when the message arrived, and NORMAL_START is applied to the state already run on to that tick. The new test fails before (the clock reads 299.75 s again at the kick) and passes after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
Found validating the converted 2022 Div B logs: - Teams that swap colours at half time (the game controller moves names, scores and cards with them) are swapped back, so friendly stays one team and its score never falls. - Where the referee's sides contradict the play (yellow's goalkeeper and yellow's robots both on the other half through 10 s of live play: one 2022 match ran with them inverted throughout, cameras agreeing with the trackers), the sides are inverted. - A ball beyond the field's walls is unseen: trackers' chip estimates put it up to hundreds of metres away for a few ticks (ball speeds to 838 m/s). Each has a regression test that fails with its fix disabled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
A 2018 Div A operator swapped the names for 3 s mid-half and back while the robots kept their markers; the converter swapped the robots too. A swap now counts only if it lasts 60 s. Regression test pins the boundary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
The game controller keeps DIRECT_FREE_* through the play after a free kick, so raw NORMAL_START/FORCE_START left the second half of 2022 Ri-one vs UBC Thunderbots under the 10 s needed, and its inverted sides stood. Live play is now what the converted replay calls live. The regression test's only live play is a taken free kick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
tools.md lists tools/convert_ssl_log.py; STRATEGY_DEVELOPMENT.md says what a converted match holds and where it differs from a sim replay: perspective and the corrections flagged in source.json, live share, robot columns, has_ball, free kicks, and the pre-2020 vision fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
Velocities are a finite difference over the runner's frame history. After a start was applied, that history still held the formation the bench runner was built in, so the first tick read the teleport as a jump: a ball at 130 m/s, robots faster. The ball filter then overshot by up to half a metre (a goal from a ball the keeper held, or a ball out of play), and the referee charged ball-speed and crashing fouls no robot made. In the open-play fidelity check this turned a 7% chance the other side scored after a ball won in the defensive third into 52%. Each side now gets a new history and Game from the scenario, as StrategyRunner does when the teams change ends. The regression test applies a start the way the bench does, right after building the runner, and fails at 30 m/s without the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
Owner-neutral situation rates per team per live minute (force starts per live minute), with live minutes per match: the weights the situation bench turns its per-situation results into goals per match with. Committed for the 118 published Division B matches and tournament_20261011_230119. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
…er match The harvester takes its starts from the goal ledger: every restart at the first tick showing its command, and every ball won at the start of its possession spell (live play only), each tagged with its situation type from the candidate's view. The old open-play harvest (passes and losses from turnover_breakdown) is gone, so there is one possession definition; --open-play becomes --per-situation: up to N of each type (kickoffs N/2), spread over source matches, seeded, near-duplicates skipped. scenario_bench plays each start against its source match's config_b (--opponent overrides; a start whose opponent is not on the branch is skipped and counted), scores the first goal within 10 s, and reports per type the paired change in P(for) and P(against), weighted by situation rates (--weights, real Division B by default) into goals per match, with a standard error. The ordinal outcome stays in each start's record. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
640 starts: 40 of each situation type, 20 of each kickoff type, from the
147 trustworthy matches, config_a's view. 293 are playable on this branch:
the rest come from matches against split_* variants that live only on
strategy/search-20261011.
scenario_bench.py --harvest-from replays/tournament_20261011_230119 \
--per-situation 40 --workers 15 --save-bank .../bank_v8.json
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
…difference Repurposed for the situation bench: for a baseline and several candidates, each candidate's bench effect (change in goal difference per match, one or more weights files) against its round-robin goal difference minus the baseline's over the same opponents, with Pearson and Spearman. Starts whose opponent is the candidate or the baseline are left out. The old standings and per-scenario score comparison is retired. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
…rust it AGENTS.md glossary: Start, Bank, Scenario bench and Situation updated, Situation rates added. STRATEGY_DEVELOPMENT.md: the bench section rewritten for situations, weights, per-start opponents, the 1.21x/1.58x over-count and what the bench cannot see; bank_v7's caveats removed with it; the validation against tournament_20261011_230119 (level right, ranking not: Spearman -0.09 over six candidates, SE ~2.3 goals a match) and where its noise comes from. tools.md, signals.md and the roadmap follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
The bench's estimate is biased per strategy, not only noisy. A 10 s horizon misses goals from build-up, and those are 3-7% of the split_* family's goals but 19-68% of the slower strategies' (counter_flow 68%). Split by the ledger, the round-robin gap to split_shape is mostly conversion, which the bench does measure (hence the right level). Between candidates, conversion, situation frequency and build-up goals differ by similar amounts, and the bench sees only the first. Per second of simulation it is also no more precise than matches. Also: the over-count need not be the same for every strategy, so it can reorder close candidates (the docs said ranking was unaffected); playable starts per type are 11-25, not 15-25; the glossary calls the bench a diagnostic, not a screen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
DefenseAreaRule called any robot touching the ball inside its own box a Multiple Defenders foul, so a keeper touch with a teammate also in the box got a penalty. Real matches: 60 of 97 false penalty calls were this. The keeper id is read from the referee data on the frame (GameFrame.referee, team info goalkeeper). Only a non-keeper touch counts. With no referee data attached, every touch still counts. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
NoProgressRule started counting at a kickoff/penalty NORMAL_START with the ball unmoved and fired exactly 10 s later. The official clock only runs once the ball is in play: 0.05 m of movement, or the 10 s auto-in-play of a kickoff/penalty. In real matches 226 of 422 no-progress calls were this false alarm. After a NORMAL_START the clock now waits for one of those. A FORCE_START puts the ball in play at once and is unchanged. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
…e area Officials never call no-progress with the ball inside either defense area (0 of 506 real calls). Ours did, and pre-empted the keeper-held-ball rule (attackers' free kick) with a neutral restart. While the ball is inside either defense area the clock is now held and reset. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
Under NORMAL_START (live play after a kickoff) the defense-area hold cleared the in-play flag too, so a ball that had already moved went back to waiting for the 10 s auto-in-play when it left a defense area: no-progress took 20 s there instead of 10 s. Only a command change takes the ball out of play now; the defense area just holds the clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
… shadow check Goalkeeper's own touch is no longer a Multiple Defenders penalty; the no-progress clock waits for the ball to be in play and is held inside the defense areas. Changes the match fingerprint: cached matches rerun. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECXiwf3VYxBTMmrLaexRQK
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Evaluation infrastructure from the 2026-10-12/13 sessions: official SSL game logs as replays, a goal ledger, the situation bench with bank v8, and three referee fixes the real logs exposed.
What's in it
Official SSL logs (
utama_core/replay/ssl_log.py,tools/convert_ssl_log.py)TrackerWrapperPacket) and their pb2 modules.Goal ledger (
utama_core/analysis/goal_ledger.py)--ratesreports how often each situation happens per live minute. Real and sim rates are inscenario_bench/banks/situation_rates_*.json.Situation bench and bank v8 (
tools/scenario_bench.py,scenario_bench/)apply_scenariostarts each side's frame history from the scenario.tournament_20261011_230119; bank v7 is deleted.bench_vs_standingschecks whether the bench's effect predicts round-robin goal difference. Docs explain why the bench gets the level right but can't rank strategies.Referee fixes (found by running our referee alongside 118 real matches), each with a regression test
Effect on evaluation
The referee and
apply_scenariochanges alter the round-robin fingerprint, so every cached match reruns.perf/planner-and-sim-speedup(separate PR) does the same, so a new fpp round-robin baseline will be run once both are merged.Tests
On
origin/main+ this branch + the perf branch merged together: lint passes;pytest utama_core/tests/ --headless --level quick --ignore-glob "**/*grsim*"gives 12680 passed, 87 xfailed, 0 failed. That run excluded the perf branch's C++-only test, which is fixed in its own PR.🤖 Generated with Claude Code