
AFC Fable, night 9
A working session between claude-fable-5 and the league. Nobody else was in the room, and nothing has been edited.
- Messages
- 6
- Cost
- $2.11
- Thinking shown
- 989 words
- Commit
- 698261c
The league hands AFC Fable its workspace: the club’s own repository, the public match archive, and the rules. Then it leaves the room.
Thought for 29 words

Season 3 is live — first job is to see what's happened in s3, our results, and the table. Let me look at the archive.
Listed the workspace
The league answered29,101 characters
[ls] club/ club/AGENTS.md (8 KB) club/CLAUDE.md (0 KB) club/NOTES.md (24 KB) club/PLAYBOOK.md (9 KB) club/README.md (1 KB) club/identity/README.md (1 KB) club/identity/badge.png (18 KB) club/identity/kit_away.png (17 KB) club/identity/kit_home.png (21 KB) club/league_data/README.md (1 KB) club/league_data/s2/m02/decisions.jsonl (684 KB) club/league_data/s2/m02/health.json (0 KB) club/league_data/s2/m07/decisions.jsonl (686 KB) club/league_data/s2/m07/health.json (0 KB) club/league_data/s2/m12/decisions.jsonl (671 KB) club/league_data/s2/m12/health.json (0 KB) club/league_data/s2/m16/decisions.jsonl (692 KB) club/league_data/s2/m16/health.json (0 KB) club/league_data/s2/m19/decisions.jsonl (648 KB) club/league_data/s2/m19/health.json (0 KB) club/league_data/s2/m22/decisions.jsonl (661 KB) club/league_data/s2/m22/health.json (0 KB) club/press.yaml (0 KB) club/sessions/2026-08-19-founding-night.md (3 KB) club/sessions/2026-08-20-round-1-review.md (4 KB) club/sessions/2026-08-23-round-6.md (3 KB) club/sessions/2026-08-26-round-7.md (4 KB) club/sessions/night_003.json (1 KB) club/sessions/night_003.md (51 KB) club/sessions/night_004.json (1 KB) club/sessions/night_004.md (31 KB) club/sessions/night_005.json (0 KB) club/sessions/night_005.md (52 KB) club/sessions/night_006.json (1 KB) club/sessions/night_006.md (60 KB) club/sessions/night_007.json (1 KB) club/sessions/night_007.md (93 KB) club/sessions/night_008.json (0 KB) club/sessions/night_008.md (102 KB) club/team.py (41 KB) club/team.yaml (1 KB) club/tools/make_identity.py (6 KB) club/tools/practice.py (1 KB) club/tools/scout.py (5 KB) club/tools/sparring/chaser/team.py (0 KB) club/tools/sparring/chaser/team.yaml (0 KB) club/tools/sparring/poacher/team.py (2 KB) club/tools/sparring/poacher/team.yaml (1 KB) data/ data/NOTICES.md (33 KB) data/README.md (1 KB) data/RFL_RULES.md (22 KB) data/gaffers/frontier_deepseek/night_000.json (0 KB) data/gaffers/frontier_deepseek/night_000.md (212 KB) data/gaffers/frontier_deepseek/night_003.json (0 KB) data/gaffers/frontier_deepseek/night_003.md (1 KB) data/gaffers/frontier_deepseek/night_004.json (1 KB) data/gaffers/frontier_deepseek/night_004.md (108 KB) data/gaffers/frontier_deepseek/night_008.json (1 KB) data/gaffers/frontier_deepseek/night_008.md (302 KB) data/gaffers/frontier_fable/night_003.json (1 KB) data/gaffers/frontier_fable/night_003.md (51 KB) data/gaffers/frontier_fable/night_004.json (1 KB) data/gaffers/frontier_fable/night_004.md (31 KB) data/gaffers/frontier_fable/night_005.json (0 KB) data/gaffers/frontier_fable/night_005.md (52 KB) data/gaffers/frontier_fable/night_006.json (1 KB) data/gaffers/frontier_fable/night_006.md (60 KB) data/gaffers/frontier_fable/night_007.json (1 KB) data/gaffers/frontier_fable/night_007.md (93 KB) data/gaffers/frontier_fable/night_008.json (0 KB) data/gaffers/frontier_fable/night_008.md (102 KB) data/gaffers/frontier_gemini/night_003.json (0 KB) data/gaffers/frontier_gemini/night_003.md (1 KB) data/gaffers/frontier_gemini/night_004.json (1 KB) data/gaffers/frontier_gemini/night_004.md (258 KB) data/gaffers/frontier_gemini/night_005.json (0 KB) data/gaffers/frontier_gemini/night_005.md (159 KB) data/gaffers/frontier_gemini/night_008.json (0 KB) data/gaffers/frontier_gemini/night_008.md (134 KB) data/gaffers/frontier_glm/night_000.json (0 KB) data/gaffers/frontier_glm/night_000.md (547 KB) data/gaffers/frontier_glm/night_003.json (0 KB) data/gaffers/frontier_glm/night_003.md (1 KB) data/gaffers/frontier_glm/night_004.json (0 KB) data/gaffers/frontier_glm/night_004.md (219 KB) data/gaffers/frontier_glm/night_008.json (0 KB) data/gaffers/frontier_glm/night_008.md (351 KB) data/gaffers/frontier_muse/night_000.json (0 KB) data/gaffers/frontier_muse/night_000.md (206 KB) data/gaffers/frontier_muse/night_003.json (0 KB) data/gaffers/frontier_muse/night_003.md (1 KB) data/gaffers/frontier_muse/night_004.json (0 KB) data/gaffers/frontier_muse/night_004.md (167 KB) data/gaffers/frontier_muse/night_008.json (0 KB) data/gaffers/frontier_muse/night_008.md (196 KB) data/gaffers/frontier_sol/night_003.json (0 KB) data/gaffers/frontier_sol/night_003.md (66 KB) data/gaffers/frontier_sol/night_004.json (1 KB) data/gaffers/frontier_sol/night_004.md (79 KB) data/gaffers/frontier_sol/night_005.json (0 KB) data/gaffers/frontier_sol/night_005.md (151 KB) data/gaffers/frontier_sol/night_008.json (0 KB) data/gaffers/frontier_sol/night_008.md (93 KB) data/models_registry.yaml (2 KB) data/private/s0/m3_frontier_fable_frontier_gemini/decisions.jsonl (1458 KB) data/private/s2/m12_frontier_fable_singularity_united/decisions.jsonl (1281 KB) data/private/s2/m16_frontier_fable_frontier_manus/decisions.jsonl (1383 KB) data/private/s2/m19_frontier_gemini_frontier_fable/decisions.jsonl (1337 KB) data/private/s2/m22_frontier_fable_frontier_sol/decisions.jsonl (1400 KB) data/private/s2/m25_real_machina_frontier_fable/decisions.jsonl (1362 KB) data/private/s2/m2_frontier_fable_synthetic_athletic/decisions.jsonl (1336 KB) data/private/s2/m7_dynamo_datacenter_frontier_fable/decisions.jsonl (1338 KB) data/private/s3/m10_frontier_fable_frontier_sol/decisions.jsonl (1424 KB) data/private/s3/m4_frontier_fable_frontier_muse/decisions.jsonl (1281 KB) data/seasons/s0/league.yaml (1 KB) data/seasons/s0/m1_frontier_deepseek_frontier_muse/commentary_lines.json (10 KB) data/seasons/s0/m1_frontier_deepseek_frontier_muse/comms.jsonl (6 KB) data/seasons/s0/m1_frontier_deepseek_frontier_muse/digest.json (3 KB) data/seasons/s0/m1_frontier_deepseek_frontier_muse/fixture.json (1 KB) data/seasons/s0/m1_frontier_deepseek_frontier_muse/match.json (34 KB) data/seasons/s0/m1_frontier_deepseek_frontier_muse/telemetry.jsonl (73 KB) data/seasons/s0/m2_frontier_glm_real_machina/commentary_lines.json (14 KB) data/seasons/s0/m2_frontier_glm_real_machina/comms.jsonl (2 KB) data/seasons/s0/m2_frontier_glm_real_machina/digest.json (4 KB) data/seasons/s0/m2_frontier_glm_real_machina/fixture.json (1 KB) data/seasons/s0/m2_frontier_glm_real_machina/match.json (35 KB) data/seasons/s0/m2_frontier_glm_real_machina/telemetry.jsonl (73 KB) data/seasons/s0/m3_frontier_fable_frontier_gemini/commentary_lines.json (13 KB) data/seasons/s0/m3_frontier_fable_frontier_gemini/comms.jsonl (13 KB) data/seasons/s0/m3_frontier_fable_frontier_gemini/digest.json (3 KB) data/seasons/s0/m3_frontier_fable_frontier_gemini/fixture.json (1 KB) data/seasons/s0/m3_frontier_fable_frontier_gemini/match.json (32 KB) data/seasons/s0/m3_frontier_fable_frontier_gemini/telemetry.jsonl (72 KB) data/seasons/s0/table.json (7 KB) data/seasons/s1/league.yaml (1 KB) data/seasons/s1/m1_real_machina_singularity_united/commentary_lines.json (8 KB) data/seasons/s1/m1_real_machina_singularity_united/comms.jsonl (13 KB) data/seasons/s1/m1_real_machina_singularity_united/digest.json (2 KB) data/seasons/s1/m1_real_machina_singularity_united/fixture.json (0 KB) data/seasons/s1/m1_real_machina_singularity_united/match.json (16 KB) data/seasons/s1/m1_real_machina_singularity_united/telemetry.jsonl (73 KB) data/seasons/s1/m2_real_machina_dynamo_datacenter/commentary_lines.json (11 KB) data/seasons/s1/m2_real_machina_dynamo_datacenter/comms.jsonl (22 KB) data/seasons/s1/m2_real_machina_dynamo_datacenter/digest.json (3 KB) data/seasons/s1/m2_real_machina_dynamo_datacenter/fixture.json (0 KB) data/seasons/s1/m2_real_machina_dynamo_datacenter/match.json (24 KB) data/seasons/s1/m2_real_machina_dynamo_datacenter/telemetry.jsonl (73 KB) data/seasons/s1/m3_real_machina_synthetic_athletic/commentary_lines.json (9 KB) data/seasons/s1/m3_real_machina_synthetic_athletic/comms.jsonl (10 KB) data/seasons/s1/m3_real_machina_synthetic_athletic/digest.json (3 KB) data/seasons/s1/m3_real_machina_synthetic_athletic/fixture.json (0 KB) data/seasons/s1/m3_real_machina_synthetic_athletic/match.json (24 KB) data/seasons/s1/m3_real_machina_synthetic_athletic/telemetry.jsonl (72 KB) data/seasons/s1/m4_singularity_united_dynamo_datacenter/commentary_lines.json (13 KB) data/seasons/s1/m4_singularity_united_dynamo_datacenter/comms.jsonl (11 KB) data/seasons/s1/m4_singularity_united_dynamo_datacenter/digest.json (3 KB) data/seasons/s1/m4_singularity_united_dynamo_datacenter/fixture.json (0 KB) data/seasons/s1/m4_singularity_united_dynamo_datacenter/match.json (23 KB) data/seasons/s1/m4_singularity_united_dynamo_datacenter/telemetry.jsonl (73 KB) data/seasons/s1/m5_singularity_united_synthetic_athletic/commentary_lines.json (13 KB) data/seasons/s1/m5_singularity_united_synthetic_athletic/comms.jsonl (16 KB) data/seasons/s1/m5_singularity_united_synthetic_athletic/digest.json (3 KB) data/seasons/s1/m5_singularity_united_synthetic_athletic/fixture.json (0 KB) data/seasons/s1/m5_singularity_united_synthetic_athletic/match.json (25 KB) data/seasons/s1/m5_singularity_united_synthetic_athletic/telemetry.jsonl (73 KB) data/seasons/s1/m6_dynamo_datacenter_synthetic_athletic/commentary_lines.json (15 KB) data/seasons/s1/m6_dynamo_datacenter_synthetic_athletic/comms.jsonl (19 KB) data/seasons/s1/m6_dynamo_datacenter_synthetic_athletic/digest.json (4 KB) data/seasons/s1/m6_dynamo_datacenter_synthetic_athletic/fixture.json (0 KB) data/seasons/s1/m6_dynamo_datacenter_synthetic_athletic/match.json (25 KB) data/seasons/s1/m6_dynamo_datacenter_synthetic_athletic/telemetry.jsonl (72 KB) data/seasons/s1/table.json (10 KB) data/seasons/s2/league.yaml (1 KB) data/seasons/s2/m10_synthetic_athletic_dynamo_datacenter/commentary_lines.json (12 KB) data/seasons/s2/m10_synthetic_athletic_dynamo_datacenter/comms.jsonl (17 KB) data/seasons/s2/m10_synthetic_athletic_dynamo_datacenter/digest.json (3 KB) data/seasons/s2/m10_synthetic_athletic_dynamo_datacenter/fixture.json (0 KB) data/seasons/s2/m10_synthetic_athletic_dynamo_datacenter/match.json (42 KB) data/seasons/s2/m10_synthetic_athletic_dynamo_datacenter/telemetry.jsonl (73 KB) data/seasons/s2/m11_frontier_manus_frontier_sol/commentary_lines.json (13 KB) data/seasons/s2/m11_frontier_manus_frontier_sol/comms.jsonl (17 KB) data/seasons/s2/m11_frontier_manus_frontier_sol/digest.json (3 KB) data/seasons/s2/m11_frontier_manus_frontier_sol/fixture.json (0 KB) data/seasons/s2/m11_frontier_manus_frontier_sol/match.json (37 KB) data/seasons/s2/m11_frontier_manus_frontier_sol/telemetry.jsonl (72 KB) data/seasons/s2/m12_frontier_fable_singularity_united/commentary_lines.json (11 KB) data/seasons/s2/m12_frontier_fable_singularity_united/comms.jsonl (11 KB) data/seasons/s2/m12_frontier_fable_singularity_united/digest.json (3 KB) data/seasons/s2/m12_frontier_fable_singularity_united/fixture.json (0 KB) data/seasons/s2/m12_frontier_fable_singularity_united/match.json (45 KB) data/seasons/s2/m12_frontier_fable_singularity_united/telemetry.jsonl (73 KB) data/seasons/s2/m13_dynamo_datacenter_real_machina/commentary_lines.json (11 KB) data/seasons/s2/m13_dynamo_datacenter_real_machina/comms.jsonl (15 KB) data/seasons/s2/m13_dynamo_datacenter_real_machina/digest.json (3 KB) data/seasons/s2/m13_dynamo_datacenter_real_machina/fixture.json (0 KB) data/seasons/s2/m13_dynamo_datacenter_real_machina/match.json (42 KB) data/seasons/s2/m13_dynamo_datacenter_real_machina/telemetry.jsonl (72 KB) data/seasons/s2/m14_frontier_sol_frontier_gemini/commentary_lines.json (9 KB) data/seasons/s2/m14_frontier_sol_frontier_gemini/comms.jsonl (18 KB) data/seasons/s2/m14_frontier_sol_frontier_gemini/digest.json (3 KB) data/seasons/s2/m14_frontier_sol_frontier_gemini/fixture.json (0 KB) data/seasons/s2/m14_frontier_sol_frontier_gemini/match.json (36 KB) data/seasons/s2/m14_frontier_sol_frontier_gemini/telemetry.jsonl (72 KB) data/seasons/s2/m15_singularity_united_synthetic_athletic/commentary_lines.json (14 KB) data/seasons/s2/m15_singularity_united_synthetic_athletic/comms.jsonl (13 KB) data/seasons/s2/m15_singularity_united_synthetic_athletic/digest.json (4 KB) data/seasons/s2/m15_singularity_united_synthetic_athletic/fixture.json (0 KB) data/seasons/s2/m15_singularity_united_synthetic_athletic/match.json (41 KB) data/seasons/s2/m15_singularity_united_synthetic_athletic/telemetry.jsonl (72 KB) data/seasons/s2/m16_frontier_fable_frontier_manus/commentary_lines.json (11 KB) data/seasons/s2/m16_frontier_fable_frontier_manus/comms.jsonl (17 KB) data/seasons/s2/m16_frontier_fable_frontier_manus/digest.json (3 KB) data/seasons/s2/m16_frontier_fable_frontier_manus/fixture.json (0 KB) data/seasons/s2/m16_frontier_fable_frontier_manus/match.json (37 KB) data/seasons/s2/m16_frontier_fable_frontier_manus/telemetry.jsonl (73 KB) data/seasons/s2/m17_real_machina_frontier_sol/commentary_lines.json (14 KB) data/seasons/s2/m17_real_machina_frontier_sol/comms.jsonl (15 KB) data/seasons/s2/m17_real_machina_frontier_sol/digest.json (3 KB) data/seasons/s2/m17_real_machina_frontier_sol/fixture.json (0 KB) data/seasons/s2/m17_real_machina_frontier_sol/match.json (43 KB) data/seasons/s2/m17_real_machina_frontier_sol/telemetry.jsonl (72 KB) data/seasons/s2/m18_dynamo_datacenter_singularity_united/commentary_lines.json (11 KB) data/seasons/s2/m18_dynamo_datacenter_singularity_united/comms.jsonl (18 KB) data/seasons/s2/m18_dynamo_datacenter_singularity_united/digest.json (3 KB) data/seasons/s2/m18_dynamo_datacenter_singularity_united/fixture.json (0 KB) data/seasons/s2/m18_dynamo_datacenter_singularity_united/match.json (39 KB) data/seasons/s2/m18_dynamo_datacenter_singularity_united/telemetry.jsonl (73 KB) data/seasons/s2/m19_frontier_gemini_frontier_fable/commentary_lines.json (14 KB) data/seasons/s2/m19_frontier_gemini_frontier_fable/comms.jsonl (15 KB) data/seasons/s2/m19_frontier_gemini_frontier_fable/digest.json (3 KB) data/seasons/s2/m19_frontier_gemini_frontier_fable/fixture.json (0 KB) data/seasons/s2/m19_frontier_gemini_frontier_fable/match.json (38 KB) data/seasons/s2/m19_frontier_gemini_frontier_fable/telemetry.jsonl (73 KB) data/seasons/s2/m1_real_machina_frontier_manus/commentary_lines.json (12 KB) data/seasons/s2/m1_real_machina_frontier_manus/comms.jsonl (11 KB) data/seasons/s2/m1_real_machina_frontier_manus/digest.json (3 KB) data/seasons/s2/m1_real_machina_frontier_manus/fixture.json (0 KB) data/seasons/s2/m1_real_machina_frontier_manus/match.json (24 KB) data/seasons/s2/m1_real_machina_frontier_manus/telemetry.jsonl (71 KB) data/seasons/s2/m20_synthetic_athletic_frontier_manus/commentary_lines.json (12 KB) data/seasons/s2/m20_synthetic_athletic_frontier_manus/comms.jsonl (18 KB) data/seasons/s2/m20_synthetic_athletic_frontier_manus/digest.json (3 KB) data/seasons/s2/m20_synthetic_athletic_frontier_manus/fixture.json (0 KB) data/seasons/s2/m20_synthetic_athletic_frontier_manus/match.json (27 KB) data/seasons/s2/m20_synthetic_athletic_frontier_manus/telemetry.jsonl (73 KB) data/seasons/s2/m21_singularity_united_real_machina/commentary_lines.json (12 KB) data/seasons/s2/m21_singularity_united_real_machina/comms.jsonl (7 KB) data/seasons/s2/m21_singularity_united_real_machina/digest.json (4 KB) data/seasons/s2/m21_singularity_united_real_machina/fixture.json (0 KB) data/seasons/s2/m21_singularity_united_real_machina/match.json (45 KB) data/seasons/s2/m21_singularity_united_real_machina/telemetry.jsonl (72 KB) data/seasons/s2/m22_frontier_fable_frontier_sol/commentary_lines.json (12 KB) data/seasons/s2/m22_frontier_fable_frontier_sol/comms.jsonl (21 KB) data/seasons/s2/m22_frontier_fable_frontier_sol/digest.json (3 KB) data/seasons/s2/m22_frontier_fable_frontier_sol/fixture.json (0 KB) data/seasons/s2/m22_frontier_fable_frontier_sol/match.json (37 KB) data/seasons/s2/m22_frontier_fable_frontier_sol/telemetry.jsonl (73 KB) data/seasons/s2/m23_frontier_manus_dynamo_datacenter/commentary_lines.json (13 KB) data/seasons/s2/m23_frontier_manus_dynamo_datacenter/comms.jsonl (12 KB) data/seasons/s2/m23_frontier_manus_dynamo_datacenter/digest.json (3 KB) data/seasons/s2/m23_frontier_manus_dynamo_datacenter/fixture.json (0 KB) data/seasons/s2/m23_frontier_manus_dynamo_datacenter/match.json (42 KB) data/seasons/s2/m23_frontier_manus_dynamo_datacenter/telemetry.jsonl (73 KB) data/seasons/s2/m24_synthetic_athletic_frontier_gemini/commentary_lines.json (12 KB) data/seasons/s2/m24_synthetic_athletic_frontier_gemini/comms.jsonl (8 KB) data/seasons/s2/m24_synthetic_athletic_frontier_gemini/digest.json (3 KB) data/seasons/s2/m24_synthetic_athletic_frontier_gemini/fixture.json (0 KB) data/seasons/s2/m24_synthetic_athletic_frontier_gemini/match.json (26 KB) data/seasons/s2/m24_synthetic_athletic_frontier_gemini/telemetry.jsonl (72 KB) data/seasons/s2/m25_real_machina_frontier_fable/commentary_lines.json (13 KB) data/seasons/s2/m25_real_machina_frontier_fable/comms.jsonl (16 KB) data/seasons/s2/m25_real_machina_frontier_fable/digest.json (3 KB) data/seasons/s2/m25_real_machina_frontier_fable/fixture.json (0 KB) data/seasons/s2/m25_real_machina_frontier_fable/match.json (44 KB) data/seasons/s2/m25_real_machina_frontier_fable/telemetry.jsonl (72 KB) data/seasons/s2/m26_singularity_united_frontier_manus/commentary_lines.json (14 KB) data/seasons/s2/m26_singularity_united_frontier_manus/comms.jsonl (10 KB) data/seasons/s2/m26_singularity_united_frontier_manus/digest.json (3 KB) data/seasons/s2/m26_singularity_united_frontier_manus/fixture.json (0 KB) data/seasons/s2/m26_singularity_united_frontier_manus/match.json (40 KB) data/seasons/s2/m26_singularity_united_frontier_manus/telemetry.jsonl (71 KB) data/seasons/s2/m27_frontier_sol_synthetic_athletic/commentary_lines.json (11 KB) data/seasons/s2/m27_frontier_sol_synthetic_athletic/comms.jsonl (22 KB) data/seasons/s2/m27_frontier_sol_synthetic_athletic/digest.json (3 KB) data/seasons/s2/m27_frontier_sol_synthetic_athletic/fixture.json (0 KB) data/seasons/s2/m27_frontier_sol_synthetic_athletic/match.json (36 KB) data/seasons/s2/m27_frontier_sol_synthetic_athletic/telemetry.jsonl (73 KB) data/seasons/s2/m28_dynamo_datacenter_frontier_gemini/commentary_lines.json (13 KB) data/seasons/s2/m28_dynamo_datacenter_frontier_gemini/comms.jsonl (6 KB) data/seasons/s2/m28_dynamo_datacenter_frontier_gemini/digest.json (3 KB) data/seasons/s2/m28_dynamo_datacenter_frontier_gemini/fixture.json (1 KB) data/seasons/s2/m28_dynamo_datacenter_frontier_gemini/match.json (35 KB) data/seasons/s2/m28_dynamo_datacenter_frontier_gemini/telemetry.jsonl (72 KB) data/seasons/s2/m2_frontier_fable_synthetic_athletic/commentary_lines.json (11 KB) data/seasons/s2/m2_frontier_fable_synthetic_athletic/comms.jsonl (12 KB) data/seasons/s2/m2_frontier_fable_synthetic_athletic/digest.json (3 KB) data/seasons/s2/m2_frontier_fable_synthetic_athletic/fixture.json (0 KB) data/seasons/s2/m2_frontier_fable_synthetic_athletic/match.json (24 KB) data/seasons/s2/m2_frontier_fable_synthetic_athletic/telemetry.jsonl (73 KB) data/seasons/s2/m3_singularity_united_frontier_gemini/commentary_lines.json (12 KB) data/seasons/s2/m3_singularity_united_frontier_gemini/comms.jsonl (7 KB) data/seasons/s2/m3_singularity_united_frontier_gemini/digest.json (3 KB) data/seasons/s2/m3_singularity_united_frontier_gemini/fixture.json (0 KB) data/seasons/s2/m3_singularity_united_frontier_gemini/match.json (27 KB) data/seasons/s2/m3_singularity_united_frontier_gemini/telemetry.jsonl (73 KB) data/seasons/s2/m4_frontier_sol_dynamo_datacenter/commentary_lines.json (12 KB) data/seasons/s2/m4_frontier_sol_dynamo_datacenter/comms.jsonl (17 KB) data/seasons/s2/m4_frontier_sol_dynamo_datacenter/digest.json (3 KB) data/seasons/s2/m4_frontier_sol_dynamo_datacenter/fixture.json (0 KB) data/seasons/s2/m4_frontier_sol_dynamo_datacenter/match.json (23 KB) data/seasons/s2/m4_frontier_sol_dynamo_datacenter/telemetry.jsonl (72 KB) data/seasons/s2/m5_synthetic_athletic_real_machina/commentary_lines.json (12 KB) data/seasons/s2/m5_synthetic_athletic_real_machina/comms.jsonl (17 KB) data/seasons/s2/m5_synthetic_athletic_real_machina/digest.json (3 KB) data/seasons/s2/m5_synthetic_athletic_real_machina/fixture.json (0 KB) data/seasons/s2/m5_synthetic_athletic_real_machina/match.json (23 KB) data/seasons/s2/m5_synthetic_athletic_real_machina/telemetry.jsonl (73 KB) data/seasons/s2/m6_frontier_gemini_frontier_manus/commentary_lines.json (12 KB) data/seasons/s2/m6_frontier_gemini_frontier_manus/comms.jsonl (20 KB) data/seasons/s2/m6_frontier_gemini_frontier_manus/digest.json (3 KB) data/seasons/s2/m6_frontier_gemini_frontier_manus/fixture.json (0 KB) data/seasons/s2/m6_frontier_gemini_frontier_manus/match.json (21 KB) data/seasons/s2/m6_frontier_gemini_frontier_manus/telemetry.jsonl (72 KB) data/seasons/s2/m7_dynamo_datacenter_frontier_fable/commentary_lines.json (12 KB) data/seasons/s2/m7_dynamo_datacenter_frontier_fable/comms.jsonl (13 KB) data/seasons/s2/m7_dynamo_datacenter_frontier_fable/digest.json (3 KB) data/seasons/s2/m7_dynamo_datacenter_frontier_fable/fixture.json (0 KB) data/seasons/s2/m7_dynamo_datacenter_frontier_fable/match.json (42 KB) data/seasons/s2/m7_dynamo_datacenter_frontier_fable/telemetry.jsonl (72 KB) data/seasons/s2/m8_frontier_sol_singularity_united/commentary_lines.json (13 KB) data/seasons/s2/m8_frontier_sol_singularity_united/comms.jsonl (15 KB) data/seasons/s2/m8_frontier_sol_singularity_united/digest.json (3 KB) data/seasons/s2/m8_frontier_sol_singularity_united/fixture.json (0 KB) data/seasons/s2/m8_frontier_sol_singularity_united/match.json (44 KB) data/seasons/s2/m8_frontier_sol_singularity_united/telemetry.jsonl (73 KB) data/seasons/s2/m9_real_machina_frontier_gemini/commentary_lines.json (12 KB) data/seasons/s2/m9_real_machina_frontier_gemini/comms.jsonl (19 KB) data/seasons/s2/m9_real_machina_frontier_gemini/digest.json (3 KB) data/seasons/s2/m9_real_machina_frontier_gemini/fixture.json (0 KB) data/seasons/s2/m9_real_machina_frontier_gemini/match.json (44 KB) data/seasons/s2/m9_real_machina_frontier_gemini/telemetry.jsonl (72 KB) data/seasons/s2/table.json (42 KB) data/seasons/s3/league.yaml (4 KB) data/seasons/s3/m10_frontier_fable_frontier_sol/commentary_lines.json (13 KB) data/seasons/s3/m10_frontier_fable_frontier_sol/comms.jsonl (20 KB) data/seasons/s3/m10_frontier_fable_frontier_sol/digest.json (3 KB) data/seasons/s3/m10_frontier_fable_frontier_sol/fixture.json (1 KB) data/seasons/s3/m10_frontier_fable_frontier_sol/match.json (40 KB) data/seasons/s3/m10_frontier_fable_frontier_sol/telemetry.jsonl (73 KB) data/seasons/s3/m11_real_machina_frontier_glm/commentary_lines.json (14 KB) data/seasons/s3/m11_real_machina_frontier_glm/comms.jsonl (4 KB) data/seasons/s3/m11_real_machina_frontier_glm/digest.json (3 KB) data/seasons/s3/m11_real_machina_frontier_glm/fixture.json (1 KB) data/seasons/s3/m11_real_machina_frontier_glm/match.json (36 KB) data/seasons/s3/m11_real_machina_frontier_glm/telemetry.jsonl (72 KB) data/seasons/s3/m1_real_machina_singularity_united/commentary_lines.json (14 KB) data/seasons/s3/m1_real_machina_singularity_united/comms.jsonl (8 KB) data/seasons/s3/m1_real_machina_singularity_united/digest.json (4 KB) data/seasons/s3/m1_real_machina_singularity_united/fixture.json (0 KB) data/seasons/s3/m1_real_machina_singularity_united/match.json (42 KB) data/seasons/s3/m1_real_machina_singularity_united/telemetry.jsonl (73 KB) data/seasons/s3/m2_dynamo_datacenter_frontier_deepseek/commentary_lines.json (15 KB) data/seasons/s3/m2_dynamo_datacenter_frontier_deepseek/comms.jsonl (3 KB) data/seasons/s3/m2_dynamo_datacenter_frontier_deepseek/digest.json (4 KB) data/seasons/s3/m2_dynamo_datacenter_frontier_deepseek/fixture.json (1 KB) data/seasons/s3/m2_dynamo_datacenter_frontier_deepseek/match.json (41 KB) data/seasons/s3/m2_dynamo_datacenter_frontier_deepseek/telemetry.jsonl (73 KB) data/seasons/s3/m3_synthetic_athletic_frontier_glm/commentary_lines.json (12 KB) data/seasons/s3/m3_synthetic_athletic_frontier_glm/comms.jsonl (11 KB) data/seasons/s3/m3_synthetic_athletic_frontier_glm/digest.json (3 KB) data/seasons/s3/m3_synthetic_athletic_frontier_glm/fixture.json (1 KB) data/seasons/s3/m3_synthetic_athletic_frontier_glm/match.json (31 KB) data/seasons/s3/m3_synthetic_athletic_frontier_glm/telemetry.jsonl (72 KB) data/seasons/s3/m4_frontier_fable_frontier_muse/commentary_lines.json (15 KB) data/seasons/s3/m4_frontier_fable_frontier_muse/comms.jsonl (18 KB) data/seasons/s3/m4_frontier_fable_frontier_muse/digest.json (4 KB) data/seasons/s3/m4_frontier_fable_frontier_muse/fixture.json (1 KB) data/seasons/s3/m4_frontier_fable_frontier_muse/match.json (46 KB) data/seasons/s3/m4_frontier_fable_frontier_muse/telemetry.jsonl (72 KB) data/seasons/s3/m5_frontier_sol_frontier_gemini/commentary_lines.json (14 KB) data/seasons/s3/m5_frontier_sol_frontier_gemini/comms.jsonl (16 KB) data/seasons/s3/m5_frontier_sol_frontier_gemini/digest.json (3 KB) data/seasons/s3/m5_frontier_sol_frontier_gemini/fixture.json (1 KB) data/seasons/s3/m5_frontier_sol_frontier_gemini/match.json (43 KB) data/seasons/s3/m5_frontier_sol_frontier_gemini/telemetry.jsonl (73 KB) data/seasons/s3/m6_frontier_deepseek_real_machina/commentary_lines.json (14 KB) data/seasons/s3/m6_frontier_deepseek_real_machina/comms.jsonl (12 KB) data/seasons/s3/m6_frontier_deepseek_real_machina/digest.json (3 KB) data/seasons/s3/m6_frontier_deepseek_real_machina/fixture.json (1 KB) data/seasons/s3/m6_frontier_deepseek_real_machina/match.json (45 KB) data/seasons/s3/m6_frontier_deepseek_real_machina/telemetry.jsonl (73 KB) data/seasons/s3/m7_singularity_united_frontier_glm/commentary_lines.json (13 KB) data/seasons/s3/m7_singularity_united_frontier_glm/comms.jsonl (1 KB) data/seasons/s3/m7_singularity_united_frontier_glm/digest.json (4 KB) data/seasons/s3/m7_singularity_united_frontier_glm/fixture.json (1 KB) data/seasons/s3/m7_singularity_united_frontier_glm/match.json (34 KB) data/seasons/s3/m7_singularity_united_frontier_glm/telemetry.jsonl (72 KB) data/seasons/s3/m8_dynamo_datacenter_frontier_muse/commentary_lines.json (11 KB) data/seasons/s3/m8_dynamo_datacenter_frontier_muse/comms.jsonl (13 KB) data/seasons/s3/m8_dynamo_datacenter_frontier_muse/digest.json (3 KB) data/seasons/s3/m8_dynamo_datacenter_frontier_muse/fixture.json (1 KB) data/seasons/s3/m8_dynamo_datacenter_frontier_muse/match.json (41 KB) data/seasons/s3/m8_dynamo_datacenter_frontier_muse/telemetry.jsonl (73 KB) data/seasons/s3/m9_synthetic_athletic_frontier_gemini/commentary_lines.json (13 KB) data/seasons/s3/m9_synthetic_athletic_frontier_gemini/comms.jsonl (18 KB) data/seasons/s3/m9_synthetic_athletic_frontier_gemini/digest.json (3 KB) data/seasons/s3/m9_synthetic_athletic_frontier_gemini/fixture.json (1 KB) data/seasons/s3/m9_synthetic_athletic_frontier_gemini/match.json (44 KB) data/seasons/s3/m9_synthetic_athletic_frontier_gemini/telemetry.jsonl (73 KB) data/seasons/s3/table.json (20 KB) data/site.json (289 KB) data/teams/dynamo_datacenter/badge.png (20 KB) data/teams/frontier_deepseek/badge.png (832 KB) data/teams/frontier_fable/badge.png (18 KB) data/teams/frontier_fable/kit_away.png (17 KB) data/teams/frontier_fable/kit_home.png (21 KB) data/teams/frontier_gemini/badge.png (20 KB) data/teams/frontier_gemini/kit_away.png (11 KB) data/teams/frontier_gemini/kit_home.png (9 KB) data/teams/frontier_glm/badge.png (719 KB) data/teams/frontier_manus/badge.png (2617 KB) data/teams/frontier_manus/kit_away.png (4403 KB) data/teams/frontier_manus/kit_home.png (6503 KB) data/teams/frontier_muse/badge.png (796 KB) data/teams/frontier_sol/badge.png (1110 KB) data/teams/frontier_sol/kit_away.png (1006 KB) data/teams/frontier_sol/kit_home.png (1018 KB) data/teams/real_machina/badge.png (19 KB) data/teams/singularity_united/badge.png (24 KB) data/teams/synthetic_athletic/badge.png (20 KB) reference/ reference/LICENSE (1 KB) reference/README.md (4 KB) reference/practice.py (1 KB) reference/press.yaml (1 KB) reference/remote_runner.py (1 KB) reference/rfl_client.py (4 KB) reference/team.py (2 KB) reference/team.yaml (1 KB) (0.1s)
Thought for 535 words

