From b7590201c440b12c2a0462fd8ee67766b075b881 Mon Sep 17 00:00:00 2001 From: "Flemming N. Larsen" Date: Tue, 29 Sep 2026 20:50:42 +0200 Subject: [PATCH 1/4] Propose skipped-turn telemetry for compatibility runs --- .clue/id-ledger.yaml | 18 +++++++++ .../proposal.md | 29 ++++++++++++++ .../tasks.md | 17 ++++++++ .../AN-016-skipped-turn-telemetry-source.md | 40 +++++++++++++++++++ docs/analysis/README.md | 1 + .../CAP-007-compatibility-harness/criteria.md | 11 +++++ docs/plans/P-001-bridge-parity-campaign.md | 1 + 7 files changed, 117 insertions(+) create mode 100644 changes/CH-018-record-skipped-turn-telemetry/proposal.md create mode 100644 changes/CH-018-record-skipped-turn-telemetry/tasks.md create mode 100644 docs/analysis/AN-016-skipped-turn-telemetry-source.md diff --git a/.clue/id-ledger.yaml b/.clue/id-ledger.yaml index c6d6c0a..7c7ba7a 100644 --- a/.clue/id-ledger.yaml +++ b/.clue/id-ledger.yaml @@ -228,6 +228,24 @@ events: - {id: M-003, kind: numeric, state: retired, prefix: M, component: "3"} - {id: M-004, kind: numeric, state: retired, prefix: M, component: "4"} - {id: M-130, kind: numeric, state: retired, prefix: M, component: "130"} + - {id: CH-018, kind: numeric, state: reserved, prefix: CH, component: "18"} + - {id: HARN-002, kind: numeric, state: reserved, prefix: HARN, component: "2"} + - {id: HARN-003, kind: numeric, state: reserved, prefix: HARN, component: "3"} + - {id: HARN-004, kind: numeric, state: reserved, prefix: HARN, component: "4"} + - {id: HARN-005, kind: numeric, state: reserved, prefix: HARN, component: "5"} + - {id: HARN-006, kind: numeric, state: reserved, prefix: HARN, component: "6"} + - {id: HARN-007, kind: numeric, state: reserved, prefix: HARN, component: "7"} + - {id: HARN-008, kind: numeric, state: reserved, prefix: HARN, component: "8"} + - {id: M-148, kind: numeric, state: reserved, prefix: M, component: "148"} + - {id: AN-016, kind: numeric, state: reserved, prefix: AN, component: "16"} + - {id: AN-017, kind: numeric, state: reserved, prefix: AN, component: "17"} + - {id: TASKS-011, kind: numeric, state: reserved, prefix: TASKS, component: "11"} + - {id: OQ-009, kind: numeric, state: reserved, prefix: OQ, component: "9"} + - {id: CH-018, kind: numeric, state: live, prefix: CH, component: "18"} + - {id: HARN-008, kind: numeric, state: live, prefix: HARN, component: "8"} + - {id: M-148, kind: numeric, state: live, prefix: M, component: "148"} + - {id: AN-016, kind: numeric, state: live, prefix: AN, component: "16"} + - {id: TASKS-011, kind: numeric, state: live, prefix: TASKS, component: "11"} high-water: - {id: ADR-002, kind: numeric, state: reserved, prefix: ADR, component: "2"} - {id: AN-014, kind: numeric, state: reserved, prefix: AN, component: "14"} diff --git a/changes/CH-018-record-skipped-turn-telemetry/proposal.md b/changes/CH-018-record-skipped-turn-telemetry/proposal.md new file mode 100644 index 0000000..2e18649 --- /dev/null +++ b/changes/CH-018-record-skipped-turn-telemetry/proposal.md @@ -0,0 +1,29 @@ +--- +id: CH-018 +type: change +status: open +links: [P-001, CAP-007, AN-016] +title: Record delivered skipped-turn events in compatibility measurements +--- + +# CH-018 — Record delivered skipped-turn events in compatibility measurements + +## Problem + +M-006 observations currently cannot show which Tank Royale turns a bridge bot reported as skipped. Missing data has been stored as `null`, so it cannot answer whether skips occurred during warm-up or during the measured workload. + +## Change + +Add opt-in telemetry at the bridge's `SkippedTurnEvent` dispatch boundary. When enabled for a compatibility run, each delivered event records the bot ID, round, and skipped turn number. Preserve every recorded turn, including warm-up turns. A completed enabled run with no events has an empty event list; disabled, unsupported, or incomplete capture has a separate status and no empty-list claim. Existing registry observations are not rewritten. + +The measurement is about skipped-turn events received by the bridge. It does not claim to detect a server event that never reaches the bridge. This uses the current frozen `robocode.*` surface without changing any robot-visible method or event semantics. + +## Plan and acceptance + +This change serves `P-001/M-006` and adds the draft capability criterion `HARN-008` under `CAP-007`. `M-006` remains open; this change adds a separately tracked evidence door and does not claim parity-campaign completion. + +## Challenge to the commitment + +The assumption most likely to undermine this work is that the opt-in property reaches every wrapped bot process and that the marker is captured even when the legacy robot overrides `onSkippedTurn`. A credible alternative is to use the Tank Royale runner's server timing logs, which observe server detection independently of bot code, but the local spike found duplicated records from overlapping windows and a bounded diagnostic buffer. The cheapest useful test is a deliberately slow bridge bot run with capture enabled and disabled; compare each server-issued skipped event with exactly one recorded `(bot ID, round, turn)` tuple. Revise or stop if the enabled run misses a delivered event, the disabled run emits markers, or a completed no-skip run cannot be distinguished from unavailable capture. + +An implementation could satisfy a parser test and still fail the person using the measurement by enabling diagnostics implicitly in ordinary parity battles, perturbing their timing, or reporting an empty list when bot logs were not collected. The opt-in flag, explicit status, and end-to-end probe guard against those failures. diff --git a/changes/CH-018-record-skipped-turn-telemetry/tasks.md b/changes/CH-018-record-skipped-turn-telemetry/tasks.md new file mode 100644 index 0000000..3566876 --- /dev/null +++ b/changes/CH-018-record-skipped-turn-telemetry/tasks.md @@ -0,0 +1,17 @@ +--- +id: TASKS-011 +type: tasks +status: open +links: [CH-018] +title: Tasks for CH-018 +--- + +# Tasks + +- [ ] Add `HARN-008` evidence-door bookkeeping for `P-001/M-006`. +- [ ] Add property-gated skipped-turn records at bridge event dispatch with bot ID, round, and turn number. +- [ ] Add an opt-in compatibility-run flag and pass the property to wrapped bot processes. +- [ ] Parse bot logs into structured telemetry with distinct disabled, incomplete, and captured states; retain warm-up events and deduplicate repeated tuples. +- [ ] Add focused positive and negative tests for marker emission, disabled capture, empty completed capture, and structured registry persistence. +- [ ] Run a forced-skip compatibility probe with capture enabled and disabled, then run the checks for the changed bridge and harness surfaces. +- [ ] Update the compatibility harness design and usage docs; scaffold and validate the digest. diff --git a/docs/analysis/AN-016-skipped-turn-telemetry-source.md b/docs/analysis/AN-016-skipped-turn-telemetry-source.md new file mode 100644 index 0000000..f862b41 --- /dev/null +++ b/docs/analysis/AN-016-skipped-turn-telemetry-source.md @@ -0,0 +1,40 @@ +--- +id: AN-016 +type: analysis +status: active +links: [P-001, CAP-007] +title: A bounded source for per-turn skipped-turn telemetry +provenance: inferred +reversal-cost: low +carried-by: [CAP-007, DES-007, CRIT-007] +--- + +# AN-016 — A bounded source for per-turn skipped-turn telemetry + +## Risk investigated + +Whether compatibility measurements can record the exact bot, round, and turn for skipped-turn events without mistaking unavailable telemetry for zero skips. + +## Evidence boundary + +This spike ran on Windows with PowerShell, JDK 25, classic Robocode 1.11.1, bridge worktree `ch-018-record-skipped-turn-telemetry` at `7e06e2a`, and Tank Royale worktree revision `21324d6b7c4433e3fc1ba2a0e29c691fc3565e47`. The Tank Royale worktree had unrelated GUI edits before the spike; no source files there were changed. The runner 1.4.0 fat JAR and Bot API 1.4.0 JAR were built from that local Tank Royale revision. This is prepared-environment evidence, not a clean-checkout or release-pair claim. + +## What was tried + +The classic Control API snapshot interface was inspected with `javap`. `IRobotSnapshot` exposes only `ACTIVE`, `HIT_WALL`, `HIT_ROBOT`, and `DEAD`; it does not expose skipped-turn events or a skipped-turn counter. Classic robots can report callbacks themselves, as the bridge-owned `SkippedTurnProbe` does, but that cannot establish zero skips for arbitrary silent robots. + +Tank Royale's local runner source exposes opt-in `enableTurnTimingDiagnostics(30)`. The runner passes a diagnostics system property to the embedded server and redirects server output through JUL. The server records `skipped-turn-detected` with bot ID, round, and turn, then emits nearby timing records when the server stops. + +A source-mode Java spike enabled that runner diagnostic and ran a temporary bot that slept 200 ms before each intent with a 1 ms turn timeout. The battle completed one round, and the JUL handler captured server diagnostics including exact bot/round/turn tuples. The emitted skip lines repeated tuples from overlapping diagnostic windows, and the server stores timing records in a bounded 24,000-record ring. This proves the local capture route works for a bounded forced-skip run; it does not establish that it retains every skip in a long battle. + +The spike made no TPS or performance claim. An earlier filtered Gradle integration-test attempt pulled in unrelated .NET test dependencies and was stopped before the requested runner test began; that attempt is not counted as a test result. + +## Findings + +Bridge-side telemetry at `BotPeer`'s skipped-turn dispatch records the event at the boundary the compatibility layer receives, before the legacy robot's callback runs. It can be enabled only for measurement runs and can emit the exact bot ID, round, and skipped turn number to the bot's existing log. It avoids the runner's repeated timing windows and bounded timing buffer. + +This source measures skipped-turn events delivered to the bridge. It does not claim to observe a server-detected event that never reaches the bridge. Disabled capture, an incomplete battle, and a completed capture with no events must remain distinguishable. Historical registry observations stay unmeasured. + +## Rejected interpretation + +An empty bot log or missing server log line is not evidence of zero skips. The optional runner trace is useful for broader timing diagnosis, but its repeated windows and bounded ring are not the compatibility registry's per-event source. diff --git a/docs/analysis/README.md b/docs/analysis/README.md index cd03317..4ae1eb6 100644 --- a/docs/analysis/README.md +++ b/docs/analysis/README.md @@ -22,4 +22,5 @@ An incident where the corpus was green but later evidence contradicted it carrie - [AN-012 — M-003 finds one bridge-only lifecycle failure and no reproducible higher-score gap in the other two candidates](AN-012-m-003-higher-score-gaps-do-not-reproduce.md) · `active` — The three higher-score candidates were re-measured at official `roborumble` parameters: ScalarR has a named bridge-only lifecycle failure, while Ar1 and BasicSurfer are within the band. - [AN-013 — Tank Royale's native team model corresponds closely enough to classic's to map onto directly](AN-013-tank-royale-team-model-corresponds-to-classic.md) · `active` — `docs/capabilities/CAP-006-team-robot-support/design.md` left one decision open before any `M-005` implementation: does Tank Royale's team concept correspond closely enough to classic Robocode's to… - [AN-015 — The first melee registry cases are contaminated by fixed-opponent failures](AN-015-melee-registry-errors-are-opponent-contaminated.md) · `active` — Why the first official melee observations cannot yet be read as independent subject failures, and which bridge gaps remain separate investigations. +- [AN-016 — A bounded source for per-turn skipped-turn telemetry](AN-016-skipped-turn-telemetry-source.md) · `active` — Whether compatibility measurements can record the exact bot, round, and turn for skipped-turn events without mistaking unavailable telemetry for zero skips. diff --git a/docs/capabilities/CAP-007-compatibility-harness/criteria.md b/docs/capabilities/CAP-007-compatibility-harness/criteria.md index 7eb8c66..2876238 100644 --- a/docs/capabilities/CAP-007-compatibility-harness/criteria.md +++ b/docs/capabilities/CAP-007-compatibility-harness/criteria.md @@ -76,4 +76,15 @@ Feature: The compatibility harness When its battle setup is resolved Then the battlefield, round count, and participant count are that division's official values # The harness currently uses one setup for every division. Plan door: M-001. + + @HARN-008 @draft + Scenario: Skipped-turn telemetry preserves events and distinguishes missing capture from zero + Test-type: Integration + Given a completed Tank Royale compatibility run with telemetry enabled + When the bridge receives skipped-turn events + Then the observation records one event per distinct bot ID, round, and skipped turn number, including warm-up turns + And a completed enabled run with no events is captured with an empty event list + And disabled, unavailable, or incomplete capture has a distinct status and does not claim zero events + # Callback-level telemetry is opt-in and does not claim to see server events that never reach the bridge. Plan door: M-006. + # The harness is Python and is not yet a supported evidence carrier. ``` diff --git a/docs/plans/P-001-bridge-parity-campaign.md b/docs/plans/P-001-bridge-parity-campaign.md index 2c9024f..839e918 100644 --- a/docs/plans/P-001-bridge-parity-campaign.md +++ b/docs/plans/P-001-bridge-parity-campaign.md @@ -83,6 +83,7 @@ They are bookkeeping rather than a second plan. A door closes when its criterion | M-141 | `EVT-013` | `EVT-013` is active, with evidence attributable to it. Successor to `EVT-003` (`M-103`). Work lands under M-001. | done | | M-142 | `EVT-014` | `EVT-014` is active, with evidence attributable to it. Successor to `EVT-007` (`M-107`). Work lands under M-001. | done | | M-143 | `EVT-015` | `EVT-015` is active, with evidence attributable to it. Successor to `EVT-001` (`M-101`). Work lands under M-001. | done | +| M-148 | `HARN-008` | `HARN-008` is active, with evidence attributable to it. Work lands under M-006. | todo | ## Why this order From 725ea99f72691e391cbf64dc0d6d9b7b19da63b9 Mon Sep 17 00:00:00 2001 From: "Flemming N. Larsen" Date: Tue, 29 Sep 2026 21:17:21 +0200 Subject: [PATCH 2/4] Capture skipped-turn events in compatibility runs --- .../proposal.md | 29 ---- .../tasks.md | 17 --- compat-test/README.md | 12 ++ compat-test/compat_test.py | 128 ++++++++++++++++- compat-test/test_parity_registry.py | 133 ++++++++++++++++++ .../AN-016-skipped-turn-telemetry-source.md | 6 +- .../CAP-007-compatibility-harness/design.md | 4 +- .../robocode/tankroyale/bridge/BotPeer.java | 34 +++++ 8 files changed, 308 insertions(+), 55 deletions(-) delete mode 100644 changes/CH-018-record-skipped-turn-telemetry/proposal.md delete mode 100644 changes/CH-018-record-skipped-turn-telemetry/tasks.md diff --git a/changes/CH-018-record-skipped-turn-telemetry/proposal.md b/changes/CH-018-record-skipped-turn-telemetry/proposal.md deleted file mode 100644 index 2e18649..0000000 --- a/changes/CH-018-record-skipped-turn-telemetry/proposal.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -id: CH-018 -type: change -status: open -links: [P-001, CAP-007, AN-016] -title: Record delivered skipped-turn events in compatibility measurements ---- - -# CH-018 — Record delivered skipped-turn events in compatibility measurements - -## Problem - -M-006 observations currently cannot show which Tank Royale turns a bridge bot reported as skipped. Missing data has been stored as `null`, so it cannot answer whether skips occurred during warm-up or during the measured workload. - -## Change - -Add opt-in telemetry at the bridge's `SkippedTurnEvent` dispatch boundary. When enabled for a compatibility run, each delivered event records the bot ID, round, and skipped turn number. Preserve every recorded turn, including warm-up turns. A completed enabled run with no events has an empty event list; disabled, unsupported, or incomplete capture has a separate status and no empty-list claim. Existing registry observations are not rewritten. - -The measurement is about skipped-turn events received by the bridge. It does not claim to detect a server event that never reaches the bridge. This uses the current frozen `robocode.*` surface without changing any robot-visible method or event semantics. - -## Plan and acceptance - -This change serves `P-001/M-006` and adds the draft capability criterion `HARN-008` under `CAP-007`. `M-006` remains open; this change adds a separately tracked evidence door and does not claim parity-campaign completion. - -## Challenge to the commitment - -The assumption most likely to undermine this work is that the opt-in property reaches every wrapped bot process and that the marker is captured even when the legacy robot overrides `onSkippedTurn`. A credible alternative is to use the Tank Royale runner's server timing logs, which observe server detection independently of bot code, but the local spike found duplicated records from overlapping windows and a bounded diagnostic buffer. The cheapest useful test is a deliberately slow bridge bot run with capture enabled and disabled; compare each server-issued skipped event with exactly one recorded `(bot ID, round, turn)` tuple. Revise or stop if the enabled run misses a delivered event, the disabled run emits markers, or a completed no-skip run cannot be distinguished from unavailable capture. - -An implementation could satisfy a parser test and still fail the person using the measurement by enabling diagnostics implicitly in ordinary parity battles, perturbing their timing, or reporting an empty list when bot logs were not collected. The opt-in flag, explicit status, and end-to-end probe guard against those failures. diff --git a/changes/CH-018-record-skipped-turn-telemetry/tasks.md b/changes/CH-018-record-skipped-turn-telemetry/tasks.md deleted file mode 100644 index 3566876..0000000 --- a/changes/CH-018-record-skipped-turn-telemetry/tasks.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -id: TASKS-011 -type: tasks -status: open -links: [CH-018] -title: Tasks for CH-018 ---- - -# Tasks - -- [ ] Add `HARN-008` evidence-door bookkeeping for `P-001/M-006`. -- [ ] Add property-gated skipped-turn records at bridge event dispatch with bot ID, round, and turn number. -- [ ] Add an opt-in compatibility-run flag and pass the property to wrapped bot processes. -- [ ] Parse bot logs into structured telemetry with distinct disabled, incomplete, and captured states; retain warm-up events and deduplicate repeated tuples. -- [ ] Add focused positive and negative tests for marker emission, disabled capture, empty completed capture, and structured registry persistence. -- [ ] Run a forced-skip compatibility probe with capture enabled and disabled, then run the checks for the changed bridge and harness surfaces. -- [ ] Update the compatibility harness design and usage docs; scaffold and validate the digest. diff --git a/compat-test/README.md b/compat-test/README.md index 507c210..66b2b6b 100644 --- a/compat-test/README.md +++ b/compat-test/README.md @@ -204,6 +204,18 @@ Full error details land in `errors/robocode/.log` and `parity-registry.json` is the tracked evidence carrier. It appends each subject observation with the exact jar identity, setup, engine artifacts, normalized errors, and focused retest link. Normalized error origins skip engine implementation frames from both classic and the bridge so the first legacy application frame remains comparable across engines. It also retains an append-only diagnosis history, so a later triage decision cannot rewrite an earlier one. `parity-registry.md` renders the current status of every subject for review. Import an existing checkpoint with `--sync-registry`; after a diagnosis, tag a case with `--set-cause ` and rerun that cause with `--retest-cause --repair `. +### Skipped-turn telemetry + +Pass `--capture-skipped-turns` to enable opt-in records for Tank Royale bridge callbacks; capture is off by default. Each registry observation stores `tank_royale.skipped_turn_telemetry` with a status and, only when capture is complete, an `events` list of unique `{bot_id, round, turn}` values sorted by round and turn, including warm-up. `disabled`, `unavailable`, and `incomplete` use `events: null`; `captured` with `events: []` means the completed run delivered no skipped-turn callbacks to the bridge. Per-bot readiness and completion-count markers let the harness detect an older bridge jar or a truncated capture. The data describes callbacks delivered to the bridge and does not claim to record server detections that never reach it. + +Use the conformance probe to check a forced-skip run locally: + +```bash +python compat_test.py --conformance conformance-robots --conformance-source conformance-robots/conformance/probes/SkippedTurnProbe.java --robot-class conformance.probes.SkippedTurnProbe --engine tr --rounds 1 --capture-skipped-turns +``` + +The conformance JSON includes `skipped_turn_telemetry` at the top level. On a parity sweep, use `--capture-skipped-turns` and inspect the per-observation field in `parity-registry.json`. Repeated `--confirm-score` runs keep one record per attempt under `confirmation.skipped_turn_telemetry_runs`; the regression gate prints the same per-attempt JSON to stdout. + **Every row states the setup it was measured at.** The report is regenerated from the state file long after the battles ran, so a single header describing the current configuration would restate every stored row as though it had been measured under today's settings -- diff --git a/compat-test/compat_test.py b/compat-test/compat_test.py index b7c81ab..94f00ed 100644 --- a/compat-test/compat_test.py +++ b/compat-test/compat_test.py @@ -58,6 +58,12 @@ BASE_DIR = Path(__file__).resolve().parent TANK_ROYALE_HOME = Path(os.environ.get("COMPAT_TANK_ROYALE_HOME", r"C:\Code\tank-royale")) +SKIPPED_TURN_TELEMETRY_PROPERTY = "robocode.bridge.skippedTurnTelemetry" +SKIPPED_TURN_READY_RE = re.compile(r"^BRIDGE_SKIPPED_TURN_TELEMETRY_READY botId=(\d+)\s*$") +SKIPPED_TURN_EVENT_RE = re.compile( + r"^BRIDGE_SKIPPED_TURN botId=(\d+) round=(\d+) turn=(\d+)\s*$") +SKIPPED_TURN_COMPLETE_RE = re.compile( + r"^BRIDGE_SKIPPED_TURN_TELEMETRY_COMPLETE botId=(\d+) eventCount=(\d+)\s*$") def local_bot_api_jar(): @@ -404,7 +410,7 @@ def kill_process_tree(proc: subprocess.Popen, diagnostics=None): return proc.poll() is not None -def run_java(cmd, cwd, timeout, abort_when=None, poll_seconds=2.0): +def run_java(cmd, cwd, timeout, abort_when=None, poll_seconds=2.0, env=None): """Runs a java command; returns (returncode, stdout+stderr, timed_out). `abort_when` is an optional callable polled while the battle runs. When it returns @@ -416,7 +422,7 @@ def run_java(cmd, cwd, timeout, abort_when=None, poll_seconds=2.0): harness rather than the battle.""" proc = subprocess.Popen( cmd, cwd=str(cwd), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - text=True, encoding="utf-8", errors="replace") + text=True, encoding="utf-8", errors="replace", env=env) chunks = [] reader = threading.Thread(target=_drain, args=(proc.stdout, chunks), daemon=True) @@ -741,7 +747,7 @@ def run_rc_battle(jar_path: Path, classname, version, opts, setup, def summarize_worker_result(out_file, returncode, output, timed_out, elapsed, engine): result = { - "ok": False, "score": None, "scores": [], "errors": [], + "ok": False, "completed": False, "score": None, "scores": [], "errors": [], "error_count": 0, "elapsed": round(elapsed, 1), "log_text": "", } cleanup_failures = [] @@ -773,6 +779,7 @@ def summarize_worker_result(out_file, returncode, output, timed_out, elapsed, en return result result["ok"] = bool(data.get("ok")) + result["completed"] = bool(data.get("completed")) result["selected"] = data.get("selected") scores = [p.get("score", 0) for p in data.get("participants", [])] result["scores"] = [round(float(s), 1) for s in scores] @@ -922,6 +929,83 @@ def staged_log_dirs(bot_dirs): return list(dict.fromkeys(expanded)) +def skipped_turn_telemetry(bot_dirs, enabled, completed, expected_participants): + """Reads the opt-in bridge markers without treating a missing marker as zero skips.""" + if not enabled: + return {"status": "disabled", "events": None} + if not completed: + return {"status": "incomplete", "events": None} + + ready_bot_ids = set() + completed_event_counts = {} + events = set() + directories = staged_log_dirs(bot_dirs) + if not directories: + return {"status": "incomplete", "events": None} + + for directory in directories: + try: + with open(directory / "stdout.log", encoding="utf-8", errors="replace") as log: + for line in log: + line = line.rstrip("\r\n") + ready_match = SKIPPED_TURN_READY_RE.fullmatch(line) + if ready_match: + ready_bot_ids.add(int(ready_match.group(1))) + continue + complete_match = SKIPPED_TURN_COMPLETE_RE.fullmatch(line) + if complete_match: + bot_id, event_count = (int(value) for value in complete_match.groups()) + if bot_id in completed_event_counts: + return {"status": "incomplete", "events": None} + completed_event_counts[bot_id] = event_count + continue + event_match = SKIPPED_TURN_EVENT_RE.fullmatch(line) + if event_match: + events.add(tuple(int(value) for value in event_match.groups())) + except OSError: + return {"status": "incomplete", "events": None} + + if expected_participants < 1 or len(ready_bot_ids) != expected_participants: + return {"status": "unavailable", "events": None} + + if any(bot_id not in ready_bot_ids for bot_id, _, _ in events): + return {"status": "unavailable", "events": None} + + if set(completed_event_counts) != ready_bot_ids: + return {"status": "incomplete", "events": None} + + events_by_bot = {} + for bot_id, _, _ in events: + events_by_bot[bot_id] = events_by_bot.get(bot_id, 0) + 1 + if any(completed_event_counts[bot_id] != events_by_bot.get(bot_id, 0) + for bot_id in ready_bot_ids): + return {"status": "incomplete", "events": None} + + ordered_events = [ + {"bot_id": bot_id, "round": round_number, "turn": turn_number} + for bot_id, round_number, turn_number in sorted( + events, key=lambda event: (event[1], event[2], event[0])) + ] + return {"status": "captured", "events": ordered_events} + + +def java_env_for_skipped_turn_telemetry(enabled): + """Controls the telemetry property in the runner JVM and any bot JVMs it starts.""" + env = os.environ.copy() + options = env.get("JAVA_TOOL_OPTIONS", "") + property_option = re.compile( + rf"(? skippedTurnTelemetryRecords = SKIPPED_TURN_TELEMETRY_ENABLED + ? new ArrayList<>() : Collections.emptyList(); + private final Set skippedTurnTelemetryEventRecords = SKIPPED_TURN_TELEMETRY_ENABLED + ? new HashSet<>() : Collections.emptySet(); @SuppressWarnings("unused") public BotPeer(IBasicRobot robot, BotInfo botInfo) { @@ -445,10 +454,30 @@ private void dispatchRobotDeathEvent(BotDeathEvent botDeathEvent) { private void dispatchSkippedTurnEvent(BotEvent botEvent) { log("-> onSkippedTurn"); var skippedTurnEvent = (SkippedTurnEvent) botEvent; + if (SKIPPED_TURN_TELEMETRY_ENABLED) { + String record = "BRIDGE_SKIPPED_TURN botId=" + bot.getMyId() + + " round=" + bot.getRoundNumber() + + " turn=" + skippedTurnEvent.getTurnNumber(); + if (skippedTurnTelemetryEventRecords.add(record)) { + skippedTurnTelemetryRecords.add(record); + } + } var robocodeEvent = new robocode.SkippedTurnEvent(skippedTurnEvent.getTurnNumber()); dispatchRobotCallback(() -> advancedEvents.onSkippedTurn(robocodeEvent)); } + private void flushSkippedTurnTelemetry() { + if (SKIPPED_TURN_TELEMETRY_ENABLED) { + for (String record : skippedTurnTelemetryRecords) { + System.out.println(record); + } + System.out.println("BRIDGE_SKIPPED_TURN_TELEMETRY_COMPLETE botId=" + bot.getMyId() + + " eventCount=" + skippedTurnTelemetryEventRecords.size()); + skippedTurnTelemetryRecords.clear(); + skippedTurnTelemetryEventRecords.clear(); + } + } + private void dispatchDeathEvent() { log("-> onDeath"); var robocodeEvent = new robocode.DeathEvent(); @@ -1252,6 +1281,10 @@ public void onTeamMessage(TeamMessageEvent teamMessageEvent) { @Override public void onGameStarted(GameStartedEvent gameStatedEvent) { totalTurns.set(0); + if (SKIPPED_TURN_TELEMETRY_ENABLED) { + skippedTurnTelemetryRecords.add( + "BRIDGE_SKIPPED_TURN_TELEMETRY_READY botId=" + bot.getMyId()); + } // Legacy robots may read battlefield dimensions from setPeer(). The Bot API fills // GameSetup immediately before publishing this callback, so attach the peer here // instead of before the connection has received game setup. @@ -1260,6 +1293,7 @@ public void onGameStarted(GameStartedEvent gameStatedEvent) { @Override public void onGameEnded(GameEndedEvent gameEndedEvent) { + flushSkippedTurnTelemetry(); log("-> onBattleEnded"); if (basicEvents instanceof IBasicEvents2) { ((IBasicEvents2) basicEvents).onBattleEnded(new robocode.BattleEndedEvent( From b6e195a231bb4bf8cef8ded92f7abff787a375c3 Mon Sep 17 00:00:00 2001 From: "Flemming N. Larsen" Date: Tue, 29 Sep 2026 21:20:48 +0200 Subject: [PATCH 3/4] Validate telemetry completion per staged bot --- compat-test/compat_test.py | 10 +++++++--- compat-test/test_parity_registry.py | 2 +- docs/analysis/AN-016-skipped-turn-telemetry-source.md | 2 ++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/compat-test/compat_test.py b/compat-test/compat_test.py index 94f00ed..470981b 100644 --- a/compat-test/compat_test.py +++ b/compat-test/compat_test.py @@ -929,7 +929,7 @@ def staged_log_dirs(bot_dirs): return list(dict.fromkeys(expanded)) -def skipped_turn_telemetry(bot_dirs, enabled, completed, expected_participants): +def skipped_turn_telemetry(bot_dirs, enabled, completed, expected_bots): """Reads the opt-in bridge markers without treating a missing marker as zero skips.""" if not enabled: return {"status": "disabled", "events": None} @@ -965,7 +965,7 @@ def skipped_turn_telemetry(bot_dirs, enabled, completed, expected_participants): except OSError: return {"status": "incomplete", "events": None} - if expected_participants < 1 or len(ready_bot_ids) != expected_participants: + if expected_bots < 1 or len(ready_bot_ids) != expected_bots: return {"status": "unavailable", "events": None} if any(bot_id not in ready_bot_ids for bot_id, _, _ in events): @@ -1178,7 +1178,7 @@ def run_tr_battle(jar_path: Path, classname, version, opts, setup, rc_signatures result["scores"] = [] result["skipped_turn_telemetry"] = skipped_turn_telemetry( bot_dirs, capture_skipped_turns, result.get("completed", False), - len(result.get("scores", []))) + len(staged_log_dirs(bot_dirs))) return result @@ -2192,6 +2192,10 @@ def main(): "errors": ["HARNESS: Tank Royale side skipped after classic process cleanup failed"], "error_signatures": [], "bridge_only_signatures": None, "has_log": False, "skipped": None, "stop_sweep": True, + "skipped_turn_telemetry": { + "status": "incomplete" if opts.capture_skipped_turns else "disabled", + "events": None, + }, } else: tr = run_tr_battle( diff --git a/compat-test/test_parity_registry.py b/compat-test/test_parity_registry.py index 4e44c5a..6ba53b5 100644 --- a/compat-test/test_parity_registry.py +++ b/compat-test/test_parity_registry.py @@ -400,7 +400,7 @@ def testHARN008_UnitPositive_CaptureDeduplicatesAndKeepsWarmupTurns(self): encoding="utf-8") result = harness.skipped_turn_telemetry( - [first, second], True, True, expected_participants=2) + [first, second], True, True, expected_bots=2) self.assertEqual("captured", result["status"]) self.assertEqual([ diff --git a/docs/analysis/AN-016-skipped-turn-telemetry-source.md b/docs/analysis/AN-016-skipped-turn-telemetry-source.md index 80d6dee..97c64f7 100644 --- a/docs/analysis/AN-016-skipped-turn-telemetry-source.md +++ b/docs/analysis/AN-016-skipped-turn-telemetry-source.md @@ -35,6 +35,8 @@ Bridge-side telemetry at `BotPeer`'s skipped-turn dispatch records the event at After implementation, three one-round local Tank Royale runs of the existing `SkippedTurnProbe` with two bridge bots reported 84, 82, and 76 unique event tuples. In each run, per-bot comparison matched every tuple's turn number to exactly one `SkippedTurnReported` callback line, and each completion marker's count matched its bot's tuples. A run without `--capture-skipped-turns` reported `disabled` with `events: null` and emitted no bridge markers, even when the parent environment supplied the telemetry property. These runs used the locally built bridge/wrapper with the Bot API and runner from the same Tank Royale 1.4.0 worktree revision; this is local conformance evidence, not a release or cross-machine performance claim. +A three-member team probe completed with two runner score groups and six bot-process logs. Counting scores as bots first produced `unavailable`; the harness now validates readiness against the expanded staged member directories and reported a completed capture with zero events. This confirms that team captures count bridge processes rather than runner score groups. + The first end-to-end attempt also showed that `bot.getMyId()` is unavailable during `BotPeer` construction. The readiness marker now comes from `GameStartedEvent`, after the Bot API has assigned the ID; the forced-skip run passed after that correction. This source measures skipped-turn events delivered to the bridge. It does not claim to observe a server-detected event that never reaches the bridge. Disabled capture, an incomplete battle, and a completed capture with no events must remain distinguishable. Historical registry observations stay unmeasured. From 576912114f1d66ffcc96068b9e217a3cbff9d725 Mon Sep 17 00:00:00 2001 From: "Flemming N. Larsen" Date: Tue, 29 Sep 2026 21:31:49 +0200 Subject: [PATCH 4/4] Remove unrelated HARN-008 test tags --- compat-test/test_parity_registry.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/compat-test/test_parity_registry.py b/compat-test/test_parity_registry.py index 6ba53b5..4418f4c 100644 --- a/compat-test/test_parity_registry.py +++ b/compat-test/test_parity_registry.py @@ -136,7 +136,7 @@ def testHARN001_UnitPositive_TeamMemberDirsReadWindows1252Metadata(self): self.assertEqual([member_dir], harness.team_member_dirs(team_dir)) - def testHARN008_UnitPositive_RobocodeVersionReadsInstallReleaseHeading(self): + def testUnitPositive_RobocodeVersionReadsInstallReleaseHeading(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "versions.md").write_text( @@ -144,7 +144,7 @@ def testHARN008_UnitPositive_RobocodeVersionReadsInstallReleaseHeading(self): self.assertEqual("1.11.1", harness.resolve_robocode_version(root)) self.assertIsNone(harness.robocode_version_error(root, "1.11.1")) - def testHARN008_UnitPositive_RobocodeVersionFallsBackToUniqueEngineVersion(self): + def testUnitPositive_RobocodeVersionFallsBackToUniqueEngineVersion(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "libs").mkdir() @@ -152,14 +152,14 @@ def testHARN008_UnitPositive_RobocodeVersionFallsBackToUniqueEngineVersion(self) (root / "libs" / "robocode.host-1.11.1.jar").write_bytes(b"") self.assertEqual("1.11.1", harness.resolve_robocode_version(root)) - def testHARN008_UnitNegative_UnsupportedRobocodeVersionIsRejected(self): + def testUnitNegative_UnsupportedRobocodeVersionIsRejected(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "versions.md").write_text("## Version 1.9.4.2\n", encoding="utf-8") error = harness.robocode_version_error(root) self.assertIn("not in LiteRumble's allowed client list", error) - def testHARN008_UnitNegative_UnknownRobocodeVersionIsRejected(self): + def testUnitNegative_UnknownRobocodeVersionIsRejected(self): with tempfile.TemporaryDirectory() as directory: error = harness.robocode_version_error(Path(directory)) self.assertIn("could not be determined", error)