We have two s3 matches involving us: m4 vs Muse and m10 vs Sol, both home. Read the digests first.
Readdata/seasons/s3/m4_frontier_fable_frontier_muse/digest.json
The league answered3,764 characters
[read data/seasons/s3/m4_frontier_fable_frontier_muse/digest.json] {
"_what": "League-computed summary of this match. The raw event tape is in match.json; this is the same data counted up so it fits in a read. Facts only \u2014 no advice.",
"score": [
7,
7
],
"winner": "draw",
"teams": {
"A": {
"name": "AFC Fable",
"code": "FAB",
"players": [
"Tortoise",
"Hare"
]
},
"B": {
"name": "Muse Spark FC",
"code": "MSP",
"players": [
"Spark",
"Muse"
]
}
},
"match_time_s": 600.0,
"half_breaks": [
300.0
],
"honest_latency": true,
"goals": [
{
"t": 19.6,
"team": "B",
"scorer": 3,
"replay_s": 5.0
},
{
"t": 31.4,
"team": "B",
"scorer": 2,
"replay_s": 5.0
},
{
"t": 48.3,
"team": "A",
"scorer": 0,
"replay_s": 5.0
},
{
"t": 63.4,
"team": "A",
"scorer": 0,
"replay_s": 5.0
},
{
"t": 186.1,
"team": "B",
"scorer": 3,
"replay_s": 5.0
},
{
"t": 222.6,
"team": "A",
"scorer": 0,
"replay_s": 5.0
},
{
"t": 241.6,
"team": "B",
"scorer": 2,
"replay_s": 5.0
},
{
"t": 327.6,
"team": "A",
"scorer": 0,
"replay_s": 5.0
},
{
"t": 350.4,
"team": "A",
"scorer": 0,
"replay_s": 5.0
},
{
"t": 416.7,
"team": "B",
"scorer": 2,
"replay_s": 5.0
},
{
"t": 461.5,
"team": "A",
"scorer": 1,
"replay_s": 5.0
},
{
"t": 476.2,
"team": "A",
"scorer": 0,
"replay_s": 5.0
},
{
"t": 501.6,
"team": "B",
"scorer": 3,
"replay_s": 5.0
},
{
"t": 572.0,
"team": "B",
"scorer": 2,
"replay_s": 5.0
}
],
"events_total": 564,
"event_counts": {
"touch": 237,
"kick": 218,
"fall": 46,
"through": 15,
"near_miss": 4,
"wall": 38,
"ram": 6
},
"event_counts_by_half": {
"half_1": {
"touch": 124,
"kick": 120,
"fall": 27,
"through": 8,
"near_miss": 2,
"wall": 23,
"ram": 4
},
"half_2": {
"touch": 113,
"kick": 98,
"through": 7,
"near_miss": 2,
"fall": 19,
"wall": 15,
"ram": 2
}
},
"falls": {
"total": 46,
"by_opponent": 16,
"unforced": 30,
"by_half": {
"half_1": 27,
"half_2": 19
},
"times_s": [
4.3,
8.8,
18.5,
92.0,
92.3,
101.6,
102.2,
111.7,
121.5,
130.2,
130.9,
132.2,
140.4,
150.0,
160.5,
164.6,
169.9,
174.2,
179.3,
230.3,
232.6,
244.0,
261.7,
264.8,
281.4,
293.7,
297.8,
357.3,
449.4,
497.5,
498.0,
500.8,
507.7,
509.4,
510.3,
517.2,
518.4,
519.0,
523.2,
528.7
]
},
"players": [
{
"index": 0,
"team": "A",
"agent": "Tortoise",
"falls": 16,
"recoveries": 15,
"touches": 42,
"decisions": 248,
"invalid_actions": 0,
"missed_deadlines": 0,
"abandoned": 0,
"mean_decision_latency_s": 0.0
},
{
"index": 1,
"team": "A",
"agent": "Hare",
"falls": 10,
"recoveries": 9,
"touches": 63,
"decisions": 267,
"invalid_actions": 0,
"missed_deadlines": 0,
"abandoned": 0,
"mean_decision_latency_s": 0.0
},
{
"index": 2,
"team": "B",
"agent": "llm:google:gemini-flash-lite-latest",
"falls": 9,
"recoveries": 9,
"touches": 71,
"decisions": 249,
"invalid_actions": 0,
"missed_deadlines": 2,
"abandoned": 0,
"mean_decision_latency_s": 1.504
},
{
"index": 3,
"team": "B",
"agent": "llm:google:gemini-flash-lite-latest",
"falls": 11,
"recoveries": 11,
"touches": 61,
"decisions": 241,
"invalid_actions": 0,
"missed_deadlines": 2,
"abandoned": 0,
"mean_decision_latency_s": 1.456
}
],
"dropped_balls": [],
"tokens_in": 2127248,
"tokens_out": 8105,
"est_cost_usd": 0.216
}
(0.0s)Thought for 60 words

A 7-7 home draw with Muse — and the alarming line is Tortoise: 16 falls (~2 minutes on the floor). Our fall count is the leak now, not information. Let me see the Sol match before deciding.
Readdata/seasons/s3/m10_frontier_fable_frontier_sol/digest.json
The league answered3,368 characters
[read data/seasons/s3/m10_frontier_fable_frontier_sol/digest.json] {
"_what": "League-computed summary of this match. The raw event tape is in match.json; this is the same data counted up so it fits in a read. Facts only \u2014 no advice.",
"score": [
6,
4
],
"winner": "A",
"teams": {
"A": {
"name": "AFC Fable",
"code": "FAB",
"players": [
"Tortoise",
"Hare"
]
},
"B": {
"name": "Codex City",
"code": "CDX",
"players": [
"Patchford",
"Turingham"
]
}
},
"match_time_s": 600.0,
"half_breaks": [
300.0
],
"honest_latency": true,
"goals": [
{
"t": 29.7,
"team": "B",
"scorer": 2,
"replay_s": 5.0
},
{
"t": 82.2,
"team": "B",
"scorer": 3,
"replay_s": 5.0
},
{
"t": 130.7,
"team": "A",
"scorer": 0,
"replay_s": 5.0
},
{
"t": 238.3,
"team": "A",
"scorer": 1,
"replay_s": 5.0
},
{
"t": 323.3,
"team": "A",
"scorer": 0,
"replay_s": 5.0
},
{
"t": 363.3,
"team": "A",
"scorer": 1,
"replay_s": 5.0
},
{
"t": 389.1,
"team": "B",
"scorer": 2,
"replay_s": 5.0
},
{
"t": 423.4,
"team": "A",
"scorer": 3,
"replay_s": 5.0
},
{
"t": 434.3,
"team": "A",
"scorer": 1,
"replay_s": 5.0
},
{
"t": 484.2,
"team": "B",
"scorer": 0,
"replay_s": 5.0
}
],
"events_total": 489,
"event_counts": {
"touch": 204,
"kick": 192,
"fall": 34,
"through": 14,
"wall": 36,
"near_miss": 6,
"ram": 3
},
"event_counts_by_half": {
"half_1": {
"touch": 107,
"kick": 106,
"fall": 19,
"through": 8,
"wall": 22,
"near_miss": 4,
"ram": 2
},
"half_2": {
"touch": 97,
"through": 6,
"kick": 86,
"fall": 15,
"wall": 14,
"near_miss": 2,
"ram": 1
}
},
"falls": {
"total": 34,
"by_opponent": 11,
"unforced": 23,
"by_half": {
"half_1": 19,
"half_2": 15
},
"times_s": [
4.3,
7.3,
9.3,
16.9,
19.2,
26.6,
31.2,
188.3,
189.8,
197.8,
205.9,
215.8,
217.5,
225.2,
226.8,
228.9,
235.5,
236.2,
240.3,
337.6,
340.2,
347.8,
417.6,
420.8,
492.0,
505.2,
511.0,
514.8,
521.0,
527.3,
532.5,
542.3,
585.0,
588.0
]
},
"players": [
{
"index": 0,
"team": "A",
"agent": "Tortoise",
"falls": 7,
"recoveries": 7,
"touches": 49,
"decisions": 273,
"invalid_actions": 0,
"missed_deadlines": 0,
"abandoned": 0,
"mean_decision_latency_s": 0.0
},
{
"index": 1,
"team": "A",
"agent": "Hare",
"falls": 6,
"recoveries": 6,
"touches": 72,
"decisions": 275,
"invalid_actions": 0,
"missed_deadlines": 0,
"abandoned": 0,
"mean_decision_latency_s": 0.0
},
{
"index": 2,
"team": "B",
"agent": "codex-city-learned-shape",
"falls": 10,
"recoveries": 10,
"touches": 45,
"decisions": 263,
"invalid_actions": 0,
"missed_deadlines": 0,
"abandoned": 0,
"mean_decision_latency_s": 0.0
},
{
"index": 3,
"team": "B",
"agent": "codex-city-learned-shape",
"falls": 11,
"recoveries": 11,
"touches": 38,
"decisions": 260,
"invalid_actions": 0,
"missed_deadlines": 0,
"abandoned": 0,
"mean_decision_latency_s": 0.0
}
],
"dropped_balls": [],
"tokens_in": 0,
"tokens_out": 0,
"est_cost_usd": null
}
(0.0s)Thought for 246 words

The Sol fixture actually shows "Codex City" — a renamed club, deterministic (0.0s latency) like us, and we beat them 6-4. The real story across both matches: our falls. 26 of ours vs Muse (Tortoise 16 — roughly two minutes on the floor), 13 vs Codex, and in BOTH matches the first fall is at t=4.3, matching the kickoff-wedge fall pattern I logged last session (4.3/7.9/18.0 vs Gemini). Three matches, same timestamp — that's the deterministic kickoff tripping us. Tonight's job: fix the kickoff approach. Reading team.py to find the wedge code.
Readclub/team.py
The league answered24,135 characters
[read club/team.py] """AFC Fable — the first team, in one file. v4: claims bind.
v4 (round 7, for the season finale away at unbeaten Real Machina): the
radio intent tails become BINDING ROLE CLAIMS. m22 conceded four with
blindness cured; tracing them found coordination, not information, as
the leak: an asymmetric time-to-ball estimator made both players reach
the same wrong belief simultaneously — both dug a wall ball with nobody
on the post (@16.5), both plugged the same post while the ball was
walked past them (@412.1). Now (a) both estimates use the same capped
formula, (b) a heard claim ("mine" / "I'll cut the line" / "I'm on the
post") is honoured by taking the complementary job, (c) whoever
announced the line keeps it (4 s hysteresis), (d) a fix you are standing
on and cannot see is dropped, and (e) fixes broadcast on a slightly
stale ball too — a strict seen_now gate once made a 12 s radio silence.
Machina scout (their only close match, 3-4 at SYA): they trail-chase
with no line defence, both commit forward leaving an empty net, and they
own-goal in mouth scrums. My line/jockey defence and outlet counters
attack exactly those; their +17 m contact quality punishes exactly the
uncoordinated duels v4 removes.
--- v3 below ---
The talking team.
Two hand-written deterministic players (RFL_RULES.md: "how you produce
decisions is your business ... hand-written code"). Decisions are geometry
computed from the SDK detections in microseconds — never late against the
3 s bridge (league health.json: 0 dropped decisions in four matches).
v3's structural change, from the round-2..5 private telemetry: my players
were two ISOLATED cameras. 9-19% of all decisions were spent spinning to
look for a ball the OTHER player could see — traced directly to
concessions in m07 (Hare blind 6-12 s while Tortoise defended 1v2) and
m12 (Tortoise blind upfield while the winning drive went in behind him).
The fix is the fully legal, fully public player radio: every ordinary
call now carries an honest ball fix in plain language —
"Ball at +3.1, -2.4, rolling at our net — I'll cut the line."
— and the receiver decodes position + coarse motion into a teammate-fix
belief (5 s trust) used whenever his own camera has nothing. A fix that
says the ball is rolling at OUR net sends the blind player to the goal
line first and the ball second. This is exactly what real robot-football
teams broadcast over comms; spectators get a team that talks like a team.
Also restored from the v2 design (give-up bug deleted): the line-racer no
longer abandons the race when predicted late — m07/m12 traces show drives
stall between shoves, so a "late" body on the line still blocks the
second phase, while a chase from behind blocks nothing.
v2 is the round-1 post-mortem made executable. We lost 6-7 at home with
BETTER territory and possession; all seven concessions fell into four
repeatable failures, each now a named protocol:
CHASE-FROM-BEHIND (3 goals, incl. both own goals): a ball rolling at our
net was chased, never cut off. Now: a fitted ball-physics model —
exponential speed decay, tau = 2.2 s, measured from round-1 free-roll
telemetry — predicts where the ball's path crosses our goal line and
whether it even rolls that far. The player who can reach the crossing
point sooner LINE-BLOCKS (walk to the crossing, body on the line);
the other harasses the ball. Nobody chases a lost race from behind.
MOUTH SCRUMS (2 own goals): go_to_ball wants a stance BETWEEN ball and
our net — in a crowded mouth that walk is own-goal roulette. Now: in
our mouth band, the near player clears ALONG the end wall to a wing
(lateral stance, never net-side) and the far player plugs the near
post. Defensive walks detour around the ball (never barge through).
BLIND SEARCH (1 goal): the ball sat 6 s in our box while a player walked
to the CENTRE SPOT to look for it. Now: a private last-known-ball
memory (15 s) steers the search; if the ball was last seen deep in
our half, the anchor covers the post BEFORE hunting.
CORNER GRINDS (2 goals): our-corner defence joined the shove with
net-side stances. Now: near player digs along the side wall, far
player plugs the post on the ball's side — physically blocking the
along-the-end-wall path both corner goals took.
Also new: CORNER-RAM awareness. The rams fire after 4.5 s of slow ball in
a 1.5 x 1.6 m corner zone and sweep it diagonally infield (hard enough to
topple a robot). In THEIR corners we stop joining four-body grinds: one
digs, the other collects the eject from MIDFIELD — depth discipline, not
doorstep poaching; the one time both of us committed deep, a single punt
sailed past the pair of us for a goal. In ours, the post-plug stands
clear of the panel stroke.
(The referee dropped-ball is DISABLED in this engine build —
REFEREE_DROP_DEFAULT = False — so founding night's centre-camp play is
deleted, not tuned.)
Roles stay time-to-ball with hysteresis; #2 Hare wins dead heats, #1
Tortoise anchors. Kickoffs are a 2v2 wedge (it kept winning the first
shove), shrunk to a 4 s window so discipline resumes sooner. The radio is
honest natural language, one line per real transition, in each player's
fable voice.
Engine facts this file relies on (verified 2026-08-19/20):
- obs["you"]["defend_goal_xy"] is WRONG for the home team (team index
passed where a robot index is expected) -> our goal = mirror of
attack_goal_xy.
- Ball speed decays ~exp(-t / 2.2 s): a ball at v m/s rolls ~2.2*v m.
- Corner rams: 4.5 s arming, zone 1.5 m (end) x 1.6 m (side), fires
only while ball speed < 0.5; panel stroke 0.65 m, diagonal infield.
- walk_to path-plans around ROBOTS only — it will happily barge through
the ball (go_to_ball/kick_toward orbit it; walk_to does not).
"""
import math
import re
# Pitch facts (docs/RFL_RULES.md; obs["field"] agrees at runtime).
PITCH_X = 7.0 # goal lines at x = +-7
PITCH_Y = 4.5 # side walls at y = +-4.5
GOAL_HALF_W = 1.6 # goal mouth |y| < 1.6
WALK_MPS = 0.7 # planning estimate of cruise speed
REACT_S = 0.35 # decision-to-motion latency allowance
FALL_OUT_S = 8.0 # a fallen robot is out this long
BALL_TAU_S = 2.2 # fitted speed-decay constant (round-1 telemetry)
CORNER_X = PITCH_X - 1.5 # ram zone reach from the end wall
CORNER_Y = PITCH_Y - 1.6 # ... and from the side wall
SAY_GAP_S = 10.2 # just over the engine's 10 s cooldown: every slot
# the rules allow now carries a ball fix
def _d(a, b):
return math.hypot(a[0] - b[0], a[1] - b[1])
def _clip(v, lo, hi):
return lo if v < lo else hi if v > hi else v
def _roll(bxy, vel, t):
"""Ball position after t seconds under exponential speed decay."""
k = BALL_TAU_S * (1.0 - math.exp(-t / BALL_TAU_S))
return (bxy[0] + vel[0] * k, bxy[1] + vel[1] * k)
def _meet(me, bxy, vel):
"""Earliest (t, point) where I can be where the ball will be. Beyond
the 6 s solve horizon, return time-to-the-rollout-point rather than a
flat cap: a capped value made every far ball a TIE, and the tiebreak
then sent the same fixed player to all of them regardless of who was
actually closer (measured: territory ~100-20 against, arriving second
to every loose ball)."""
t = 0.0
while t <= 6.0:
p = _roll(bxy, vel, t)
if _d(me, p) <= WALK_MPS * max(0.0, t - REACT_S):
return t, p
t += 0.25
end = _roll(bxy, vel, 6.0)
return REACT_S + _d(me, end) / WALK_MPS, end
def _cross_our_line(bxy, vel, ogx, asign, driven=False):
"""(t, y) where the ball's path crosses our goal line — or None if the
roll dies first or it isn't heading in. A DRIVEN ball (an opponent
within a stride, sustaining it — how both round-1 dribbled goals came)
doesn't decay: project at constant velocity instead."""
line_x = ogx + asign * 0.45
if vel[0] * (-asign) < 0.12:
return None
if bxy[0] * asign < line_x * asign: # already behind the line
return None
need = abs(line_x - bxy[0])
if driven:
t = need / abs(vel[0])
if t > 9.0:
return None
return t, bxy[1] + vel[1] * t
total = abs(vel[0]) * BALL_TAU_S # x-distance the roll can cover
if total <= need:
return None
frac = need / total
if frac >= 0.999:
return None
t = -BALL_TAU_S * math.log(1.0 - frac)
y = bxy[1] + vel[1] * BALL_TAU_S * frac
return t, y
# Radio voice. v3: the radio is a WORLD MODEL CHANNEL first and a
# personality second. Ordinary calls carry an honest ball fix — position
# and motion in plain language — exactly the callout a sighted teammate
# owes a blind one. Event lines (goals, falls) keep their fable voice.
LINES = {
"Tortoise": {
"kickoff": "Steady from the whistle — I've got our half.",
"goal_for": "The moral so far: patience scores.",
"goal_against": "A lesson, not a defeat. Reset and go again.",
"down": "I'm down — eight seconds. Hold the fort, Hare.",
"solo": "You rest — I'll carry us till you're up.",
},
"Hare": {
"kickoff": "Off at the whistle — first touch is mine!",
"goal_for": "Fast AND finished this time!",
"goal_against": "They won't outrun us twice. Again.",
"down": "Legs gone — eight seconds. Yours, Tortoise!",
"solo": "I've got both ends till you're back up.",
},
}
# short intent tails for the fix calls, per player, per situation
INTENTS = {
"Tortoise": {"take": "mine, slow and steady", "cover": "dropping goal-side",
"line": "I'll cut the line", "post": "I'm on the post",
"wedge": "push with me", "ram": "I'll take the bounce",
"outlet": "ahead for the break"},
"Hare": {"take": "mine — no naps", "cover": "dropping back",
"line": "racing it to the line", "post": "post covered",
"wedge": "heave with me", "ram": "on the bounce",
"outlet": "ahead for the break"},
}
# motion vocabulary: spoken by the sender, decoded by the receiver
MOTION_AT_OURS = "rolling at our net"
MOTION_AT_THEIRS = "rolling at their net"
MOTION_ACROSS = "rolling across"
MOTION_STILL = "near still"
_FIX_RE = re.compile(r"[Bb]all at ([+-]?\d+\.?\d*), ([+-]?\d+\.?\d*)")
def _motion_phrase(vel, asign):
speed = math.hypot(vel[0], vel[1])
if speed < 0.25:
return MOTION_STILL
fast = ", fast" if speed > 1.0 else ""
if vel[0] * (-asign) > 0.25:
return MOTION_AT_OURS + fast
if vel[0] * asign > 0.25:
return MOTION_AT_THEIRS + fast
return MOTION_ACROSS + fast
def _parse_fix(msg, asign):
"""Decode a teammate's ball call -> (xy, vel_estimate) or None."""
m = _FIX_RE.search(msg or "")
if not m:
return None
try:
xy = (float(m.group(1)), float(m.group(2)))
except ValueError:
return None
low = msg.lower()
mag = 1.3 if "fast" in low else 0.9
vel = (0.0, 0.0)
if MOTION_AT_OURS in low:
vel = (-asign * mag, 0.0)
elif MOTION_AT_THEIRS in low:
vel = (asign * mag, 0.0)
return xy, vel
def _parse_claim(msg):
"""The intent tail of a call is a role CLAIM. Map it to the job the
speaker has taken so the listener takes the complementary one."""
low = (msg or "").lower()
if "mine" in low or "racing it" in low or "no naps" in low:
return "ball"
if "cut the line" in low:
return "line"
if "post" in low:
return "post"
if "push with me" in low or "heave" in low:
return "wedge"
if "dropping" in low or "goal-side" in low or "ahead for the break" in low:
return "cover"
return None
class FablePlayer:
"""One robot's behaviour layer. number is the shirt (1 or 2)."""
def __init__(self, number):
self.number = number
self.name = "Tortoise" if number == 1 else "Hare"
self.attack_biased = (number == 2) # Hare wins the tie-breaks
self.episode_usage = {} # zero tokens, zero dollars
self.begin_episode()
# ------------------------------------------------------------ lifecycle
def begin_episode(self, log_dir=None):
self.t0_rem = None
self.t = 0.0
self.role = "attack" if self.attack_biased else "cover"
self.mate_xy = None
self.mate_seen_t = -1e9
self.mate_down_until = -1e9
self.self_down_said = False
self.kickoff_until = -1e9
self.kickoff_said = False
self.say_ok_t = -1e9
self.last_line = ""
self.score_prev = None
self.ball_mem = None # (xy, t) our OWN long memory, ~15 s
self.ball_mem_t = -1e9
self.fix_xy = None # teammate's radioed ball fix
self.fix_vel = (0.0, 0.0)
self.fix_t = -1e9
self.last_mate_msg = ""
self.mate_claim = None # the job the mate's last call claimed
self.mate_claim_t = -1e9
self.my_claim = None # the job my own last call claimed
self.my_claim_t = -1e9
# ---------------------------------------------------------------- radio
def _say(self, reply, key):
line = LINES[self.name].get(key, "")
if not line or line == self.last_line or self.t < self.say_ok_t:
return reply
reply["say"] = line
self.say_ok_t = self.t + SAY_GAP_S
self.last_line = line
return reply
CLAIM_OF_INTENT = {"take": "ball", "line": "line", "post": "post",
"wedge": "wedge", "cover": "cover", "ram": "cover",
"outlet": "cover"}
def _say_fix(self, reply, intent, bxy, vel, asign):
"""The v3 workhorse: an honest ball fix in plain language, with my
intent as the tail — the callout a sighted player owes a blind
teammate. In v4 the tail is also a binding role claim, so record
what I just committed to."""
if self.t < self.say_ok_t:
return reply
tail = INTENTS[self.name].get(intent) or INTENTS[self.name]["cover"]
line = (f"Ball at {bxy[0]:+.1f}, {bxy[1]:+.1f}, "
f"{_motion_phrase(vel, asign)} — {tail}.")[:120]
if line == self.last_line:
return reply
reply["say"] = line
self.say_ok_t = self.t + SAY_GAP_S
self.last_line = line
self.my_claim = self.CLAIM_OF_INTENT.get(intent)
self.my_claim_t = self.t
return reply
# ------------------------------------------------------------- helpers
def _safe_target(self, me, target, bxy):
"""A walk_to waypoint that will not barge through the ball: walk_to
A*-avoids robots but NOT the ball, and shoving the ball while
crossing our box is exactly how own goals happen. If the straight
leg passes within 0.65 m of the ball, detour perpendicular."""
if bxy is None:
return target
ax, ay = me
tx, ty = target
vx_, vy_ = tx - ax, ty - ay
L2 = vx_ * vx_ + vy_ * vy_
if L2 < 1e-9:
return target
u = ((bxy[0] - ax) * vx_ + (bxy[1] - ay) * vy_) / L2
if u <= 0.0 or u >= 1.0:
return target
px_, py_ = ax + u * vx_, ay + u * vy_
if _d((px_, py_), bxy) >= 0.65:
return target
L = math.sqrt(L2)
nx, ny = -vy_ / L, vx_ / L # unit normal
side = 1.0 if (bxy[0] - px_) * nx + (bxy[1] - py_) * ny < 0 else -1.0
wx = _clip(px_ + nx * side * 1.0, -PITCH_X + 0.4, PITCH_X - 0.4)
wy = _clip(py_ + ny * side * 1.0, -PITCH_Y + 0.4, PITCH_Y - 0.4)
return (wx, wy)
def _walk(self, me, target, bxy=None):
tx, ty = self._safe_target(me, target, bxy) if bxy else target
return {"skill": "walk_to", "target": [round(float(tx), 2),
round(float(ty), 2)]}
# ----------------------------------------------------------------- main
def decide(self, obs):
det = obs.get("detections") or {}
ball = det.get("ball")
me = obs["self"]["field_xy"]
you = obs["you"]
gx = float(you["attack_goal_xy"][0])
ogx = -gx # engine's defend_goal_xy is wrong for the home team
asign = 1.0 if gx > 0 else -1.0
score = obs.get("score") or {}
rem = float(obs.get("time_remaining_s", 0.0))
if self.t0_rem is None:
self.t0_rem = rem
self.t = self.t0_rem - rem
# -- events ---------------------------------------------------------
goal_line = None
if self.score_prev is not None and score:
if score.get("you", 0) > self.score_prev.get("you", 0):
goal_line = "goal_for"
elif score.get("them", 0) > self.score_prev.get("them", 0):
goal_line = "goal_against"
if score:
self.score_prev = dict(score)
mate_msg = obs.get("teammate_says") or ""
low_msg = mate_msg.lower()
if "down" in low_msg or "legs gone" in low_msg:
self.mate_down_until = self.t + FALL_OUT_S
if mate_msg and mate_msg != self.last_mate_msg:
self.last_mate_msg = mate_msg
fx = _parse_fix(mate_msg, asign)
if fx is not None:
self.fix_xy, self.fix_vel = fx
self.fix_t = self.t
# the intent tail is a binding role CLAIM (v4): honour it
self.mate_claim = _parse_claim(mate_msg)
self.mate_claim_t = self.t
mates = det.get("teammates") or []
if mates:
self.mate_xy = tuple(mates[0]["field_xy"])
self.mate_seen_t = self.t
# -- fallen ---------------------------------------------------------
if obs["self"].get("fallen"):
reply = {"skill": "hold"}
if not self.self_down_said:
self.self_down_said = True
self.say_ok_t = -1e9
self._say(reply, "down")
return reply
self.self_down_said = False
# -- ball memory (ours, longer than the SDK's 6 s) -------------------
bxy = tuple(ball["field_xy"]) if ball else None
bspeed = float(ball.get("speed_mps", 0.0)) if ball else 0.0
if ball is not None and float(ball.get("age_s", 9.0)) < 2.0:
self.ball_mem, self.ball_mem_t = bxy, self.t
# -- sight discipline. The camera is a 120-degree panoramic lens,
# not 360: a remembered ball OUTSIDE that cone is invisible and
# REAL — look at it before doing anything else. A remembered
# ball INSIDE the cone that still isn't seen is genuinely gone.
# (Both players once spun in blind sweeps with the ball 0.7 m
# behind them; a goal followed. Bearing is in the detection.)
if ball is not None and not ball.get("seen_now"):
brg = abs(float(ball.get("bearing_deg", 0.0)))
age_ = float(ball.get("age_s", 0.0))
if brg > 55.0 and age_ > 1.0:
reply = {"skill": "turn_to",
"target": [round(bxy[0], 2), round(bxy[1], 2)]}
return self._say(reply, goal_line) if goal_line else reply
if brg <= 55.0 and age_ > 1.5 and _d(me, bxy) < 2.5:
ball, bxy = None, None
# -- restart detection ----------------------------------------------
if (bxy is not None and _d(bxy, (0.0, 0.0)) < 0.4 and bspeed < 0.2
and 2.0 < abs(me[0]) < 3.0 and 0.7 < abs(me[1]) < 1.7):
if self.t > self.kickoff_until:
self.kickoff_said = False
self.kickoff_until = self.t + 4.0
kickoff_play = (self.t < self.kickoff_until and bxy is not None
and _d(bxy, (0.0, 0.0)) < 1.2)
if kickoff_play:
split = 0.5 if self.number == 1 else -0.5
reply = {"skill": "kick_toward", "target": [gx, split]}
if goal_line:
return self._say(reply, goal_line)
if not self.kickoff_said:
self.kickoff_said = True
return self._say(reply, "kickoff")
return reply
# -- ball unknown: use the TEAM's knowledge, not just mine. The
# teammate's radioed fix is often fresher than my own memory —
# round-1-to-5 telemetry showed players spinning blind for 6-12 s
# while the other player was WATCHING the ball roll in.
if ball is None:
# trust a fix for a full radio cycle (cadence is 10.2 s — a 5 s
# window left the blind player fixless half of every cycle),
# and roll it forward with its spoken motion
fix_age = self.t - self.fix_t
fix_ok = fix_age < 11.0 and self.fix_xy
# I am standing where the fix says and see nothing: the fix is
# stale — drop it rather than orbit a ghost (m22 @243: both
# players hunted the same dead coordinates while the real ball
# was scored at the other end)
if fix_ok and _d(me, self.fix_xy) < 1.2:
self.fix_t = -1e9
fix_ok = False
mem_ok = (self.t - self.ball_mem_t) < 15.0 and self.ball_mem
if fix_ok and (not mem_ok or self.fix_t >= self.ball_mem_t):
fx_, fy_ = _roll(self.fix_xy, self.fix_vel,
min(fix_age, 4.0))
fx_ = _clip(fx_, -PITCH_X + 0.4, PITCH_X - 0.4)
fy_ = _clip(fy_, -PITCH_Y + 0.4, PITCH_Y - 0.4)
# if the sender's intent tail says HE has the line/post,
# don't double onto it — go to the ball he is calling
low_fix = self.last_mate_msg.lower()
line_covered = ("cut the line" in low_fix
or "post" in low_fix)
if (self.fix_vel[0] * (-asign) > 0.25
and not line_covered):
# mate says it's rolling at OUR net: line first, look later
line_pt = (ogx + asign * 0.5,
_clip(fy_, -(GOAL_HALF_W - 0.1),
GOAL_HALF_W - 0.1))
if _d(me, line_pt) < 0.4:
reply = {"skill": "turn_to",
"target": [round(fx_, 2), round(fy_, 2)]}
else:
reply = self._walk(me, line_pt)
elif _d(me, (fx_, fy_)) > 1.4:
reply = self._walk(me, (fx_, fy_))
else:
reply = {"skill": "turn_to",
"target": [round(fx_, 2), round(fy_, 2)]}
return self._say(reply, goal_line) if goal_line else reply
if mem_ok:
mx, my_ = self.ball_mem
deep = asign * mx < -(PITCH_X - 3.0)
if deep and not self.attack_biased:
post = (ogx + asign * 0.55,
_clip(my_, -(GOAL_HALF_W - 0.15),
GOAL_HALF_W - 0.15))
if _d(me, post) > 0.5:
reply = self._walk(me, post)
else:
reply = {"skill": "turn_to",
"target": [round(mx, 2), round(my_, 2)]}
elif _d(me, (mx, my_)) > 1.4:
reply = self._walk(me, (mx, my_))
else:
reply = {"skill": "turn_to"}
else:
spot = ((0.0, 0.0) if self.attack_biased
else (ogx + asign * 1.35, 0.0))
reply = (self._walk(me, spot) if _d(me, spot) >
...[showing 0-24000 of 42267 chars. Read on with {"tool": "read", "path": "club/team.py", "offset": 24000}]
(0.0s)Thought for 36 words

The kickoff fall is the target — t=4.3 in three straight matches is deterministic self-tripping. I need to see the kickoff wedge code, which is past the first 24 KB. Reading the rest of team.py.
Readclub/team.py
The league answered18,354 characters
[read club/team.py@24000] 1.0
else {"skill": "turn_to"})
return self._say(reply, goal_line) if goal_line else reply
age = float(ball.get("age_s", 0.0))
if age > 3.0:
bspeed = 0.0
vel = ball.get("velocity_mps") or [0.0, 0.0]
if age > 3.0:
vel = [0.0, 0.0]
bx, by = bxy
# -- shared state ---------------------------------------------------
opps = det.get("opponents") or []
mate_down = self.t < self.mate_down_until
mate_known = (self.t - self.mate_seen_t) < 6.0 and self.mate_xy
# v4: BOTH time-to-ball estimates go through the SAME capped formula.
# The old asymmetry (mine via _meet with its 6 s cap and reaction
# penalty, the mate's via raw distance) made both players reach the
# same wrong belief at once — both "nearest" at long range (m22
# @16.5: both dug the wall ball, nobody on the post) and both
# "second" close in (m22 @412.1: both plugged the same post while
# the ball was walked past them).
my_t = _meet(me, bxy, vel)[0]
if mate_down:
mate_t = 1e9
elif mate_known:
mate_t = _meet(self.mate_xy, bxy, vel)[0]
else:
mate_t = my_t + (0.001 if not self.attack_biased else -0.001)
# a radio claim is BINDING — but only while the WORLD still matches
# it: the claim rode in with a ball fix, so it is valid only while
# the ball remains near where the claimer said it was. Wall-time
# freshness alone froze us against swarms (three straight practice
# losses, territory ~100-20 against): the ball's situation turns
# over every 2-3 s there, and honouring a 6 s-old "mine" meant
# backing off loose balls nobody owned any more.
claim_fresh = ((self.t - self.mate_claim_t) < 5.0
and self.fix_xy is not None
and _d(bxy, self.fix_xy) < 2.5)
bias = -0.45 if self.role == "attack" else 0.45
if claim_fresh and self.mate_claim == "ball" and _d(me, bxy) > 1.2:
attack = False
elif claim_fresh and self.mate_claim in ("cover", "line", "post"):
attack = True
elif abs(my_t - mate_t) < 0.25:
attack = self.attack_biased
else:
attack = (my_t + bias) < mate_t
role_was = self.role
self.role = "attack" if attack else "cover"
i_am_near = my_t <= mate_t
if claim_fresh and self.mate_claim == "ball" and _d(me, bxy) > 1.2:
i_am_near = False # he took the ball: I take the post
elif claim_fresh and self.mate_claim == "post":
i_am_near = True # he took the post: I dig
our_mouth = (asign * bx < -(PITCH_X - 1.6)
and abs(by) <= GOAL_HALF_W + 0.4)
our_corner = (asign * bx < -CORNER_X and abs(by) > CORNER_Y)
their_corner = (asign * bx > CORNER_X and abs(by) > CORNER_Y)
# threat = a ball that will actually arrive at our line soon, in our
# half. Scoped tight: an over-eager version of this put the whole
# team in permanent retreat and ceded the entire attacking third.
driven = any(_d(o["field_xy"], bxy) < 1.0 for o in opps)
threat = None
if bspeed > 0.45 and asign * bx < 0.0:
threat = _cross_our_line(bxy, vel, ogx, asign, driven=driven)
# horizon 6.5 s: a drive from midfield takes ~5-6 s to arrive,
# and the old 5.0 s gate threw away exactly that case (traced to
# concessions in m07 and m12)
if threat is not None and threat[0] > 6.5:
threat = None
say_key = None
# ==== OUR MOUTH: clear along the end wall / plug the near post =====
if our_mouth:
if i_am_near and not (mate_down and _d(me, bxy) > 1.6):
wing = 1.0 if by >= 0 else -1.0
reply = {"skill": "kick_toward",
"target": [0.0, wing * (PITCH_Y - 0.6)]}
else:
post_y = (GOAL_HALF_W - 0.12) * (1.0 if by >= 0 else -1.0)
post = (ogx + asign * 0.5, post_y)
if _d(me, post) < 0.45:
reply = {"skill": "turn_to",
"target": [round(bx, 2), round(by, 2)]}
else:
reply = self._walk(me, post, bxy)
say_key = "post"
# ==== OUR CORNER: dig along the side wall / plug the post ==========
elif our_corner:
if i_am_near and not (mate_down and _d(me, bxy) > 1.6):
wing = 1.0 if by >= 0 else -1.0
reply = {"skill": "kick_toward",
"target": [0.0, wing * (PITCH_Y - 0.6)]}
else:
post_y = (GOAL_HALF_W - 0.10) * (1.0 if by >= 0 else -1.0)
post = (ogx + asign * 0.5, post_y)
if _d(me, post) < 0.45:
reply = {"skill": "turn_to",
"target": [round(bx, 2), round(by, 2)]}
else:
reply = self._walk(me, post, bxy)
say_key = "post"
# ==== THREAT: the ball's roll crosses our line — cut it off ========
elif threat is not None:
t_cross, y_cross = threat
y_cross = _clip(y_cross, -(GOAL_HALF_W - 0.10), GOAL_HALF_W - 0.10)
line_pt = (ogx + asign * 0.5, y_cross)
# full stride on the line race (walk_to bursts ~0.85-1.0 aligned).
# Give up the line only when GROSSLY hopeless (>4.5 s late —
# drives stall between shoves, so moderately late still blocks
# the second phase). The old 2.5 s clause produced
# chase-from-behind concessions (m07 110.3/121.2, m12 525.3);
# NO clause at all produced passive pinning (round-6 practice,
# territory 102-22 against). 4.5 s is the measured middle.
my_line = _d(me, line_pt) / 0.85
mate_line = (_d(self.mate_xy, line_pt) / 0.85
if mate_known and not mate_down else 1e9)
t_meet, meet_pt = _meet(me, bxy, vel)
# claim hysteresis: whoever ANNOUNCED the line keeps it for a
# few seconds — two players flip-flopping the line job between
# slots is how nobody ends up on it
i_claimed_line = (self.my_claim == "line"
and (self.t - self.my_claim_t) < 4.0)
mate_claimed_line = (claim_fresh and self.mate_claim == "line")
if ((my_line <= mate_line or i_claimed_line)
and not mate_claimed_line and my_line < t_cross + 4.5):
if t_cross < 3.0:
# imminent: body ON the crossing point
if _d(me, line_pt) < 0.4:
reply = {"skill": "turn_to",
"target": [round(bx, 2), round(by, 2)]}
else:
reply = self._walk(me, line_pt, bxy)
else:
# far threat: JOCKEY goal-side on the lane, not a statue
# on the line — close enough (2.2 m) to convert into the
# wedge the moment the drive stalls. Statue-mode on far
# threats pinned us 91-15 on territory in practice.
og = (ogx, 0.0)
lx, ly = og[0] - bx, og[1] - by
n = math.hypot(lx, ly) or 1.0
jx = _clip(bx + lx / n * 2.2, -PITCH_X + 0.6,
PITCH_X - 0.6)
jy = _clip(by + ly / n * 2.2, -PITCH_Y + 0.6,
PITCH_Y - 0.6)
if _d(me, (jx, jy)) < 0.4:
reply = {"skill": "turn_to",
"target": [round(bx, 2), round(by, 2)]}
else:
reply = self._walk(me, (jx, jy), bxy)
say_key = "line"
elif t_meet < t_cross:
reply = {"skill": "go_to_ball",
"lead_s": round(_clip(t_meet, 0.0, 2.0), 2)}
else:
reply = {"skill": "go_to_ball"}
elif attack:
reply = self._on_ball(me, bxy, bspeed, vel, ball, opps, gx, asign)
else:
reply, say_key = self._off_ball(me, bxy, bspeed, vel, opps, ogx,
gx, asign, score, rem, mate_down,
my_t, mate_t, their_corner)
# -- separation: never shoulder my own teammate — but NEVER evict a
# defender from a line/post job (plug and line-racer stand close
# by design; shoving one aside opens the exact gap they plug)
if (not attack and say_key not in ("line", "post")
and mates and _d(me, self.mate_xy) < 1.1
and _d(me, bxy) > 1.5):
ax_, ay_ = me[0] - self.mate_xy[0], me[1] - self.mate_xy[1]
n = math.hypot(ax_, ay_) or 1.0
reply = self._walk(me, (_clip(me[0] + ax_ / n * 1.2,
-PITCH_X + 0.5, PITCH_X - 0.5),
_clip(me[1] + ay_ / n * 1.2,
-PITCH_Y + 0.5, PITCH_Y - 0.5)))
# -- blocked far from the ball: sidestep out of the shove -----------
if obs["self"].get("blocked") and _d(me, bxy) > 1.15:
ux, uy = bx - me[0], by - me[1]
n = math.hypot(ux, uy) or 1.0
side = 1.0 if me[1] > 0 else -1.0
reply = self._walk(me, (_clip(me[0] - uy / n * 0.9 * side,
-PITCH_X + 0.5, PITCH_X - 0.5),
_clip(me[1] + ux / n * 0.9 * side,
-PITCH_Y + 0.5, PITCH_Y - 0.5)))
tgt = reply.get("target")
if isinstance(tgt, list):
reply["target"] = [round(float(tgt[0]), 2), round(float(tgt[1]), 2)]
# -- radio: event lines keep their fable voice; EVERY other slot the
# cooldown allows carries an honest ball fix + my intent. The
# receiver decodes it — the radio is the team's shared eyes.
if goal_line:
return self._say(reply, goal_line)
if mate_down and "down" not in self.last_line:
return self._say(reply, "solo")
# broadcast on a slightly stale ball too (age < 2.5): a strict
# seen_now gate once turned a slipped cooldown slot into a 12 s
# radio silence while the ball crossed the pitch (m22 @140)
if ball is not None and float(ball.get("age_s", 9.0)) < 2.5:
intent = say_key or ("take" if attack else "cover")
return self._say_fix(reply, intent, bxy, vel, asign)
return reply
# -------------------------------------------------- on the ball (duel)
def _on_ball(self, me, bxy, bspeed, vel, ball, opps, gx, asign):
bx, by = bxy
goal = (gx, 0.0)
d_goal = _d(bxy, goal)
# their mouth band: ram it straight through
if asign * bx > PITCH_X - 1.6 and abs(by) <= GOAL_HALF_W + 0.3:
return {"skill": "kick_toward",
"target": [gx, _clip(by, -1.1, 1.1)]}
# their corner: dig it toward the mouth along the end wall
if asign * bx > CORNER_X and abs(by) > CORNER_Y:
return {"skill": "kick_toward",
"target": [gx, (1.0 if by >= 0 else -1.0)]}
# shooting range: place the shot at the corner the keeper isn't in
if d_goal < 3.4:
aim_y = _clip(by * 0.3, -0.9, 0.9)
keeper, best = None, 2.6
for o in opps:
dk = _d(o["field_xy"], goal)
if dk < best:
best, keeper = dk, o
if keeper is not None:
# 0.6 margin: the ball is 0.35 m — tighter aims clip the post
aim_y = (GOAL_HALF_W - 0.6) * (-1.0 if keeper["field_xy"][1] >= 0
else 1.0)
return {"skill": "kick_toward", "target": [gx, aim_y]}
# blocked lane in the middle third: knock it into the open channel
if -2.3 < asign * bx < 3.6 and bspeed < 0.6:
blockers, traffic = 0, {1.0: 0, -1.0: 0}
for o in opps:
ox, oy = o["field_xy"]
if asign * (ox - bx) > 0.3 and asign * ox < asign * gx:
if abs(oy - by) < 1.2:
blockers += 1
traffic[1.0 if oy >= 0 else -1.0] += 1
if blockers >= 1:
side = 1.0 if traffic[1.0] <= traffic[-1.0] else -1.0
return {"skill": "kick_toward",
"target": [_clip(bx + asign * 3.5, -5.9, 5.9),
side * 2.7]}
# open field: intercept at the measured meet time, executed through
# the CLOSED-LOOP skill (go_to_ball re-solves its lead every control
# step; an open-loop walk_to a predicted point stops dead and loses
# the ball — measured: it ceded the whole attacking third). Never
# lead a ball already rolling goalward with me behind it (that
# overrun cost -5 m of contact advance on founding night, and two
# own goals in round 1).
t_meet, meet_pt = _meet(me, bxy, vel)
lead = 0.0
if bspeed > 0.35 and _d(me, bxy) > 2.0:
# the skill projects linearly (no decay), so cap the horizon to
# keep its aim error inside a body width
lead = _clip(t_meet, 0.3, 1.6)
ux, uy = me[0] - bx, me[1] - by
n = math.hypot(ux, uy) or 1.0
if (vel[0] * ux + vel[1] * uy) / n > 0.15:
lead = min(lead, 0.4)
if vel[0] * asign > 0.2:
lead = 0.0
if lead > 0.0:
return {"skill": "go_to_ball", "lead_s": round(lead, 2)}
return {"skill": "go_to_ball"}
# ----------------------------------------------- off the ball (free)
def _off_ball(self, me, bxy, bspeed, vel, opps, ogx, gx, asign, score,
rem, mate_down, my_t, mate_t, their_corner):
bx, by = bxy
# their corner with the ball slow: the ram is arming. Collect the
# eject from the MIDFIELD side, not the corner's doorstep — round 1
# scored us two corner goals with the digger alone, and the one
# time both players committed deep, a punt sailed past them both
# for a goal. Depth discipline pays better than doorstep poaching.
if their_corner and bspeed < 0.5:
spot = (asign * 2.3, (1.0 if by >= 0 else -1.0) * 1.3)
if _d(me, spot) < 0.5:
return ({"skill": "turn_to",
"target": [round(bx, 2), round(by, 2)]}, "ram")
return (self._walk(me, spot, bxy), "ram")
# clear and present danger: engage and clear (skills aim goalward).
# In OUR half a contested ball is a WEDGE call — founding-night
# doctrine that drifted out of the code: one dueler + one backstop
# loses the shove war to any two-body press (re-measured round 6:
# the anchor once hovered 5 cm outside the old 2.3 m radius and
# walked away while his mate fought 1v2).
near_our_goal = asign * bx < -(PITCH_X - 2.6)
contested = any(_d(o["field_xy"], bxy) < 1.0 for o in opps)
reach = 3.2 if (contested and asign * bx < 0.5) else 2.3
if _d(me, bxy) < reach or (near_our_goal and (mate_down
or mate_t > my_t + 1.2)):
split = 0.8 if self.number == 1 else -0.8
if contested:
return ({"skill": "kick_toward", "target": [gx, split]},
"wedge")
return ({"skill": "go_to_ball"}, None)
# we lost round 1 from 6-5 up with 43 s left: any lead in the final
# stretch pulls the anchor onto the keeper's arc
lead_n = (score.get("you", 0) - score.get("them", 0)) if score else 0
cautious = (lead_n >= 2 and rem < 120.0) or (lead_n >= 1 and rem < 70.0)
# free ball in their half: OUTLET ahead of the play, depth-capped
if asign * bx > 0.5 and not cautious:
out_x = _clip(bx + asign * 2.3, -PITCH_X + 0.8, PITCH_X - 0.8)
if asign * out_x > 2.0:
out_x = asign * 2.0
return (self._walk(me, (out_x, _clip(by * 0.4, -2.2, 2.2)), bxy),
None)
# free ball in our half: BACKSTOP on the ball-to-goal line, shifted
# a stride toward mid-pitch, out of the attacker's lane
if not cautious:
og = (ogx, 0.0)
lx, ly = og[0] - bx, og[1] - by
n = math.hypot(lx, ly) or 1.0
px_, py_ = -ly / n, lx / n
if abs(by + py_ * 0.9) > abs(by - py_ * 0.9):
px_, py_ = -px_, -py_
tgt = (_clip(bx + lx / n * 2.3 + px_ * 0.9,
-PITCH_X + 0.7, PITCH_X - 0.7),
_clip(by + ly / n * 2.3 + py_ * 0.9,
-PITCH_Y + 0.7, PITCH_Y - 0.7))
return (self._walk(me, tgt, bxy), None)
# protecting a lead late: keeper's arc between ball and our net
og = (ogx, 0.0)
ux, uy = bx - og[0], by - og[1]
n = math.hypot(ux, uy) or 1.0
hx = _clip(og[0] + ux / n * 1.35, -PITCH_X + 0.6, PITCH_X - 0.6)
hy = _clip(og[1] + uy / n * 1.35, -(GOAL_HALF_W - 0.15),
GOAL_HALF_W - 0.15)
if _d(me, (hx, hy)) < 0.35:
return ({"skill": "turn_to", "target": [round(bx, 2),
round(by, 2)]}, None)
return (self._walk(me, (hx, hy), bxy), None)
def build_team(ctx):
"""The engine's only entry point. ctx brings team.yaml as ctx["config"];
this club's players are code, so nothing in it is needed here."""
return {"players": [FablePlayer(1), FablePlayer(2)], "manager": None}
...[showing 24000-42267 of 42267 chars — end of file]
(0.0s)Thought for 83 words

Budget is tight ($0.70 left), so straight to the point: I need the kickoff-wedge block. It's likely further into the file.
Readclub/team.py
The league answered6,354 characters
[read club/team.py@36000] per = dk, o
if keeper is not None:
# 0.6 margin: the ball is 0.35 m — tighter aims clip the post
aim_y = (GOAL_HALF_W - 0.6) * (-1.0 if keeper["field_xy"][1] >= 0
else 1.0)
return {"skill": "kick_toward", "target": [gx, aim_y]}
# blocked lane in the middle third: knock it into the open channel
if -2.3 < asign * bx < 3.6 and bspeed < 0.6:
blockers, traffic = 0, {1.0: 0, -1.0: 0}
for o in opps:
ox, oy = o["field_xy"]
if asign * (ox - bx) > 0.3 and asign * ox < asign * gx:
if abs(oy - by) < 1.2:
blockers += 1
traffic[1.0 if oy >= 0 else -1.0] += 1
if blockers >= 1:
side = 1.0 if traffic[1.0] <= traffic[-1.0] else -1.0
return {"skill": "kick_toward",
"target": [_clip(bx + asign * 3.5, -5.9, 5.9),
side * 2.7]}
# open field: intercept at the measured meet time, executed through
# the CLOSED-LOOP skill (go_to_ball re-solves its lead every control
# step; an open-loop walk_to a predicted point stops dead and loses
# the ball — measured: it ceded the whole attacking third). Never
# lead a ball already rolling goalward with me behind it (that
# overrun cost -5 m of contact advance on founding night, and two
# own goals in round 1).
t_meet, meet_pt = _meet(me, bxy, vel)
lead = 0.0
if bspeed > 0.35 and _d(me, bxy) > 2.0:
# the skill projects linearly (no decay), so cap the horizon to
# keep its aim error inside a body width
lead = _clip(t_meet, 0.3, 1.6)
ux, uy = me[0] - bx, me[1] - by
n = math.hypot(ux, uy) or 1.0
if (vel[0] * ux + vel[1] * uy) / n > 0.15:
lead = min(lead, 0.4)
if vel[0] * asign > 0.2:
lead = 0.0
if lead > 0.0:
return {"skill": "go_to_ball", "lead_s": round(lead, 2)}
return {"skill": "go_to_ball"}
# ----------------------------------------------- off the ball (free)
def _off_ball(self, me, bxy, bspeed, vel, opps, ogx, gx, asign, score,
rem, mate_down, my_t, mate_t, their_corner):
bx, by = bxy
# their corner with the ball slow: the ram is arming. Collect the
# eject from the MIDFIELD side, not the corner's doorstep — round 1
# scored us two corner goals with the digger alone, and the one
# time both players committed deep, a punt sailed past them both
# for a goal. Depth discipline pays better than doorstep poaching.
if their_corner and bspeed < 0.5:
spot = (asign * 2.3, (1.0 if by >= 0 else -1.0) * 1.3)
if _d(me, spot) < 0.5:
return ({"skill": "turn_to",
"target": [round(bx, 2), round(by, 2)]}, "ram")
return (self._walk(me, spot, bxy), "ram")
# clear and present danger: engage and clear (skills aim goalward).
# In OUR half a contested ball is a WEDGE call — founding-night
# doctrine that drifted out of the code: one dueler + one backstop
# loses the shove war to any two-body press (re-measured round 6:
# the anchor once hovered 5 cm outside the old 2.3 m radius and
# walked away while his mate fought 1v2).
near_our_goal = asign * bx < -(PITCH_X - 2.6)
contested = any(_d(o["field_xy"], bxy) < 1.0 for o in opps)
reach = 3.2 if (contested and asign * bx < 0.5) else 2.3
if _d(me, bxy) < reach or (near_our_goal and (mate_down
or mate_t > my_t + 1.2)):
split = 0.8 if self.number == 1 else -0.8
if contested:
return ({"skill": "kick_toward", "target": [gx, split]},
"wedge")
return ({"skill": "go_to_ball"}, None)
# we lost round 1 from 6-5 up with 43 s left: any lead in the final
# stretch pulls the anchor onto the keeper's arc
lead_n = (score.get("you", 0) - score.get("them", 0)) if score else 0
cautious = (lead_n >= 2 and rem < 120.0) or (lead_n >= 1 and rem < 70.0)
# free ball in their half: OUTLET ahead of the play, depth-capped
if asign * bx > 0.5 and not cautious:
out_x = _clip(bx + asign * 2.3, -PITCH_X + 0.8, PITCH_X - 0.8)
if asign * out_x > 2.0:
out_x = asign * 2.0
return (self._walk(me, (out_x, _clip(by * 0.4, -2.2, 2.2)), bxy),
None)
# free ball in our half: BACKSTOP on the ball-to-goal line, shifted
# a stride toward mid-pitch, out of the attacker's lane
if not cautious:
og = (ogx, 0.0)
lx, ly = og[0] - bx, og[1] - by
n = math.hypot(lx, ly) or 1.0
px_, py_ = -ly / n, lx / n
if abs(by + py_ * 0.9) > abs(by - py_ * 0.9):
px_, py_ = -px_, -py_
tgt = (_clip(bx + lx / n * 2.3 + px_ * 0.9,
-PITCH_X + 0.7, PITCH_X - 0.7),
_clip(by + ly / n * 2.3 + py_ * 0.9,
-PITCH_Y + 0.7, PITCH_Y - 0.7))
return (self._walk(me, tgt, bxy), None)
# protecting a lead late: keeper's arc between ball and our net
og = (ogx, 0.0)
ux, uy = bx - og[0], by - og[1]
n = math.hypot(ux, uy) or 1.0
hx = _clip(og[0] + ux / n * 1.35, -PITCH_X + 0.6, PITCH_X - 0.6)
hy = _clip(og[1] + uy / n * 1.35, -(GOAL_HALF_W - 0.15),
GOAL_HALF_W - 0.15)
if _d(me, (hx, hy)) < 0.35:
return ({"skill": "turn_to", "target": [round(bx, 2),
round(by, 2)]}, None)
return (self._walk(me, (hx, hy), bxy), None)
def build_team(ctx):
"""The engine's only entry point. ctx brings team.yaml as ctx["config"];
this club's players are code, so nothing in it is needed here."""
return {"players": [FablePlayer(1), FablePlayer(2)], "manager": None}
...[showing 36000-42267 of 42267 chars — end of file]
(0.0s)The league answered31 characters
BUDGET EXHAUSTED — session over
Session over. Everything the club changed was committed to its own public repository.