10 Commits

Author SHA1 Message Date
SirStone 96065597c0 fix(divergence): guard reward-normalizer cold start; drop arctanh recovery
Welford variance-collapse divided by 1e-8 producing bit-exact +/−5e8 /
+−1.25e8 poisoned rewards into TD targets; guard skips normalization
until stats meaningful; tanh-inversion removal bounds log-prob path.

Fixes #60.
2026-08-24 01:03:04 +02:00
SirStone eae6fc15a2 research(Evo_Bot): GA/ES parameter recommendations for ~750-weight neuroevolution (#65)
Extracted concrete numbers from 13 papers in docs/papers/neuroevolution/.
Key findings: use CMA-ES or mutation-only truncation GA, mutate ALL weights
(not 5%), sigma=0.005-0.01, pop=64-200, no crossover, single elite.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-23 23:32:10 +02:00
SirStone 81718e3a4c fix(SAC_LSTM_Bot): un-invert dashboard axes — orientation selftest added 2026-08-23 10:47:48 +02:00
SirStone 04c149ea28 refactor(SAC_LSTM_Bot): reconcile graph tooling — dashboard-only output 2026-08-23 10:30:16 +02:00
SirStone 533a146342 feat(SAC_LSTM_Bot): live campaign dashboard — five panels, auto-reload, run-1 comparison dropped 2026-08-23 10:25:51 +02:00
SirStone 03a1853e57 feat(SAC_LSTM_Bot): embedded plain-English reading guides in graphs 2026-08-23 10:11:44 +02:00
SirStone 002a568c7e fix(SAC_LSTM_Bot): atomic+validated SVG output for progress graphs 2026-08-23 10:06:05 +02:00
SirStone f3ac0888bb feat(SAC_LSTM_Bot): readable eval chart — trends primary, raw dots secondary, v1 comparison separated 2026-08-23 09:59:30 +02:00
SirStone 0b0933294a feat(SAC_LSTM_Bot): progress graph tooling + first graphs 2026-08-23 09:08:53 +02:00
SirStone 781e41595e docs(SAC_LSTM_Bot): simple-words story + dictionary for readability 2026-08-23 08:48:37 +02:00
7 changed files with 1574 additions and 8 deletions
+743
View File
@@ -0,0 +1,743 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="2000" viewBox="0 0 1400 2000" font-family="sans-serif">
<rect width="1400" height="2000" fill="white"/>
<text x="700" y="32" text-anchor="middle" font-size="21" font-weight="bold">SAC-LSTM campaign dashboard - live run (current only)</text>
<text x="700" y="56" text-anchor="middle" font-size="12" fill="#555">generated 2026-08-23 10:46:52 - auto-reloads every 60 s (open this file in Chrome)</text>
<text x="70" y="100" font-size="15" font-weight="bold">Test matches - win % vs opponents</text>
<text x="70" y="115" font-size="11" fill="#555">raw dots = single test matches, thick = rolling-mean-10</text>
<line x1="70" y1="720.0" x2="697" y2="720.0" stroke="#dddddd"/>
<line x1="70" y1="600.0" x2="697" y2="600.0" stroke="#dddddd"/>
<line x1="70" y1="480.0" x2="697" y2="480.0" stroke="#dddddd"/>
<line x1="70" y1="360.0" x2="697" y2="360.0" stroke="#dddddd"/>
<line x1="70" y1="240.0" x2="697" y2="240.0" stroke="#dddddd"/>
<line x1="70" y1="120.0" x2="697" y2="120.0" stroke="#dddddd"/>
<line x1="70" y1="720" x2="697" y2="720" stroke="black"/>
<line x1="70" y1="720" x2="70" y2="120" stroke="black"/>
<line x1="128.2" y1="720" x2="128.2" y2="724" stroke="black"/>
<text x="128.2" y="737" text-anchor="middle" font-size="11">10</text>
<line x1="192.8" y1="720" x2="192.8" y2="724" stroke="black"/>
<text x="192.8" y="737" text-anchor="middle" font-size="11">20</text>
<line x1="257.5" y1="720" x2="257.5" y2="724" stroke="black"/>
<text x="257.5" y="737" text-anchor="middle" font-size="11">30</text>
<line x1="322.1" y1="720" x2="322.1" y2="724" stroke="black"/>
<text x="322.1" y="737" text-anchor="middle" font-size="11">40</text>
<line x1="386.7" y1="720" x2="386.7" y2="724" stroke="black"/>
<text x="386.7" y="737" text-anchor="middle" font-size="11">50</text>
<line x1="451.4" y1="720" x2="451.4" y2="724" stroke="black"/>
<text x="451.4" y="737" text-anchor="middle" font-size="11">60</text>
<line x1="516.0" y1="720" x2="516.0" y2="724" stroke="black"/>
<text x="516.0" y="737" text-anchor="middle" font-size="11">70</text>
<line x1="580.6" y1="720" x2="580.6" y2="724" stroke="black"/>
<text x="580.6" y="737" text-anchor="middle" font-size="11">80</text>
<line x1="645.3" y1="720" x2="645.3" y2="724" stroke="black"/>
<text x="645.3" y="737" text-anchor="middle" font-size="11">90</text>
<line x1="66" y1="720.0" x2="70" y2="720.0" stroke="black"/>
<text x="63" y="724.0" text-anchor="end" font-size="11">0</text>
<line x1="66" y1="600.0" x2="70" y2="600.0" stroke="black"/>
<text x="63" y="604.0" text-anchor="end" font-size="11">20</text>
<line x1="66" y1="480.0" x2="70" y2="480.0" stroke="black"/>
<text x="63" y="484.0" text-anchor="end" font-size="11">40</text>
<line x1="66" y1="360.0" x2="70" y2="360.0" stroke="black"/>
<text x="63" y="364.0" text-anchor="end" font-size="11">60</text>
<line x1="66" y1="240.0" x2="70" y2="240.0" stroke="black"/>
<text x="63" y="244.0" text-anchor="end" font-size="11">80</text>
<line x1="66" y1="120.0" x2="70" y2="120.0" stroke="black"/>
<text x="63" y="124.0" text-anchor="end" font-size="11">100</text>
<text x="383" y="753" text-anchor="middle" font-size="12">test match number (each opponent)</text>
<text x="16" y="420" text-anchor="middle" font-size="12" transform="rotate(-90 16 420)">win rate (%)</text>
<circle cx="70.0" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="76.5" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="82.9" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="89.4" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="95.9" cy="540.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="102.3" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="108.8" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="115.2" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="121.7" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="128.2" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="134.6" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="141.1" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="147.6" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="154.0" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="160.5" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="167.0" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="173.4" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="179.9" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="186.4" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="192.8" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="199.3" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="205.7" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="212.2" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="218.7" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="225.1" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="231.6" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="238.1" cy="660.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="244.5" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="251.0" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="257.5" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="263.9" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="270.4" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="276.8" cy="660.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="283.3" cy="660.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="289.8" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="296.2" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="302.7" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="309.2" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="315.6" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="322.1" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="328.6" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="335.0" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="341.5" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="347.9" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="354.4" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="360.9" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="367.3" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="373.8" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="380.3" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="386.7" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="393.2" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="399.7" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="406.1" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="412.6" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="419.1" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="425.5" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="432.0" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="438.4" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="444.9" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="451.4" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="457.8" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="464.3" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="470.8" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="477.2" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="483.7" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="490.2" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="496.6" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="503.1" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="509.5" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="516.0" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="522.5" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="528.9" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="535.4" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="541.9" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="548.3" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="554.8" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="561.3" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="567.7" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="574.2" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="580.6" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="587.1" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="593.6" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="600.0" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="606.5" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="613.0" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="619.4" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="625.9" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="632.4" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="638.8" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="645.3" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="651.8" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="658.2" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="664.7" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="671.1" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="677.6" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="684.1" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="690.5" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<circle cx="697.0" cy="720.0" r="2" fill="#d62728" opacity="0.25"/>
<polyline points="70.0,720.0 76.5,720.0 82.9,720.0 89.4,720.0 95.9,684.0 102.3,690.0 108.8,694.3 115.2,697.5 121.7,700.0 128.2,702.0 134.6,702.0 141.1,702.0 147.6,702.0 154.0,702.0 160.5,720.0 167.0,720.0 173.4,720.0 179.9,720.0 186.4,720.0 192.8,720.0 199.3,720.0 205.7,720.0 212.2,720.0 218.7,720.0 225.1,720.0 231.6,720.0 238.1,714.0 244.5,714.0 251.0,714.0 257.5,714.0 263.9,714.0 270.4,714.0 276.8,708.0 283.3,702.0 289.8,702.0 296.2,702.0 302.7,708.0 309.2,708.0 315.6,708.0 322.1,708.0 328.6,708.0 335.0,708.0 341.5,714.0 347.9,720.0 354.4,720.0 360.9,720.0 367.3,720.0 373.8,720.0 380.3,720.0 386.7,720.0 393.2,720.0 399.7,720.0 406.1,720.0 412.6,720.0 419.1,720.0 425.5,720.0 432.0,720.0 438.4,720.0 444.9,720.0 451.4,720.0 457.8,720.0 464.3,720.0 470.8,720.0 477.2,720.0 483.7,720.0 490.2,720.0 496.6,720.0 503.1,720.0 509.5,720.0 516.0,720.0 522.5,720.0 528.9,720.0 535.4,720.0 541.9,720.0 548.3,720.0 554.8,720.0 561.3,720.0 567.7,720.0 574.2,720.0 580.6,720.0 587.1,720.0 593.6,720.0 600.0,720.0 606.5,720.0 613.0,720.0 619.4,720.0 625.9,720.0 632.4,720.0 638.8,720.0 645.3,720.0 651.8,720.0 658.2,720.0 664.7,720.0 671.1,720.0 677.6,720.0 684.1,720.0 690.5,720.0 697.0,720.0" fill="none" stroke="#d62728" stroke-width="3.5" opacity="1.0"/>
<circle cx="70.0" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="76.5" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="82.9" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="89.4" cy="180.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="95.9" cy="480.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="102.3" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="108.8" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="115.2" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="121.7" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="128.2" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="134.6" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="141.1" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="147.6" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="154.0" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="160.5" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="167.0" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="173.4" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="179.9" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="186.4" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="192.8" cy="360.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="199.3" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="205.7" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="212.2" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="218.7" cy="120.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="225.1" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="231.6" cy="600.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="238.1" cy="600.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="244.5" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="251.0" cy="120.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="257.5" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="263.9" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="270.4" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="276.8" cy="540.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="283.3" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="289.8" cy="600.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="296.2" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="302.7" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="309.2" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="315.6" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="322.1" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="328.6" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="335.0" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="341.5" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="347.9" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="354.4" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="360.9" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="367.3" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="373.8" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="380.3" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="386.7" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="393.2" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="399.7" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="406.1" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="412.6" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="419.1" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="425.5" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="432.0" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="438.4" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="444.9" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="451.4" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="457.8" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="464.3" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="470.8" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="477.2" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="483.7" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="490.2" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="496.6" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="503.1" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="509.5" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="516.0" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="522.5" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="528.9" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="535.4" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="541.9" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="548.3" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="554.8" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="561.3" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="567.7" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="574.2" cy="540.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="580.6" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="587.1" cy="600.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="593.6" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="600.0" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="606.5" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="613.0" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="619.4" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="625.9" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="632.4" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="638.8" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="645.3" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="651.8" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="658.2" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="664.7" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="671.1" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="677.6" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="684.1" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="690.5" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<circle cx="697.0" cy="720.0" r="2" fill="#1f77b4" opacity="0.25"/>
<polyline points="70.0,720.0 76.5,720.0 82.9,720.0 89.4,585.0 95.9,564.0 102.3,590.0 108.8,608.6 115.2,622.5 121.7,633.3 128.2,642.0 134.6,642.0 141.1,642.0 147.6,642.0 154.0,696.0 160.5,720.0 167.0,720.0 173.4,720.0 179.9,720.0 186.4,720.0 192.8,684.0 199.3,684.0 205.7,684.0 212.2,684.0 218.7,624.0 225.1,624.0 231.6,612.0 238.1,600.0 244.5,600.0 251.0,540.0 257.5,576.0 263.9,576.0 270.4,576.0 276.8,558.0 283.3,618.0 289.8,606.0 296.2,618.0 302.7,630.0 309.2,630.0 315.6,690.0 322.1,690.0 328.6,690.0 335.0,690.0 341.5,708.0 347.9,708.0 354.4,720.0 360.9,720.0 367.3,720.0 373.8,720.0 380.3,720.0 386.7,720.0 393.2,720.0 399.7,720.0 406.1,720.0 412.6,720.0 419.1,720.0 425.5,720.0 432.0,720.0 438.4,720.0 444.9,720.0 451.4,720.0 457.8,720.0 464.3,720.0 470.8,720.0 477.2,720.0 483.7,720.0 490.2,720.0 496.6,720.0 503.1,720.0 509.5,720.0 516.0,720.0 522.5,720.0 528.9,720.0 535.4,720.0 541.9,720.0 548.3,720.0 554.8,720.0 561.3,720.0 567.7,720.0 574.2,702.0 580.6,702.0 587.1,690.0 593.6,690.0 600.0,690.0 606.5,690.0 613.0,690.0 619.4,690.0 625.9,690.0 632.4,690.0 638.8,708.0 645.3,708.0 651.8,720.0 658.2,720.0 664.7,720.0 671.1,720.0 677.6,720.0 684.1,720.0 690.5,720.0 697.0,720.0" fill="none" stroke="#1f77b4" stroke-width="3.5" opacity="1.0"/>
<circle cx="70.0" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="76.5" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="82.9" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="89.4" cy="600.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="95.9" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="102.3" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="108.8" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="115.2" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="121.7" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="128.2" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="134.6" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="141.1" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="147.6" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="154.0" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="160.5" cy="660.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="167.0" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="173.4" cy="240.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="179.9" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="186.4" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="192.8" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="199.3" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="205.7" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="212.2" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="218.7" cy="660.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="225.1" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="231.6" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="238.1" cy="360.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="244.5" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="251.0" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="257.5" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="263.9" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="270.4" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="276.8" cy="660.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="283.3" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="289.8" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="296.2" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="302.7" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="309.2" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="315.6" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="322.1" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="328.6" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="335.0" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="341.5" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="347.9" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="354.4" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="360.9" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="367.3" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="373.8" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="380.3" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="386.7" cy="660.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="393.2" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="399.7" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="406.1" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="412.6" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="419.1" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="425.5" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="432.0" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="438.4" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="444.9" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="451.4" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="457.8" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="464.3" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="470.8" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="477.2" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="483.7" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="490.2" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="496.6" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="503.1" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="509.5" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="516.0" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="522.5" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="528.9" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="535.4" cy="660.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="541.9" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="548.3" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="554.8" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="561.3" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="567.7" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="574.2" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="580.6" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="587.1" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="593.6" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="600.0" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="606.5" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="613.0" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="619.4" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="625.9" cy="660.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="632.4" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="638.8" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="645.3" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="651.8" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="658.2" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="664.7" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="671.1" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="677.6" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="684.1" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="690.5" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<circle cx="697.0" cy="720.0" r="2" fill="#2ca02c" opacity="0.25"/>
<polyline points="70.0,720.0 76.5,720.0 82.9,720.0 89.4,690.0 95.9,696.0 102.3,700.0 108.8,702.9 115.2,705.0 121.7,706.7 128.2,708.0 134.6,708.0 141.1,708.0 147.6,708.0 154.0,720.0 160.5,714.0 167.0,714.0 173.4,666.0 179.9,666.0 186.4,666.0 192.8,666.0 199.3,666.0 205.7,666.0 212.2,666.0 218.7,660.0 225.1,666.0 231.6,666.0 238.1,678.0 244.5,678.0 251.0,678.0 257.5,678.0 263.9,678.0 270.4,678.0 276.8,672.0 283.3,678.0 289.8,678.0 296.2,678.0 302.7,714.0 309.2,714.0 315.6,714.0 322.1,714.0 328.6,714.0 335.0,714.0 341.5,720.0 347.9,720.0 354.4,720.0 360.9,720.0 367.3,720.0 373.8,720.0 380.3,720.0 386.7,714.0 393.2,714.0 399.7,714.0 406.1,714.0 412.6,714.0 419.1,714.0 425.5,714.0 432.0,714.0 438.4,714.0 444.9,714.0 451.4,720.0 457.8,720.0 464.3,720.0 470.8,720.0 477.2,720.0 483.7,720.0 490.2,720.0 496.6,720.0 503.1,720.0 509.5,720.0 516.0,720.0 522.5,720.0 528.9,720.0 535.4,714.0 541.9,714.0 548.3,714.0 554.8,714.0 561.3,714.0 567.7,714.0 574.2,714.0 580.6,714.0 587.1,714.0 593.6,714.0 600.0,720.0 606.5,720.0 613.0,720.0 619.4,720.0 625.9,714.0 632.4,714.0 638.8,714.0 645.3,714.0 651.8,714.0 658.2,714.0 664.7,714.0 671.1,714.0 677.6,714.0 684.1,714.0 690.5,720.0 697.0,720.0" fill="none" stroke="#2ca02c" stroke-width="3.5" opacity="1.0"/>
<line x1="82" y1="772" x2="110" y2="772" stroke="#d62728" stroke-width="3"/>
<text x="116" y="776" font-size="12">Corners - 98 evals</text>
<line x1="82" y1="790" x2="110" y2="790" stroke="#1f77b4" stroke-width="3"/>
<text x="116" y="794" font-size="12">Crazy - 98 evals</text>
<line x1="82" y1="808" x2="110" y2="808" stroke="#2ca02c" stroke-width="3"/>
<text x="116" y="812" font-size="12">Target - 98 evals</text>
<text x="747" y="100" font-size="15" font-weight="bold">Real fights - win % per opponent</text>
<text x="747" y="115" font-size="11" fill="#555">training_log.jsonl only - learning in REAL battles, not tests</text>
<line x1="747" y1="720.0" x2="1375" y2="720.0" stroke="#dddddd"/>
<line x1="747" y1="600.0" x2="1375" y2="600.0" stroke="#dddddd"/>
<line x1="747" y1="480.0" x2="1375" y2="480.0" stroke="#dddddd"/>
<line x1="747" y1="360.0" x2="1375" y2="360.0" stroke="#dddddd"/>
<line x1="747" y1="240.0" x2="1375" y2="240.0" stroke="#dddddd"/>
<line x1="747" y1="120.0" x2="1375" y2="120.0" stroke="#dddddd"/>
<line x1="747" y1="720" x2="1375" y2="720" stroke="black"/>
<line x1="747" y1="720" x2="747" y2="120" stroke="black"/>
<line x1="840.9" y1="720" x2="840.9" y2="724" stroke="black"/>
<text x="840.9" y="737" text-anchor="middle" font-size="11">300</text>
<line x1="935.2" y1="720" x2="935.2" y2="724" stroke="black"/>
<text x="935.2" y="737" text-anchor="middle" font-size="11">600</text>
<line x1="1029.4" y1="720" x2="1029.4" y2="724" stroke="black"/>
<text x="1029.4" y="737" text-anchor="middle" font-size="11">900</text>
<line x1="1123.7" y1="720" x2="1123.7" y2="724" stroke="black"/>
<text x="1123.7" y="737" text-anchor="middle" font-size="11">1200</text>
<line x1="1217.9" y1="720" x2="1217.9" y2="724" stroke="black"/>
<text x="1217.9" y="737" text-anchor="middle" font-size="11">1500</text>
<line x1="1312.2" y1="720" x2="1312.2" y2="724" stroke="black"/>
<text x="1312.2" y="737" text-anchor="middle" font-size="11">1800</text>
<line x1="743" y1="720.0" x2="747" y2="720.0" stroke="black"/>
<text x="740" y="724.0" text-anchor="end" font-size="11">0</text>
<line x1="743" y1="600.0" x2="747" y2="600.0" stroke="black"/>
<text x="740" y="604.0" text-anchor="end" font-size="11">20</text>
<line x1="743" y1="480.0" x2="747" y2="480.0" stroke="black"/>
<text x="740" y="484.0" text-anchor="end" font-size="11">40</text>
<line x1="743" y1="360.0" x2="747" y2="360.0" stroke="black"/>
<text x="740" y="364.0" text-anchor="end" font-size="11">60</text>
<line x1="743" y1="240.0" x2="747" y2="240.0" stroke="black"/>
<text x="740" y="244.0" text-anchor="end" font-size="11">80</text>
<line x1="743" y1="120.0" x2="747" y2="120.0" stroke="black"/>
<text x="740" y="124.0" text-anchor="end" font-size="11">100</text>
<text x="1061" y="753" text-anchor="middle" font-size="12">game number (100-game buckets)</text>
<text x="16" y="420" text-anchor="middle" font-size="12" transform="rotate(-90 16 420)">win %</text>
<polyline points="762.4,720.0 793.8,720.0 825.2,720.0 856.6,720.0 888.1,720.0 919.5,720.0 950.9,720.0 982.3,720.0 1013.7,720.0 1045.1,720.0 1076.6,720.0 1108.0,720.0 1139.4,720.0 1170.8,720.0 1202.2,720.0 1233.6,720.0 1265.0,720.0 1296.5,720.0 1327.9,700.0 1359.3,720.0" fill="none" stroke="#1f77b4" stroke-width="3.5" opacity="1.0"/>
<polyline points="825.2,720.0 856.6,720.0 888.1,720.0 919.5,720.0 950.9,720.0 982.3,720.0 1013.7,720.0 1045.1,720.0" fill="none" stroke="#ff7f0e" stroke-width="3.5" opacity="1.0"/>
<polyline points="1108.0,720.0 1139.4,720.0 1170.8,720.0" fill="none" stroke="#ff7f0e" stroke-width="3.5" opacity="1.0"/>
<polyline points="1233.6,720.0 1265.0,720.0 1296.5,720.0 1327.9,720.0 1359.3,720.0" fill="none" stroke="#ff7f0e" stroke-width="3.5" opacity="1.0"/>
<polyline points="762.4,720.0 793.8,720.0" fill="none" stroke="#2ca02c" stroke-width="3.5" opacity="1.0"/>
<polyline points="888.1,720.0 919.5,720.0 950.9,720.0 982.3,720.0 1013.7,720.0 1045.1,720.0" fill="none" stroke="#2ca02c" stroke-width="3.5" opacity="1.0"/>
<polyline points="1202.2,690.0 1233.6,720.0" fill="none" stroke="#2ca02c" stroke-width="3.5" opacity="1.0"/>
<polyline points="1296.5,720.0 1327.9,720.0 1359.3,720.0" fill="none" stroke="#2ca02c" stroke-width="3.5" opacity="1.0"/>
<polyline points="762.4,720.0 793.8,720.0 825.2,720.0 856.6,720.0 888.1,720.0 919.5,720.0 950.9,660.0 982.3,720.0 1013.7,720.0" fill="none" stroke="#9467bd" stroke-width="3.5" opacity="1.0"/>
<polyline points="1139.4,720.0 1170.8,720.0" fill="none" stroke="#9467bd" stroke-width="3.5" opacity="1.0"/>
<polyline points="762.4,720.0 793.8,720.0 825.2,720.0 856.6,720.0 888.1,720.0 919.5,720.0 950.9,720.0 982.3,720.0 1013.7,720.0 1045.1,720.0 1076.6,720.0 1108.0,720.0 1139.4,720.0 1170.8,720.0 1202.2,720.0 1233.6,720.0 1265.0,720.0 1296.5,720.0 1327.9,720.0 1359.3,720.0" fill="none" stroke="#d62728" stroke-width="3.5" opacity="1.0"/>
<line x1="759" y1="772" x2="787" y2="772" stroke="#1f77b4" stroke-width="3"/>
<text x="793" y="776" font-size="12">Crazy (470 games)</text>
<line x1="759" y1="790" x2="787" y2="790" stroke="#ff7f0e" stroke-width="3"/>
<text x="793" y="794" font-size="12">RamFire (450 games)</text>
<line x1="759" y1="808" x2="787" y2="808" stroke="#2ca02c" stroke-width="3"/>
<text x="793" y="812" font-size="12">Target (220 games)</text>
<line x1="759" y1="826" x2="787" y2="826" stroke="#9467bd" stroke-width="3"/>
<text x="793" y="830" font-size="12">SacTwin (190 games)</text>
<line x1="759" y1="844" x2="787" y2="844" stroke="#d62728" stroke-width="3"/>
<text x="793" y="848" font-size="12">Corners (670 games)</text>
<text x="70" y="940" font-size="15" font-weight="bold">Training losses (log scale)</text>
<text x="70" y="955" font-size="11" fill="#555">training_metrics.jsonl - big early spikes are normal</text>
<line x1="70" y1="1428.0" x2="697" y2="1428.0" stroke="#dddddd"/>
<line x1="70" y1="1401.9" x2="697" y2="1401.9" stroke="#dddddd"/>
<line x1="70" y1="1375.8" x2="697" y2="1375.8" stroke="#dddddd"/>
<line x1="70" y1="1349.7" x2="697" y2="1349.7" stroke="#dddddd"/>
<line x1="70" y1="1323.6" x2="697" y2="1323.6" stroke="#dddddd"/>
<line x1="70" y1="1297.4" x2="697" y2="1297.4" stroke="#dddddd"/>
<line x1="70" y1="1271.3" x2="697" y2="1271.3" stroke="#dddddd"/>
<line x1="70" y1="1245.2" x2="697" y2="1245.2" stroke="#dddddd"/>
<line x1="70" y1="1219.1" x2="697" y2="1219.1" stroke="#dddddd"/>
<line x1="70" y1="1193.0" x2="697" y2="1193.0" stroke="#dddddd"/>
<line x1="70" y1="1166.9" x2="697" y2="1166.9" stroke="#dddddd"/>
<line x1="70" y1="1140.8" x2="697" y2="1140.8" stroke="#dddddd"/>
<line x1="70" y1="1114.7" x2="697" y2="1114.7" stroke="#dddddd"/>
<line x1="70" y1="1088.6" x2="697" y2="1088.6" stroke="#dddddd"/>
<line x1="70" y1="1062.4" x2="697" y2="1062.4" stroke="#dddddd"/>
<line x1="70" y1="1036.3" x2="697" y2="1036.3" stroke="#dddddd"/>
<line x1="70" y1="1010.2" x2="697" y2="1010.2" stroke="#dddddd"/>
<line x1="70" y1="984.1" x2="697" y2="984.1" stroke="#dddddd"/>
<line x1="70" y1="958.0" x2="697" y2="958.0" stroke="#dddddd"/>
<line x1="70" y1="1428" x2="697" y2="1428" stroke="black"/>
<line x1="70" y1="1428" x2="70" y2="958" stroke="black"/>
<line x1="70.0" y1="1428" x2="70.0" y2="1432" stroke="black"/>
<text x="70.0" y="1445" text-anchor="middle" font-size="11">1</text>
<line x1="226.8" y1="1428" x2="226.8" y2="1432" stroke="black"/>
<text x="226.8" y="1445" text-anchor="middle" font-size="11">51</text>
<line x1="383.5" y1="1428" x2="383.5" y2="1432" stroke="black"/>
<text x="383.5" y="1445" text-anchor="middle" font-size="11">101</text>
<line x1="540.2" y1="1428" x2="540.2" y2="1432" stroke="black"/>
<text x="540.2" y="1445" text-anchor="middle" font-size="11">151</text>
<line x1="697.0" y1="1428" x2="697.0" y2="1432" stroke="black"/>
<text x="697.0" y="1445" text-anchor="middle" font-size="11">201</text>
<line x1="66" y1="1428.0" x2="70" y2="1428.0" stroke="black"/>
<text x="63" y="1432.0" text-anchor="end" font-size="11">0.1</text>
<line x1="66" y1="1310.5" x2="70" y2="1310.5" stroke="black"/>
<text x="63" y="1314.5" text-anchor="end" font-size="11">3.16e+03</text>
<line x1="66" y1="1193.0" x2="70" y2="1193.0" stroke="black"/>
<text x="63" y="1197.0" text-anchor="end" font-size="11">1e+08</text>
<line x1="66" y1="1075.5" x2="70" y2="1075.5" stroke="black"/>
<text x="63" y="1079.5" text-anchor="end" font-size="11">3.16e+12</text>
<line x1="66" y1="958.0" x2="70" y2="958.0" stroke="black"/>
<text x="63" y="962.0" text-anchor="end" font-size="11">1e+17</text>
<text x="383" y="1461" text-anchor="middle" font-size="12">metric line number</text>
<text x="16" y="1193" text-anchor="middle" font-size="12" transform="rotate(-90 16 1193)">loss (log)</text>
<polyline points="70.0,1366.3 73.1,1368.7 76.3,1365.5 79.4,1360.9 82.5,1355.0 85.7,1351.6 88.8,1353.9 91.9,1357.7 95.1,979.1 98.2,1370.9 101.3,1393.2 104.5,1379.4 107.6,1366.7 110.8,1357.6 113.9,1351.5 117.0,1349.9 120.2,1348.1 123.3,1347.8 126.4,1345.5 129.6,1346.3 132.7,1348.6 135.8,1350.0 139.0,1103.1 142.1,1359.7 145.2,1364.0 148.4,1366.7 151.5,1064.5 154.6,1382.5 157.8,1382.4 160.9,1385.5 164.1,1383.9 167.2,1386.3 170.3,1390.4 173.5,1392.9 176.6,1392.1 179.7,1392.5 182.9,1404.7 186.0,1403.6 189.1,1397.8 192.3,1409.3 195.4,1415.2 198.5,979.1 201.7,979.1 204.8,1391.7 207.9,1385.3 211.1,1375.4 214.2,1372.1 217.3,979.1 220.5,1364.5 223.6,1359.7 226.8,1101.7 229.9,1357.2 233.0,1354.6 236.2,1363.0 239.3,1377.5 242.4,1390.2 245.6,1375.3 248.7,979.1 251.8,979.1 255.0,1353.1 258.1,1350.3 261.2,1352.5 264.4,1352.6 267.5,979.1 270.6,1352.6 273.8,1350.5 276.9,1354.6 280.0,1359.8 283.2,1355.3 286.3,1359.3 289.4,1354.0 292.6,1362.1 295.7,1365.6 298.9,1367.3 302.0,1374.4 305.1,1397.8 308.3,1104.4 311.4,1375.4 314.5,1261.1 317.7,1156.2 320.8,1076.6 323.9,1059.3 327.1,1058.7 330.2,1058.0 333.3,1057.7 336.5,1057.4 339.6,1057.2 342.7,1057.4 345.9,1056.5 349.0,1056.0 352.1,1055.7 355.3,1055.3 358.4,1054.6 361.6,1054.1 364.7,979.2 367.8,979.2 371.0,979.2 374.1,979.3 377.2,1052.4 380.4,1051.9 383.5,1051.6 386.6,1051.2 389.8,1050.7 392.9,1050.2 396.0,1049.7 399.2,1049.3 402.3,1048.8 405.4,1048.3 408.6,1047.8 411.7,1046.9 414.9,1046.4 418.0,1045.9 421.1,1045.5 424.3,1045.0 427.4,1044.6 430.5,1044.1 433.7,1043.6 436.8,1043.1 439.9,979.4 443.1,1042.3 446.2,1042.0 449.3,1041.6 452.5,1041.3 455.6,1040.9 458.7,1040.2 461.9,1039.9 465.0,1039.6 468.1,1039.2 471.3,1038.9 474.4,1038.5 477.6,1038.9 480.7,1037.9 483.8,1037.4 487.0,979.4 490.1,1036.9 493.2,1036.5 496.4,1036.0 499.5,979.4 502.6,1035.4 505.8,1035.1 508.9,1034.7 512.0,1034.4 515.2,1034.0 518.3,1033.7 521.4,1033.2 524.6,1032.8 527.7,979.5 530.8,1032.0 534.0,979.5 537.1,1031.3 540.2,1030.9 543.4,1030.5 546.5,1030.2 549.7,1029.9 552.8,1029.6 555.9,1029.4 559.1,1029.1 562.2,1028.7 565.3,1028.3 568.5,1028.0 571.6,1028.1 574.7,1027.3 577.9,1026.9 581.0,979.6 584.1,1026.2 587.3,1025.6 590.4,1025.3 593.5,1025.0 596.7,979.6 599.8,979.6 603.0,1023.8 606.1,1023.2 609.2,1022.9 612.4,1022.6 615.5,1022.3 618.6,1022.0 621.8,1021.7 624.9,1021.4 628.0,1021.1 631.2,1020.8 634.3,1020.5 637.4,1020.2 640.6,1019.9 643.7,1019.3 646.8,1019.1 650.0,1018.8 653.1,1018.6 656.2,1018.5 659.4,1018.0 662.5,1017.5 665.6,1017.3 668.8,1017.0 671.9,1016.7 675.1,1016.5 678.2,1016.2 681.3,1016.0 684.5,1015.8 687.6,1015.5 690.7,1015.3 693.9,979.8 697.0,1014.8" fill="none" stroke="#1f77b4" stroke-width="1.8" opacity="1.0"/>
<polyline points="70.0,1388.6 73.1,1384.3 76.3,1382.4 79.4,1380.1 82.5,1377.2 85.7,1375.8 88.8,1376.7 91.9,1378.2 95.1,1381.7 98.2,1384.2 101.3,1390.4 104.5,1394.2 107.6,1384.3 110.8,1379.2 113.9,1376.0 117.0,1373.9 120.2,1373.1 123.3,1372.6 126.4,1371.7 129.6,1371.0 132.7,1371.2 135.8,1371.3 139.0,1371.8 142.1,1372.0 145.2,1373.0 148.4,1373.4 151.5,1375.0 154.6,1375.2 157.8,1377.9 160.9,1379.3 164.1,1380.5 167.2,1381.0 170.3,1380.8 173.5,1380.4 176.6,1382.1 179.7,1382.3 182.9,1380.9 186.0,1380.5 189.1,1382.1 192.3,1379.9 195.4,1379.2 198.5,1378.0 201.7,1375.1 204.8,1372.7 207.9,1371.7 211.1,1370.2 214.2,1369.3 217.3,1367.8 220.5,1366.8 223.6,1365.7 226.8,1364.8 229.9,1364.0 233.0,1363.3 236.2,1363.7 239.3,1363.9 242.4,1364.5 245.6,1365.6 248.7,1367.1 251.8,1369.1 255.0,1370.0 258.1,1371.0 261.2,1371.8 264.4,1371.9 267.5,1371.2 270.6,1370.3 273.8,1371.1 276.9,1370.3 280.0,1369.3 283.2,1370.3 286.3,1370.0 289.4,1371.0 292.6,1369.7 295.7,1369.3 298.9,1369.5 302.0,1369.1 305.1,1367.5 308.3,1365.0 311.4,1364.7 314.5,1332.0 317.7,1278.9 320.8,1239.1 323.9,1230.5 327.1,1230.2 330.2,1229.8 333.3,1229.7 336.5,1229.5 339.6,1229.4 342.7,1229.2 345.9,1229.1 349.0,1228.8 352.1,1228.7 355.3,1228.5 358.4,1228.1 361.6,1227.9 364.7,1227.8 367.8,1227.6 371.0,1227.5 374.1,1227.3 377.2,1227.0 380.4,1226.8 383.5,1226.6 386.6,1226.4 389.8,1226.2 392.9,1225.9 396.0,1225.7 399.2,1225.5 402.3,1225.2 405.4,1225.0 408.6,1224.7 411.7,1224.3 414.9,1224.0 418.0,1223.8 421.1,1223.6 424.3,1223.3 427.4,1223.1 430.5,1222.9 433.7,1222.6 436.8,1222.4 439.9,1222.2 443.1,1222.0 446.2,1221.8 449.3,1221.7 452.5,1221.5 455.6,1221.3 458.7,1220.9 461.9,1220.8 465.0,1220.6 468.1,1220.5 471.3,1220.3 474.4,1220.1 477.6,1219.9 480.7,1219.8 483.8,1219.5 487.0,1219.4 490.1,1219.3 493.2,1219.1 496.4,1218.8 499.5,1218.6 502.6,1218.5 505.8,1218.4 508.9,1218.2 512.0,1218.0 515.2,1217.8 518.3,1217.7 521.4,1217.5 524.6,1217.3 527.7,1217.0 530.8,1216.8 534.0,1216.7 537.1,1216.5 540.2,1216.3 543.4,1216.1 546.5,1215.9 549.7,1215.8 552.8,1215.6 555.9,1215.5 559.1,1215.4 562.2,1215.2 565.3,1215.0 568.5,1214.8 571.6,1214.6 574.7,1214.5 577.9,1214.3 581.0,1214.1 584.1,1213.8 587.3,1213.6 590.4,1213.5 593.5,1213.3 596.7,1213.1 599.8,1213.0 603.0,1212.8 606.1,1212.4 609.2,1212.3 612.4,1212.1 615.5,1212.0 618.6,1211.8 621.8,1211.7 624.9,1211.5 628.0,1211.4 631.2,1211.2 634.3,1211.1 637.4,1210.9 640.6,1210.8 643.7,1210.5 646.8,1210.4 650.0,1210.2 653.1,1210.1 656.2,1210.0 659.4,1209.8 662.5,1209.6 665.6,1209.5 668.8,1209.3 671.9,1209.2 675.1,1209.1 678.2,1208.9 681.3,1208.8 684.5,1208.7 687.6,1208.6 690.7,1208.5 693.9,1208.3 697.0,1208.2" fill="none" stroke="#ff7f0e" stroke-width="1.8" opacity="1.0"/>
<line x1="82" y1="1480" x2="110" y2="1480" stroke="#1f77b4" stroke-width="3"/>
<text x="116" y="1484" font-size="12">critic_loss</text>
<line x1="82" y1="1498" x2="110" y2="1498" stroke="#ff7f0e" stroke-width="3"/>
<text x="116" y="1502" font-size="12">|actor_loss|</text>
<text x="747" y="940" font-size="15" font-weight="bold">Alpha temperature</text>
<text x="747" y="955" font-size="11" fill="#555">training_metrics.jsonl - high = exploring, low = exploiting</text>
<line x1="747" y1="1428.0" x2="1375" y2="1428.0" stroke="#dddddd"/>
<line x1="747" y1="1310.5" x2="1375" y2="1310.5" stroke="#dddddd"/>
<line x1="747" y1="1193.0" x2="1375" y2="1193.0" stroke="#dddddd"/>
<line x1="747" y1="1075.5" x2="1375" y2="1075.5" stroke="#dddddd"/>
<line x1="747" y1="958.0" x2="1375" y2="958.0" stroke="#dddddd"/>
<line x1="747" y1="1428" x2="1375" y2="1428" stroke="black"/>
<line x1="747" y1="1428" x2="747" y2="958" stroke="black"/>
<line x1="747.0" y1="1428" x2="747.0" y2="1432" stroke="black"/>
<text x="747.0" y="1445" text-anchor="middle" font-size="11">1</text>
<line x1="904.0" y1="1428" x2="904.0" y2="1432" stroke="black"/>
<text x="904.0" y="1445" text-anchor="middle" font-size="11">51</text>
<line x1="1061.0" y1="1428" x2="1061.0" y2="1432" stroke="black"/>
<text x="1061.0" y="1445" text-anchor="middle" font-size="11">101</text>
<line x1="1218.0" y1="1428" x2="1218.0" y2="1432" stroke="black"/>
<text x="1218.0" y="1445" text-anchor="middle" font-size="11">151</text>
<line x1="1375.0" y1="1428" x2="1375.0" y2="1432" stroke="black"/>
<text x="1375.0" y="1445" text-anchor="middle" font-size="11">201</text>
<line x1="743" y1="1428.0" x2="747" y2="1428.0" stroke="black"/>
<text x="740" y="1432.0" text-anchor="end" font-size="11">0</text>
<line x1="743" y1="1310.5" x2="747" y2="1310.5" stroke="black"/>
<text x="740" y="1314.5" text-anchor="end" font-size="11">0.252</text>
<line x1="743" y1="1193.0" x2="747" y2="1193.0" stroke="black"/>
<text x="740" y="1197.0" text-anchor="end" font-size="11">0.504</text>
<line x1="743" y1="1075.5" x2="747" y2="1075.5" stroke="black"/>
<text x="740" y="1079.5" text-anchor="end" font-size="11">0.756</text>
<line x1="743" y1="958.0" x2="747" y2="958.0" stroke="black"/>
<text x="740" y="962.0" text-anchor="end" font-size="11">1.01</text>
<text x="1061" y="1461" text-anchor="middle" font-size="12">metric line number</text>
<text x="16" y="1193" text-anchor="middle" font-size="12" transform="rotate(-90 16 1193)">alpha</text>
<polyline points="747.0,961.7 750.1,962.3 753.3,962.6 756.4,963.0 759.6,963.6 762.7,964.5 765.8,964.8 769.0,965.1 772.1,965.6 775.3,965.9 778.4,966.5 781.5,967.4 784.7,967.6 787.8,967.0 791.0,966.5 794.1,965.9 797.2,965.4 800.4,964.8 803.5,964.1 806.7,963.6 809.8,963.0 812.9,962.5 816.1,961.9 819.2,961.3 822.4,960.8 825.5,960.5 828.6,959.6 831.8,959.3 834.9,959.8 838.1,960.1 841.2,960.5 844.3,960.8 847.5,961.1 850.6,961.5 853.8,962.0 856.9,962.5 860.0,963.3 863.2,963.9 866.3,964.4 869.5,965.0 872.6,965.5 875.7,966.0 878.9,966.3 882.0,965.9 885.2,965.5 888.3,965.2 891.4,964.9 894.6,964.4 897.7,964.0 900.9,963.5 904.0,963.0 907.1,962.4 910.3,962.0 913.4,961.6 916.6,960.6 919.7,960.1 922.8,959.5 926.0,958.5 929.1,958.0 932.3,958.4 935.4,958.8 938.5,959.4 941.7,960.0 944.8,960.5 948.0,961.5 951.1,962.1 954.2,962.6 957.4,963.2 960.5,963.7 963.7,964.0 966.8,964.6 969.9,965.1 973.1,965.7 976.2,965.9 979.4,965.9 982.5,965.4 985.6,964.3 988.8,963.7 991.9,963.5 995.1,964.1 998.2,964.6 1001.3,964.9 1004.5,965.6 1007.6,966.4 1010.8,966.7 1013.9,967.0 1017.0,967.2 1020.2,967.8 1023.3,968.1 1026.5,968.6 1029.6,969.2 1032.7,969.7 1035.9,970.4 1039.0,971.0 1042.2,971.2 1045.3,971.5 1048.4,971.8 1051.6,972.3 1054.7,972.9 1057.9,973.4 1061.0,973.7 1064.1,974.2 1067.3,974.8 1070.4,975.3 1073.6,975.9 1076.7,976.4 1079.8,977.0 1083.0,977.5 1086.1,978.0 1089.3,979.0 1092.4,979.5 1095.5,980.1 1098.7,980.5 1101.8,981.0 1105.0,981.4 1108.1,981.9 1111.2,982.5 1114.4,983.0 1117.5,983.5 1120.7,983.9 1123.8,984.3 1126.9,984.7 1130.1,985.3 1133.2,985.8 1136.4,986.7 1139.5,987.3 1142.6,987.7 1145.8,988.2 1148.9,988.7 1152.1,989.2 1155.2,989.8 1158.3,990.2 1161.5,990.9 1164.6,991.3 1167.8,991.7 1170.9,992.3 1174.0,993.0 1177.2,993.6 1180.3,994.0 1183.5,994.3 1186.6,994.9 1189.7,995.4 1192.9,995.9 1196.0,996.4 1199.2,996.9 1202.3,997.5 1205.4,998.0 1208.6,998.5 1211.7,999.0 1214.9,999.5 1218.0,1000.0 1221.1,1000.5 1224.3,1000.9 1227.4,1001.3 1230.6,1001.7 1233.7,1002.1 1236.8,1002.5 1240.0,1003.0 1243.1,1003.5 1246.3,1004.0 1249.4,1004.5 1252.5,1005.0 1255.7,1005.5 1258.8,1006.0 1262.0,1006.9 1265.1,1007.4 1268.2,1007.9 1271.4,1008.4 1274.5,1008.9 1277.7,1009.4 1280.8,1010.1 1283.9,1011.1 1287.1,1011.6 1290.2,1012.1 1293.4,1012.6 1296.5,1013.1 1299.6,1013.6 1302.8,1014.1 1305.9,1014.5 1309.1,1015.0 1312.2,1015.7 1315.3,1016.2 1318.5,1016.7 1321.6,1017.6 1324.8,1018.0 1327.9,1018.5 1331.0,1019.0 1334.2,1019.5 1337.3,1020.0 1340.5,1020.8 1343.6,1021.3 1346.7,1021.8 1349.9,1022.3 1353.0,1022.8 1356.2,1023.3 1359.3,1023.6 1362.4,1024.1 1365.6,1024.6 1368.7,1025.1 1371.9,1025.6 1375.0,1026.0" fill="none" stroke="#9467bd" stroke-width="1.8" opacity="1.0"/>
<text x="70" y="1600" font-size="15" font-weight="bold">Throughput - games per hour</text>
<text x="70" y="1615" font-size="11" fill="#555">method: training_metrics.jsonl 'epoch' deltas (training_log.jsonl has no timestamps); 1 row = one 10-game chunk</text>
<line x1="70" y1="1780.0" x2="1375" y2="1780.0" stroke="#dddddd"/>
<line x1="70" y1="1739.5" x2="1375" y2="1739.5" stroke="#dddddd"/>
<line x1="70" y1="1699.0" x2="1375" y2="1699.0" stroke="#dddddd"/>
<line x1="70" y1="1658.5" x2="1375" y2="1658.5" stroke="#dddddd"/>
<line x1="70" y1="1618.0" x2="1375" y2="1618.0" stroke="#dddddd"/>
<line x1="70" y1="1780" x2="1375" y2="1780" stroke="black"/>
<line x1="70" y1="1780" x2="70" y2="1618" stroke="black"/>
<line x1="194.6" y1="1780" x2="194.6" y2="1784" stroke="black"/>
<text x="194.6" y="1797" text-anchor="middle" font-size="11">20</text>
<line x1="325.8" y1="1780" x2="325.8" y2="1784" stroke="black"/>
<text x="325.8" y="1797" text-anchor="middle" font-size="11">40</text>
<line x1="456.9" y1="1780" x2="456.9" y2="1784" stroke="black"/>
<text x="456.9" y="1797" text-anchor="middle" font-size="11">60</text>
<line x1="588.1" y1="1780" x2="588.1" y2="1784" stroke="black"/>
<text x="588.1" y="1797" text-anchor="middle" font-size="11">80</text>
<line x1="719.2" y1="1780" x2="719.2" y2="1784" stroke="black"/>
<text x="719.2" y="1797" text-anchor="middle" font-size="11">100</text>
<line x1="850.4" y1="1780" x2="850.4" y2="1784" stroke="black"/>
<text x="850.4" y="1797" text-anchor="middle" font-size="11">120</text>
<line x1="981.5" y1="1780" x2="981.5" y2="1784" stroke="black"/>
<text x="981.5" y="1797" text-anchor="middle" font-size="11">140</text>
<line x1="1112.7" y1="1780" x2="1112.7" y2="1784" stroke="black"/>
<text x="1112.7" y="1797" text-anchor="middle" font-size="11">160</text>
<line x1="1243.8" y1="1780" x2="1243.8" y2="1784" stroke="black"/>
<text x="1243.8" y="1797" text-anchor="middle" font-size="11">180</text>
<line x1="1375.0" y1="1780" x2="1375.0" y2="1784" stroke="black"/>
<text x="1375.0" y="1797" text-anchor="middle" font-size="11">200</text>
<line x1="66" y1="1780.0" x2="70" y2="1780.0" stroke="black"/>
<text x="63" y="1784.0" text-anchor="end" font-size="11">0</text>
<line x1="66" y1="1739.5" x2="70" y2="1739.5" stroke="black"/>
<text x="63" y="1743.5" text-anchor="end" font-size="11">1688</text>
<line x1="66" y1="1699.0" x2="70" y2="1699.0" stroke="black"/>
<text x="63" y="1703.0" text-anchor="end" font-size="11">3375</text>
<line x1="66" y1="1658.5" x2="70" y2="1658.5" stroke="black"/>
<text x="63" y="1662.5" text-anchor="end" font-size="11">5063</text>
<line x1="66" y1="1618.0" x2="70" y2="1618.0" stroke="black"/>
<text x="63" y="1622.0" text-anchor="end" font-size="11">6750</text>
<text x="722" y="1813" text-anchor="middle" font-size="12">chunk interval (10-game chunks)</text>
<text x="16" y="1699" text-anchor="middle" font-size="12" transform="rotate(-90 16 1699)">games / hour</text>
<circle cx="70.0" cy="1698.0" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="76.6" cy="1774.9" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="83.1" cy="1658.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="89.7" cy="1776.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="96.2" cy="1762.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="102.8" cy="1776.0" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="109.3" cy="1655.7" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="115.9" cy="1775.9" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="122.5" cy="1654.3" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="129.0" cy="1776.0" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="135.6" cy="1762.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="142.1" cy="1773.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="148.7" cy="1689.3" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="155.3" cy="1772.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="161.8" cy="1698.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="168.4" cy="1771.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="174.9" cy="1693.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="181.5" cy="1772.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="188.0" cy="1696.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="194.6" cy="1771.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="201.2" cy="1692.0" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="207.7" cy="1771.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="214.3" cy="1689.3" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="220.8" cy="1771.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="227.4" cy="1653.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="233.9" cy="1773.9" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="240.5" cy="1687.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="247.1" cy="1772.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="253.6" cy="1653.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="260.2" cy="1775.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="266.7" cy="1650.9" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="273.3" cy="1773.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="279.8" cy="1682.0" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="286.4" cy="1774.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="293.0" cy="1687.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="299.5" cy="1774.0" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="306.1" cy="1691.6" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="312.6" cy="1774.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="319.2" cy="1690.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="325.8" cy="1775.7" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="332.3" cy="1690.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="338.9" cy="1776.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="345.4" cy="1659.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="352.0" cy="1772.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="358.5" cy="1645.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="365.1" cy="1773.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="371.7" cy="1680.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="378.2" cy="1772.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="384.8" cy="1666.9" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="391.3" cy="1775.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="397.9" cy="1690.0" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="404.4" cy="1772.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="411.0" cy="1679.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="417.6" cy="1775.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="424.1" cy="1686.0" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="430.7" cy="1775.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="437.2" cy="1762.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="443.8" cy="1775.9" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="450.4" cy="1694.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="456.9" cy="1775.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="463.5" cy="1680.9" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="470.0" cy="1775.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="476.6" cy="1681.3" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="483.1" cy="1775.9" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="489.7" cy="1691.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="496.3" cy="1772.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="502.8" cy="1683.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="509.4" cy="1774.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="515.9" cy="1642.7" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="522.5" cy="1775.3" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="529.0" cy="1683.7" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="535.6" cy="1774.6" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="542.2" cy="1639.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="548.7" cy="1775.6" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="555.3" cy="1696.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="561.8" cy="1775.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="568.4" cy="1691.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="574.9" cy="1773.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="581.5" cy="1696.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="588.1" cy="1771.7" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="594.6" cy="1632.7" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="601.2" cy="1757.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="607.7" cy="1771.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="614.3" cy="1641.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="620.9" cy="1770.0" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="627.4" cy="1650.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="634.0" cy="1771.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="640.5" cy="1639.0" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="647.1" cy="1770.9" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="653.6" cy="1689.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="660.2" cy="1770.7" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="666.8" cy="1705.7" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="673.3" cy="1771.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="679.9" cy="1652.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="686.4" cy="1771.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="693.0" cy="1660.7" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="699.5" cy="1771.9" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="706.1" cy="1681.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="712.7" cy="1770.7" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="719.2" cy="1643.7" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="725.8" cy="1770.3" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="732.3" cy="1690.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="738.9" cy="1771.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="745.5" cy="1684.3" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="752.0" cy="1769.3" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="758.6" cy="1688.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="765.1" cy="1769.9" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="771.7" cy="1695.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="778.2" cy="1773.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="784.8" cy="1694.9" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="791.4" cy="1770.6" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="797.9" cy="1673.3" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="804.5" cy="1770.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="811.0" cy="1676.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="817.6" cy="1771.0" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="824.1" cy="1691.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="830.7" cy="1769.9" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="837.3" cy="1692.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="843.8" cy="1770.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="850.4" cy="1670.9" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="856.9" cy="1770.6" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="863.5" cy="1698.0" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="870.1" cy="1770.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="876.6" cy="1762.9" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="883.2" cy="1770.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="889.7" cy="1688.9" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="896.3" cy="1770.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="902.8" cy="1690.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="909.4" cy="1771.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="916.0" cy="1693.3" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="922.5" cy="1769.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="929.1" cy="1760.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="935.6" cy="1769.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="942.2" cy="1682.6" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="948.7" cy="1770.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="955.3" cy="1761.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="961.9" cy="1770.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="968.4" cy="1680.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="975.0" cy="1769.6" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="981.5" cy="1681.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="988.1" cy="1769.9" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="994.6" cy="1692.7" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1001.2" cy="1771.3" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1007.8" cy="1685.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1014.3" cy="1771.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1020.9" cy="1690.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1027.4" cy="1772.0" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1034.0" cy="1696.3" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1040.6" cy="1770.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1047.1" cy="1682.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1053.7" cy="1771.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1060.2" cy="1673.0" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1066.8" cy="1770.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1073.3" cy="1676.0" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1079.9" cy="1769.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1086.5" cy="1668.3" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1093.0" cy="1771.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1099.6" cy="1694.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1106.1" cy="1771.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1112.7" cy="1695.6" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1119.2" cy="1771.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1125.8" cy="1689.7" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1132.4" cy="1771.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1138.9" cy="1761.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1145.5" cy="1771.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1152.0" cy="1688.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1158.6" cy="1771.7" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1165.2" cy="1684.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1171.7" cy="1772.3" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1178.3" cy="1702.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1184.8" cy="1774.6" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1191.4" cy="1702.0" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1197.9" cy="1772.3" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1204.5" cy="1688.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1211.1" cy="1772.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1217.6" cy="1696.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1224.2" cy="1772.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1230.7" cy="1695.0" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1237.3" cy="1771.9" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1243.8" cy="1707.2" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1250.4" cy="1772.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1257.0" cy="1699.3" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1263.5" cy="1774.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1270.1" cy="1672.7" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1276.6" cy="1771.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1283.2" cy="1689.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1289.7" cy="1771.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1296.3" cy="1696.0" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1302.9" cy="1774.3" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1309.4" cy="1689.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1316.0" cy="1772.3" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1322.5" cy="1687.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1329.1" cy="1771.8" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1335.7" cy="1690.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1342.2" cy="1772.0" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1348.8" cy="1691.0" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1355.3" cy="1772.5" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1361.9" cy="1701.1" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1368.4" cy="1770.4" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<circle cx="1375.0" cy="1682.9" r="1.6" fill="#7f7f7f" opacity="0.25"/>
<polyline points="96.2,1730.7 161.8,1740.2 227.4,1724.1 293.0,1727.4 358.5,1721.3 424.1,1738.6 489.7,1725.4 555.3,1727.8 620.9,1709.3 686.4,1720.0 752.0,1730.7 817.6,1725.8 883.2,1738.6 948.7,1741.6 1014.3,1730.2 1079.9,1726.2 1145.5,1738.5 1211.1,1735.3 1276.6,1731.1 1342.2,1731.1" fill="none" stroke="#2ca02c" stroke-width="3.5" opacity="1.0"/>
<rect x="0" y="1820" width="1400" height="180" fill="#f2f2f2"/>
<text x="16" y="1841" font-size="15" font-weight="bold">How to read</text>
<text x="1384" y="1840" text-anchor="end" font-size="11" fill="#666">Regenerate anytime: python3 tools/plot_progress.py</text>
<text x="16" y="1863" font-size="13">Test matches: dots are single fights, thick line shows trend.</text>
<text x="16" y="1880" font-size="13">Real battles only. Rising lines mean the bot improves.</text>
<text x="16" y="1897" font-size="13">Loss spikes are normal early; endless growth is bad.</text>
<text x="16" y="1914" font-size="13">Alpha high means experimenting; falling too fast freezes habits.</text>
<text x="16" y="1931" font-size="13">Throughput flat is healthy; dips mean something slowed.</text>
<text x="16" y="1948" font-size="13">This file reloads itself in Chrome every sixty seconds.</text>
<text x="16" y="1965" font-size="13">Regenerate anytime with tools/watch_dashboard.sh or the python command.</text>
<script type="text/javascript"><![CDATA[ setTimeout(function(){ location.reload(); }, 60000); ]]></script>
</svg>

After

Width:  |  Height:  |  Size: 63 KiB

+57
View File
@@ -1,3 +1,46 @@
## The story so far, in simple words
This project trains a robot tank. It plays many fights against other tanks.
After each fight it changes itself a little. It keeps the changes that helped it win.
Night 1 (run 1) finished without problems. It ran for 14 hours alone. It never crashed.
It beat an old copy of itself most of the time. It beat Crazy about half the time.
Three things went badly. First, learning was not stable. Good skill appeared, then disappeared again.
Second, the saved "best" version came from one lucky perfect score. It was not really its best.
Third, the bot learned to hide and survive. It almost never shot back.
The human approved five fixes. All five were put into the code.
Run 2 used these fixes. Its error numbers grew far too big. Learning broke.
We made one speed number smaller. This number sets how fast one part learns.
Then we dropped the broken progress and started clean. This is run 3. It is running now.
Next we watch run 3. One of three doors will open.
Door 1: it stays steady. We let it run to the end.
Door 2: the numbers grow too big again. We turn the next speed number down.
Door 3: it stays steady but still fights badly. We teach aiming as a separate, direct lesson.
Updated: 2026-08-23 — this section is refreshed at every major step.
## Small dictionary
- **training**: the time when the bot plays fights and changes itself to improve. It learns only during training.
- **battle**: one group of fights against one opponent. The bot restarts between groups.
- **round**: one single fight. Win it by destroying the enemy tank or outliving it.
- **chunk**: one work block: a battle of up to 10 rounds, then some learning from it.
- **eval (test match)**: a test match. The bot does not learn during these. We use them only to measure.
- **win rate**: how many test matches were won, as a percent. 8 wins in 10 matches = 80%.
- **checkpoint**: a saved copy of the bot's brain (a zip file). Written every few learning steps.
- **"best" checkpoint**: the saved copy we currently call best. Run 1 picked one from a lucky score, hence the quotes.
- **replay buffer**: the bot's memory of past moments: what it saw, did, and received. Learning picks old moments from it.
- **loss (critic/actor)**: a number saying how wrong the bot's inner guesses are. Lower usually means better. Losses growing huge mean trouble.
- **alpha**: a dial setting how much the bot tries new moves instead of repeating known good ones.
- **MA / composite score**: MA is the average of the last few win rates; it smooths luck. Composite is the average of MAs across all test opponents.
- **twin (SacTwin)**: a frozen copy of our own bot, used as a practice partner. Beating it proves real improvement.
- **lever**: one numbered change we prepared, waiting for approval. There are levers 1 to 5.
- **watchman**: a helper who checks the running training at set times and stops it if something breaks.
---
# Campaign Notebook — campaign-v1 (SAC_LSTM_Bot)
Overnight training campaign on branch `research/goto-controller`.
@@ -44,6 +87,8 @@ SACLSTM_SAVE_INTERVAL=5 \
## Phase log
Every entry below starts with a plain-language first sentence. Technical detail follows for those who want it.
- **2026-08-21 23:22** — Claim posted on #56 (comment 471). Config locked, notebook committed (`2f49cb2`).
- **2026-08-21 23:27** — Smoke weights archived; release build; bootstrap + probe battles vs Corners established a hidden-256 baseline checkpoint (`sac_latest.zip`, 10.5 MB) and twin seed.
- **2026-08-21 23:32** — Twin regenerated (md5-verified). **Launch attempt 1** (SAVE_INTERVAL=20, TOTAL_ROUNDS=2000): ran 16+ chunks, evals every 2 chunks — but **zero checkpoints persisted** (see incident). Killed 23:48.
@@ -273,3 +318,15 @@ SACLSTM_LR_CRITIC=1e-4 \
| Crash banners | 0 |
### Progress graphs
One live dashboard: `docs/campaign_dashboard.svg` (current run only — test wins, real-fight wins, losses, alpha, throughput; auto-reloads every 60 s when open in Chrome). Keep it fresh with `tools/watch_dashboard.sh` (regenerates every 60 s), or one-shot `python3 tools/plot_progress.py` (pure stdlib; paths overridable via argv, `--selftest` for sanity check).
## ~10:47 — the dashboard's axes were upside-down since creation
The progress graphs have been lying since they were made: 0% was drawn at the TOP of every panel and the newest games appeared on the LEFT. The cause is a one-line formula bug in `tools/plot_progress.py`: `map_fn` interpolated as `p1 - t*(p1-p0)` instead of `p0 + t*(p1-p0)`, so all five panels plotted `100 - value` on y and reversed time on x. Tick labels were computed by separate (correct) code, which is why the numbers on the axes never matched the ink.
Fix + guard: formula corrected; every call site audited (panels 1/2/5 use `map_fn` for both axes and are fixed by the same line; panels 3/4 already used a correct local x-lambda; no other consumer of `map_fn` exists in the repo). `--selftest` now renders a known rising series through the full build path and fails loudly unless higher value = smaller SVG y and newer data = further right — proven to catch this exact bug when the old formula is re-injected. Dashboard regenerated from live logs.
Honest status while reading the now-correct charts: run 3 is only hours old and winning ~0% — recent evals are 0/10 vs Corners, Crazy and Target alike, and real-fight buckets sit at 0–1 wins per 100 games. Expected for a fresh brain. Night-1's gains were real but were intentionally reset by the stability restart that began attempt-3; the curve starts from zero again here.
+12 -4
View File
@@ -69,8 +69,16 @@ proc update*(rn: var RewardNormalizer; r: float64) =
rn.m2 += delta * delta2
proc normalize*(rn: RewardNormalizer; r: float64): float64 =
## Returns (r - mean) / (std + eps).
## Cold start (n < 2): returns 0.0 to avoid NaN/inf.
if rn.n < 2: return 0.0
## Returns (r - mean) / (std + eps) once statistics are meaningful
## (n >= 4 and spread well above zero). Before that, returns the RAW
## reward unchanged — Welford M2 collapses to exactly 0 when early raw
## rewards are identical, and dividing by the 1e-8 floor then z-scores
## the first differing reward to ~1e8, poisoning TD targets.
if rn.n < 4: return r
let variance = rn.m2 / rn.n.float64 # ponytail: population var; switch to n-1 if bias matters
result = (r - rn.mean) / (sqrt(variance) + NormEps)
let stddev = sqrt(variance)
if stddev <= 1e-3 * (abs(rn.mean) + 1.0): return r
# ponytail: warm-up pass-through ceiling — raw rewards bypass normalization
# until stats are meaningful; upgrade = persist Welford state in checkpoint
# if warm-up noise ever hurts learning.
result = (r - rn.mean) / (stddev + NormEps)
+1 -4
View File
@@ -200,10 +200,7 @@ proc squashedLogProb(mu, logStd, action: Tensor[float32]):
tuple[logProb: float32;
dLogProbDMu, dLogProbDLogStd: Tensor[float32]] =
let std = logStd.map(proc(v: float32): float32 = exp(v))
# Recover pre-tanh z ≈ arctanh(action)
let z = action.map(proc(a: float32): float32 =
let ac = clamp(a, -1.0'f32 + 1e-6'f32, 1.0'f32 - 1e-6'f32)
0.5'f32 * ln((1.0'f32 + ac) / (1.0'f32 - ac)))
let z = mu # deterministic reparam: action = tanh(mu), so z ≡ mu, diff ≡ 0
result.dLogProbDMu = newTensor[float32](4)
result.dLogProbDLogStd = newTensor[float32](4)
let twoPiLog = 0.5'f32 * ln(2.0'f32 * PI.float32)
+578
View File
@@ -0,0 +1,578 @@
#!/usr/bin/env python3
"""Build the single self-updating SAC training dashboard.
Pure-stdlib SVG output (matplotlib not available on this box).
Generates ONE file:
docs/campaign_dashboard.svg - five panels, current (v2) run only:
1. test-match win % vs opponents (campaign_v2_stdout.log eval lines)
2. real-fight win % per opponent (training_log.jsonl, ~100-game buckets)
3. critic_loss / |actor_loss| (training_metrics.jsonl, shared log-y)
4. alpha temperature (training_metrics.jsonl, linear)
5. throughput, games/hour buckets (training_metrics.jsonl 'epoch' deltas;
training_log.jsonl has NO timestamps -
verified field names - and one metrics
row == one 10-game chunk, counts match
the stdout "=== Chunk N/N ===" markers)
plus an embedded JS snippet that reloads the page every 60 s when the SVG is
opened as a top-level document in Chrome.
Usage:
python3 tools/plot_progress.py [campaign_log] [metrics_jsonl] [games_jsonl] [outdir]
All args optional; defaults relative to the SAC_LSTM_Bot/ root (parent of tools/).
python3 tools/plot_progress.py --selftest # tiny built-in sanity check
"""
import json
import math
import os
import re
import sys
import tempfile
from datetime import datetime
from pathlib import Path
import xml.etree.ElementTree as ET
ROOT = Path(__file__).resolve().parent.parent
EVAL_RE = re.compile(r">>> \[eval\] win rate: (\d+)/(\d+) \(([\d.]+)%\) vs (\S+)")
TREND_WINDOW = 10 # rolling mean shown as the thick trend line (panel 1)
GAME_BUCKET = 100 # games per bucket, real-fight panel
RATE_BUCKET = 20 # metric intervals per throughput bucket (~200 games)
GAMES_PER_ROW = 10 # one training_metrics.jsonl row per 10-round chunk
COLORS = {"Corners": "#d62728", "Crazy": "#1f77b4", "Target": "#2ca02c",
"RamFire": "#ff7f0e", "SacTwin": "#9467bd"}
CRITIC_C, ACTOR_C = "#1f77b4", "#ff7f0e"
RELOAD_JS = ('<script type="text/javascript"><![CDATA[ '
'setTimeout(function(){ location.reload(); }, 60000); ]]></script>')
W, H = 1400, 2000
TITLE_H, GUIDE_H = 80, 180
# rows: (header_y, panel_top_y, panel_bottom_y, x_left, x_right)
C1_L, C1_R = 70, 697
C2_L, C2_R = 747, 1375
ROWS = {
1: (100, 120, 720, C1_L, C1_R),
2: (100, 120, 720, C2_L, C2_R),
3: (940, 958, 1428, C1_L, C1_R),
4: (940, 958, 1428, C2_L, C2_R),
5: (1600, 1618, 1780, C1_L, C2_R),
}
PANEL_TITLES = [
"Test matches - win % vs opponents",
"Real fights - win % per opponent",
"Training losses (log scale)",
"Alpha temperature",
"Throughput - games per hour",
]
GUIDE_LINES = [
"Test matches: dots are single fights, thick line shows trend.",
"Real battles only. Rising lines mean the bot improves.",
"Loss spikes are normal early; endless growth is bad.",
"Alpha high means experimenting; falling too fast freezes habits.",
"Throughput flat is healthy; dips mean something slowed.",
"This file reloads itself in Chrome every sixty seconds.",
"Regenerate anytime with tools/watch_dashboard.sh or the python command.",
]
# ---------- parsing ----------
def parse_eval_series(path):
"""Return {opponent: [win% per eval, in file order]}."""
series = {}
if not path.is_file():
print(f"[skip] eval log not found: {path}")
return series
for line in path.read_text(errors="replace").splitlines():
m = EVAL_RE.search(line)
if not m:
continue
try:
pct = float(m.group(3))
except ValueError:
continue
series.setdefault(m.group(4), []).append(pct)
return series
def parse_metrics(path):
"""Return list of metric dicts, skipping malformed lines."""
rows = []
if not path.is_file():
print(f"[skip] metrics file not found: {path}")
return rows
for line in path.read_text(errors="replace").splitlines():
line = line.strip()
if not line:
continue
try:
rows.append(json.loads(line))
except json.JSONDecodeError:
continue
return rows
def parse_games(path):
"""Return [(opponent, won_bool)] for type=='game' rows, skipping junk."""
out = []
if not path.is_file():
print(f"[skip] games log not found: {path}")
return out
for line in path.read_text(errors="replace").splitlines():
try:
r = json.loads(line)
except json.JSONDecodeError:
continue
if r.get("type") != "game":
continue
opp, win = r.get("opponent"), r.get("win")
if isinstance(opp, str) and isinstance(win, bool):
out.append((opp, win))
return out
def metric_col(rows, key, positive=False):
"""[(index, value)] for float-parseable rows; abs() applied; optional >0 filter."""
out = []
for i, r in enumerate(rows):
try:
v = abs(float(r[key]))
except (KeyError, TypeError, ValueError):
continue
if positive and v <= 0:
continue
out.append((i, v))
return out
def rolling(vals, w=TREND_WINDOW):
out, s = [], 0.0
for i, v in enumerate(vals):
s += v
if i >= w:
s -= vals[i - w]
out.append(s / min(i + 1, w))
return out
def bucket_means(vals, n):
"""Split vals into <=n contiguous buckets of near-equal size; per-bucket mean."""
if not vals:
return []
n = min(n, len(vals))
k, rem = divmod(len(vals), n)
out, i = [], 0
for b in range(n):
size = k + (1 if b < rem else 0)
out.append(sum(vals[i:i + size]) / size)
i += size
return out
# ---------- tiny SVG helpers ----------
def esc(s):
return str(s).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
def write_svg(path, text):
"""Validate the finished SVG, then atomically swap it into place.
Readers never see partial output; an invalid render aborts without
touching the previous good file."""
try:
ET.fromstring(text)
except ET.ParseError as e:
print(f"[error] {path.name}: generated SVG invalid, keeping old file ({e})")
return False
tmp = path.with_name(path.name + ".tmp")
tmp.write_text(text)
os.replace(tmp, path)
return True
def polyline(pts, color, width=1.5, dash=None, opacity=1.0):
if len(pts) < 2:
return ""
d = f' stroke-dasharray="{dash}"' if dash else ""
p = " ".join(f"{x:.1f},{y:.1f}" for x, y in pts)
return (f'<polyline points="{p}" fill="none" stroke="{color}" '
f'stroke-width="{width}" opacity="{opacity}"{d}/>\n')
def dots(pts, color, r=2, opacity=0.25):
return "".join(f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{r}" '
f'fill="{color}" opacity="{opacity}"/>\n' for x, y in pts)
def hgrid(x0, x1, ys):
return "".join(f'<line x1="{x0}" y1="{y:.1f}" x2="{x1}" y2="{y:.1f}" '
f'stroke="#dddddd"/>\n' for y in ys)
def axis(x0, y0, x1, y1, xt, yt, xlabel, ylabel, ylog=False):
"""Draw axes + ticks + labels. xt/yt are (value, px) tick lists."""
s = (f'<line x1="{x0}" y1="{y0}" x2="{x1}" y2="{y0}" stroke="black"/>\n'
f'<line x1="{x0}" y1="{y0}" x2="{x0}" y2="{y1}" stroke="black"/>\n')
for v, px in xt:
s += (f'<line x1="{px:.1f}" y1="{y0}" x2="{px:.1f}" y2="{y0 + 4}" stroke="black"/>\n'
f'<text x="{px:.1f}" y="{y0 + 17}" text-anchor="middle" font-size="11">'
f"{esc(v)}</text>\n")
for v, py in yt:
s += (f'<line x1="{x0 - 4}" y1="{py:.1f}" x2="{x0}" y2="{py:.1f}" stroke="black"/>\n'
f'<text x="{x0 - 7}" y="{py + 4:.1f}" text-anchor="end" font-size="11">'
f"{esc(v)}</text>\n")
s += (f'<text x="{(x0 + x1) // 2}" y="{y0 + 33}" text-anchor="middle" font-size="12">'
f"{esc(xlabel)}</text>\n"
f'<text x="16" y="{(y0 + y1) // 2}" text-anchor="middle" font-size="12" '
f'transform="rotate(-90 16 {(y0 + y1) // 2})">{esc(ylabel)}</text>\n')
return s
def ticks_linear(vmin, vmax, p0, p1, n=5, fmt="{:.0f}"):
return [(fmt.format(vmin + (vmax - vmin) * i / (n - 1)),
p0 + (p1 - p0) * i / (n - 1)) for i in range(n)]
def ticks_log(vmin, vmax, p0, p1, n=5):
vals = [10 ** (vmin + (vmax - vmin) * i / (n - 1)) for i in range(n)]
return [("{:.3g}".format(v), p0 + (p1 - p0) * i / (n - 1)) for i, v in enumerate(vals)]
def legend(items, x, y):
"""items: [(color, label)]"""
s = ""
for i, (color, label) in enumerate(items):
yy = y + i * 18
s += (f'<line x1="{x}" y1="{yy}" x2="{x + 28}" y2="{yy}" stroke="{color}" '
f'stroke-width="3"/>\n'
f'<text x="{x + 34}" y="{yy + 4}" font-size="12">{esc(label)}</text>\n')
return s
def map_fn(p0, p1, vmin, vmax, log=False):
def f(v):
t = (math.log10(v) - vmin) / (vmax - vmin) if log else (v - vmin) / (vmax - vmin)
return p0 + max(0.0, min(1.0, t)) * (p1 - p0)
return f
def guide_block(w, h, lines):
"""Plain-English "How to read" band at the bottom of the canvas."""
y = H - GUIDE_H
s = f'<rect x="0" y="{y}" width="{w}" height="{GUIDE_H}" fill="#f2f2f2"/>\n'
s += (f'<text x="16" y="{y + 21}" font-size="15" font-weight="bold">'
f"How to read</text>\n"
f'<text x="{w - 16}" y="{y + 20}" text-anchor="end" font-size="11" '
f'fill="#666">Regenerate anytime: python3 tools/plot_progress.py</text>\n')
for i, ln in enumerate(lines):
s += f'<text x="16" y="{y + 43 + i * 17}" font-size="13">{esc(ln)}</text>\n'
return s
def header(x, y, title, sub=None):
s = (f'<text x="{x}" y="{y}" font-size="15" font-weight="bold">'
f"{esc(title)}</text>\n")
if sub:
s += (f'<text x="{x}" y="{y + 15}" font-size="11" fill="#555">'
f"{esc(sub)}</text>\n")
return s
# ---------- panels ----------
def panel_test_matches(series, geo):
_, pt, pb, x0, x1 = geo
s = ""
if not series:
s += f'<text x="{x0 + 10}" y="{pt + 40}" font-size="12" fill="#a00">' \
"no eval lines found</text>\n"
return
xmax = max(max(len(v) for v in series.values()), 2)
xm, ym = map_fn(x0, x1, 1, xmax), map_fn(pb, pt, 0, 100)
s += hgrid(x0, x1, [ym(v) for v in range(0, 101, 20)])
step = max(1, (xmax // 8 // 10) * 10)
xt = [(str(v), xm(v)) for v in range(step, xmax + 1, step)] or [("1", xm(1))]
s += axis(x0, pb, x1, pt, xt, ticks_linear(0, 100, pb, pt, n=6),
"test match number (each opponent)", "win rate (%)")
items = []
for name in ("Corners", "Crazy", "Target"):
vals = series.get(name, [])
if not vals:
continue
c = COLORS[name]
s += dots([(xm(i + 1), ym(v)) for i, v in enumerate(vals)], c)
s += polyline([(xm(i + 1), ym(v))
for i, v in enumerate(rolling(vals))], c, 3.5)
items.append((c, f"{name} - {len(vals)} evals"))
s += legend(items, x0 + 12, pb + 52)
return s
def panel_real_fights(games, geo):
_, pt, pb, x0, x1 = geo
s = ""
if not games:
s += f'<text x="{x0 + 10}" y="{pt + 40}" font-size="12" fill="#a00">' \
"no game rows found</text>\n"
return
edges = list(range(0, len(games) + 1, GAME_BUCKET))
if edges[-1] != len(games):
edges.append(len(games))
buckets = list(zip(edges[:-1], edges[1:]))
xm, ym = map_fn(x0, x1, 1, max(len(games), 2)), map_fn(pb, pt, 0, 100)
s += hgrid(x0, x1, [ym(v) for v in range(0, 101, 20)])
step = max(GAME_BUCKET, GAME_BUCKET * (len(games) // GAME_BUCKET // 8 + 1))
xt = [(str(v), xm(v)) for v in range(step, len(games) + 1, step)]
s += axis(x0, pb, x1, pt, xt, ticks_linear(0, 100, pb, pt, n=6),
f"game number ({GAME_BUCKET}-game buckets)", "win %")
opponents = []
for opp, _ in games:
if opp not in opponents:
opponents.append(opp)
items = []
for name in opponents:
c = COLORS.get(name, "#7f7f7f")
by_b = {}
for bi, (lo, hi) in enumerate(buckets):
sub = [w for o, w in games[lo:hi] if o == name]
if sub:
by_b[bi] = 100.0 * sum(sub) / len(sub)
pts = []
segs, prev = [], None
for bi in sorted(by_b):
if prev is not None and bi != prev + 1:
segs.append(pts)
pts = []
center = (buckets[bi][0] + buckets[bi][1]) / 2
pts.append((xm(center), ym(by_b[bi])))
prev = bi
if len(pts) >= 2:
segs.append(pts)
for seg in segs:
s += polyline(seg, c, 3.5)
n_played = sum(1 for o, _ in games if o == name)
items.append((c, f"{name} ({n_played} games)"))
s += legend(items, x0 + 12, pb + 52)
return s
def panel_losses(rows, geo):
_, pt, pb, x0, x1 = geo
s = ""
critic = metric_col(rows, "critic_loss", positive=True)
actor = metric_col(rows, "actor_loss") # abs() applied; sign dropped
actor = [(i, v) for i, v in actor if v > 0]
if not (critic or actor):
s += f'<text x="{x0 + 10}" y="{pt + 40}" font-size="12" fill="#a00">' \
"no valid loss points</text>\n"
return
allv = [v for _, v in critic + actor]
lo, hi = math.floor(math.log10(min(allv))), math.ceil(math.log10(max(allv)))
if lo == hi:
hi = lo + 1
n = len(rows)
xm = lambda i: x0 + (x1 - x0) * i / max(n - 1, 1)
ym = map_fn(pb, pt, lo, hi, log=True)
s += hgrid(x0, x1, [ym(10 ** e) for e in range(lo, hi + 1)])
s += axis(x0, pb, x1, pt, ticks_linear(1, n, x0, x1, n=5),
ticks_log(lo, hi, pb, pt), "metric line number", "loss (log)")
s += polyline([(xm(i), ym(v)) for i, v in critic], CRITIC_C, 1.8)
s += polyline([(xm(i), ym(v)) for i, v in actor], ACTOR_C, 1.8)
s += legend([(CRITIC_C, "critic_loss"), (ACTOR_C, "|actor_loss|")],
x0 + 12, pb + 52)
return s
def panel_alpha(rows, geo):
_, pt, pb, x0, x1 = geo
s = ""
alpha = metric_col(rows, "alpha")
if not alpha:
s += f'<text x="{x0 + 10}" y="{pt + 40}" font-size="12" fill="#a00">' \
"no alpha points</text>\n"
return
n = len(rows)
hi = max(1.0, max(v for _, v in alpha))
xm = lambda i: x0 + (x1 - x0) * i / max(n - 1, 1)
ym = map_fn(pb, pt, 0, hi)
s += hgrid(x0, x1, [ym(v) for v in
[hi * k / 4 for k in range(5)]])
s += axis(x0, pb, x1, pt, ticks_linear(1, n, x0, x1, n=5),
ticks_linear(0, hi, pb, pt, n=5, fmt="{:.3g}"),
"metric line number", "alpha")
s += polyline([(xm(i), ym(v)) for i, v in alpha], "#9467bd", 1.8)
return s
def panel_throughput(rows, geo):
_, pt, pb, x0, x1 = geo
s = ""
eps = []
for r in rows:
try:
eps.append(float(r["epoch"]))
except (KeyError, TypeError, ValueError):
continue
rates = [] # games/hour per inter-row interval
for a, b in zip(eps, eps[1:]):
dt = b - a
if dt > 0:
rates.append(3600.0 * GAMES_PER_ROW / dt)
if not rates:
s += f'<text x="{x0 + 10}" y="{pt + 40}" font-size="12" fill="#a00">' \
"no usable epoch timestamps</text>\n"
return
bm = bucket_means(rates, RATE_BUCKET)
xm = map_fn(x0, x1, 1, len(rates))
ymax = max(max(rates), max(bm)) * 1.1
ym = map_fn(pb, pt, 0, ymax)
s += hgrid(x0, x1, [ym(ymax * k / 4) for k in range(5)])
step = max(1, len(rates) // 10)
xt = [(str(v), xm(v)) for v in range(step, len(rates) + 1, step)]
s += axis(x0, pb, x1, pt, xt, ticks_linear(0, ymax, pb, pt, n=5, fmt="{:.0f}"),
f"chunk interval ({GAMES_PER_ROW}-game chunks)", "games / hour")
s += dots([(xm(i + 1), ym(v)) for i, v in enumerate(rates)], "#7f7f7f", r=1.6)
if len(bm) >= 2:
ctr = [xm(round((i + 0.5) * len(rates) / len(bm))) for i in range(len(bm))]
s += polyline(list(zip(ctr, map(ym, bm))), "#2ca02c", 3.5)
return s
# ---------- assembly ----------
def build_dashboard(campaign, metrics_f, games_f, out):
series = parse_eval_series(campaign)
print("[info] evals parsed: " +
(", ".join(f"{k}={len(v)}" for k, v in sorted(series.items())) or "(none)"))
rows = parse_metrics(metrics_f)
print(f"[info] metric rows parsed: {len(rows)}")
games = parse_games(games_f)
print(f"[info] game rows parsed: {len(games)}")
s = (f'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" '
f'viewBox="0 0 {W} {H}" font-family="sans-serif">\n'
f'<rect width="{W}" height="{H}" fill="white"/>\n'
f'<text x="{W // 2}" y="32" text-anchor="middle" font-size="21" '
f'font-weight="bold">SAC-LSTM campaign dashboard - live run (current only)</text>\n'
f'<text x="{W // 2}" y="56" text-anchor="middle" font-size="12" fill="#555">'
f'generated {datetime.now():%Y-%m-%d %H:%M:%S} - auto-reloads every 60 s '
f'(open this file in Chrome)</text>\n')
drawers = [
(ROWS[1], PANEL_TITLES[0],
"raw dots = single test matches, thick = rolling-mean-%d" % TREND_WINDOW,
lambda: panel_test_matches(series, ROWS[1])),
(ROWS[2], PANEL_TITLES[1],
"training_log.jsonl only - learning in REAL battles, not tests",
lambda: panel_real_fights(games, ROWS[2])),
(ROWS[3], PANEL_TITLES[2],
"training_metrics.jsonl - big early spikes are normal",
lambda: panel_losses(rows, ROWS[3])),
(ROWS[4], PANEL_TITLES[3],
"training_metrics.jsonl - high = exploring, low = exploiting",
lambda: panel_alpha(rows, ROWS[4])),
(ROWS[5], PANEL_TITLES[4],
"method: training_metrics.jsonl 'epoch' deltas (training_log.jsonl has "
"no timestamps); 1 row = one 10-game chunk",
lambda: panel_throughput(rows, ROWS[5])),
]
for geo, title, sub, drawer in drawers:
s += header(geo[3], geo[0], title, sub)
s += drawer()
s += guide_block(W, H, GUIDE_LINES)
s += RELOAD_JS + "\n"
s += "</svg>\n"
return write_svg(out, s)
# ---------- selftest ----------
def selftest():
with tempfile.TemporaryDirectory() as td:
td = Path(td)
(td / "log").write_text(
">>> [eval] win rate: 3/10 (30%) vs Corners\n"
"garbage line\n"
">>> [eval] win rate: 7/10 (70%) vs Crazy\n"
">>> [eval] win rate: broken\n"
">>> [eval] win rate: 5/10 (50%) vs Corners\n"
">>> [eval] win rate: 4/10 (40%) vs Crazy\n")
ser = parse_eval_series(td / "log")
assert ser == {"Corners": [30.0, 50.0], "Crazy": [70.0, 40.0]}, ser
assert rolling([10] * 25, 20)[-1] == 10.0
assert rolling([1, 2, 3], 20) == [1.0, 1.5, 2.0]
assert len(bucket_means(list(range(1287)), RATE_BUCKET)) == RATE_BUCKET
bm = bucket_means([0, 10], RATE_BUCKET)
assert bm == [0.0, 10.0], bm # fewer points than buckets -> no empty buckets
(td / "m.jsonl").write_text(
'{"epoch": 1000.0, "critic_loss": 10, "actor_loss": -2, "alpha": 0.5}\n'
"not json\n"
'{"epoch": 1060.0, "critic_loss": 100, "actor_loss": -4, "alpha": 0.25}\n'
'{"epoch": 1090.0, "critic_loss": 50, "actor_loss": 3, "alpha": 0.2}\n')
rows = parse_metrics(td / "m.jsonl")
assert len(rows) == 3 and rows[1]["critic_loss"] == 100
glines = []
for i in range(150): # 2 full GAME_BUCKETs, both opponents in both
glines.append(json.dumps(
{"type": "game", "round": i % 10 + 1, "ticks": 100,
"score": i % 3, "total_score": i, "win": i % 3 == 0,
"opponent": ("Corners", "Crazy")[i % 2]}))
(td / "g.jsonl").write_text("\n".join(glines) + "\n")
games = parse_games(td / "g.jsonl")
assert len(games) == 150 and games[0] == ("Corners", True)
assert games[-1] == ("Crazy", False) # i=149: odd -> Crazy; 149%3!=0 -> loss
assert metric_col(rows, "actor_loss") == [(0, 2.0), (1, 4.0), (2, 3.0)]
dash = td / "dash.svg"
assert build_dashboard(td / "log", td / "m.jsonl", td / "g.jsonl", dash)
text = dash.read_text()
ET.fromstring(text) # whole doc must parse -> closing tag present
assert RELOAD_JS in text, "auto-reload script missing"
for t in PANEL_TITLES:
assert t in text, f"panel title missing: {t}"
assert text.count(PANEL_TITLES[0]) == 1
assert "How to read" in text, "reading guide missing"
assert 'width="1400"' in text and 'height="2000"' in text
# circles: 4 eval dots (panel 1) + 2 throughput rate dots (panel 5)
assert text.count("<circle") == 6, text.count("<circle")
# 2 trends + 2 real-fight opp lines + 2 losses + 1 alpha + 1 throughput
assert text.count("<polyline") == 8, text.count("<polyline")
# orientation guard: a known rising series (10% -> 90%) rendered through
# the FULL build path must plot upward (smaller SVG y) and forward in
# time (larger x). Fails loudly if axis mapping is ever inverted again.
ori_log = td / "ori.log"
ori_log.write_text(
">>> [eval] win rate: 1/10 (10%) vs Corners\n"
">>> [eval] win rate: 9/10 (90%) vs Corners\n")
ori_dash = td / "ori.svg"
assert build_dashboard(ori_log, td / "m.jsonl", td / "g.jsonl", ori_dash)
m = re.search(r'<polyline points="([^"]+)"', ori_dash.read_text())
pts = [tuple(map(float, p.split(","))) for p in m.group(1).split()]
assert len(pts) == 2, pts
(xa, ya), (xb, yb) = pts
assert yb < ya, f"y-axis inverted: win % rose 10->90 but ink moved down ({ya} -> {yb})"
assert xb > xa, f"x-axis reversed: newer eval plotted left ({xa} -> {xb})"
print("selftest OK")
def main():
if "--selftest" in sys.argv:
selftest()
return
args = [a for a in sys.argv[1:] if not a.startswith("-")]
campaign = Path(args[0]) if len(args) > 0 else ROOT / "campaign_v2_stdout.log"
metrics = Path(args[1]) if len(args) > 1 else ROOT / "training_metrics.jsonl"
games = Path(args[2]) if len(args) > 2 else ROOT / "training_log.jsonl"
outdir = Path(args[3]) if len(args) > 3 else ROOT / "docs"
outdir.mkdir(parents=True, exist_ok=True)
try:
ok = build_dashboard(campaign, metrics, games, outdir / "campaign_dashboard.svg")
except Exception as e:
print(f"[error] dashboard build failed: {e}")
ok = False
if ok:
print(f"[done] dashboard written to {outdir / 'campaign_dashboard.svg'}")
else:
print("[error] dashboard NOT updated - check paths above")
sys.exit(1)
if __name__ == "__main__":
main()
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
# Keep docs/campaign_dashboard.svg fresh: regenerate every 60 s.
# Errors go to stderr and never exit the loop silently.
dir=$(dirname "$0")
while :; do
if ! python3 "$dir/plot_progress.py"; then
echo "[watch_dashboard] $(date '+%F %T') regeneration failed (see error above)" >&2
fi
sleep 60
done
@@ -0,0 +1,173 @@
# GA/ES Parameters for ~750-Weight Neuroevolution
Research for issue #65. Concrete parameter recommendations extracted from 13 papers in `docs/papers/neuroevolution/`.
## Context
Evo_Bot's TOPO_Gun: fixed-topology feedforward ANN, 745-1489 weights depending on layer sizes. Task is predicting enemy dodge behavior from a 30-tick sliding window, outputting a guess factor. Online evolution during Robocode matches.
---
## 1. Population Size
**Recommendation: 64-256, not 300.**
| Source | Network size | Population | Notes |
|--------|-------------|------------|-------|
| Uber Deep GA (Such et al. 2017) | 4M params (Atari), 167k (Humanoid) | 1,000 | Massive networks, distributed; overkill for ~750 weights |
| OpenAI ES (Salimans et al. 2017) | 1.7M params | 720-1,440 workers | NES-style, not a population GA |
| Canonical ES (Chrabaszcz et al. 2018) | 1.7M params | 798 (lambda), mu=50 | mu=50 selected as best across games |
| World Models CMA-ES (Ha & Schmidhuber 2018) | 867-1,088 params (controller) | 64 | CMA-ES with 16 evals per individual |
| Challenges paper (Muller & Glasmachers 2018) | 1,352-2,349 weights | default CMA-ES lambda | LM-MA-ES for ~1k-2k weights |
| Evolving Generalists (Triebold & Yaman 2023) | 246-728 weights | xNES default: 4+floor(3*ln(d)) | For d=745 -> ~24; for d=1489 -> ~26 |
| NRA (Le Clei & Bellec 2022) | 3-322 params (dynamic) | 8-512 | 256+elitism best for ~300-param tasks; 16 enough for <100 params |
| Playing Atari 6 Neurons (Cuccu et al. 2018) | ~3k connections, 6-18 neurons | xNES default | Small networks, 100 generations sufficient |
**Key finding:** For ~750 weights, CMA-ES default lambda = 4+floor(3*ln(745)) = ~24 is a starting floor. The World Models paper (867-1,088 params, closest to our size) used 64 with CMA-ES and solved CarRacing. NRA used 256+elitism for ~300-param Pendulum networks. For a simple truncation-selection GA (not CMA-ES), 100-200 is a reasonable population; 300 is slightly wasteful but not harmful if evaluation is cheap.
**Verdict: Start with 100-200 for a truncation GA. If using CMA-ES or xNES, use their defaults (~24-26). 300 is too large for the weight count but acceptable if per-evaluation cost is low (Robocode rounds are fast).**
---
## 2. Mutation Rate and Distribution
**Recommendation: Additive Gaussian on ALL weights, sigma=0.002-0.02, not 5% at sigma=0.1.**
| Source | Mutation scheme | Notes |
|--------|----------------|-------|
| Uber Deep GA (Such et al. 2017) | theta' = theta + sigma * epsilon, epsilon ~ N(0,I). Sigma determined empirically per task. | Mutates ALL weights every generation, not a fraction. No "mutation rate" — every weight gets noise. |
| OpenAI ES (Salimans et al. 2017) | sigma = fixed hyperparameter (not adapted). Perturbation on full parameter vector. | Full-vector Gaussian perturbation, sigma tuned. |
| Canonical ES (Chrabaszcz et al. 2018) | N(0, sigma^2) added to all params. Network init from N(0, 0.05). | sigma is the step-size, adapted or fixed. |
| World Models (Ha & Schmidhuber 2018) | CMA-ES adapts sigma and full covariance matrix. | Self-adapting sigma — no manual sigma needed. |
| Challenges paper (Muller & Glasmachers 2018) | CSA (cumulative step-size adaptation) essential. Fixed sigma converges as slowly as random search. | Step-size adaptation is critical; fixed sigma is a known failure mode. |
| NRA (Le Clei & Bellec 2022) | N(0, 0.01) perturbation to all weights and biases. Top 50% selection. | sigma=0.01 for small networks. |
**Key finding:** No paper uses a "5% mutation rate" (mutating only 5% of weights). ALL papers mutate ALL weights simultaneously with small additive Gaussian noise. The "mutation rate" concept from traditional GAs (flip probability per gene) does not apply to real-valued neuroevolution. Instead, the noise magnitude (sigma) controls exploration.
For ~750 weights:
- NRA uses sigma=0.01 for networks up to ~300 params
- Uber GA uses sigma empirically per task (typical range 0.002-0.02 for Atari)
- CMA-ES/xNES adapt sigma automatically
**Verdict: Mutate ALL weights every generation. Use sigma=0.005-0.01 as starting point. If using CMA-ES, sigma self-adapts. The "5% of weights mutated" approach is non-standard and likely harmful — it under-explores the search space.**
---
## 3. Selection Pressure
**Recommendation: Top 10-50% (truncation) or top mu out of lambda. 20% is reasonable.**
| Source | Selection | Notes |
|--------|-----------|-------|
| Uber Deep GA (Such et al. 2017) | Truncation selection, top T individuals become parents. T not specified as percentage — varies. | Parents chosen uniformly at random from top T. |
| Canonical ES (Chrabaszcz et al. 2018) | Top mu=50 out of lambda=798 (~6%). Weighted mean of top mu. | mu=50 found optimal across games; tested mu in {10,20,50,100,200,400}. |
| NRA (Le Clei & Bellec 2022) | Top 50% duplicated, bottom 50% replaced. | Simple and effective for small populations. |
| Evolving Generalists (Triebold & Yaman 2023) | xNES default selection. | NES uses weighted rank-based update. |
**Key finding:** Selection pressure varies widely. Canonical ES uses ~6% (mu=50 out of 798). NRA uses 50%. Standard CMA-ES uses mu = lambda/2 (50%). Top 20% is in the middle range and is fine for a truncation GA.
**Verdict: 20% is reasonable. For small populations (64-100), 50% (top half) may work better. For larger populations (200+), stricter selection (10-20%) is appropriate. The Canonical ES result suggests mu=50 works well regardless of lambda for Atari-scale problems.**
---
## 4. Crossover
**Recommendation: No crossover. Mutation-only.**
| Source | Crossover? | Notes |
|--------|-----------|-------|
| Uber Deep GA (Such et al. 2017) | **No crossover.** "Historically, GAs often involve crossover, but for simplicity we did not include it." | Explicitly dropped crossover for DNN weights. |
| OpenAI ES (Salimans et al. 2017) | No crossover. | ES-style: mean update, not recombination of individuals. |
| NRA (Le Clei & Bellec 2022) | **No crossover.** "stripping down many mechanisms popular in traditional evolutionary methods, like agent crossover and speciation" | Crossover explicitly excluded. |
| CMA-ES/xNES | No crossover in the traditional sense. | Weighted recombination of top individuals into distribution mean — not pairwise crossover. |
| NEAT (Stanley & Miikkulainen 2011) | Has crossover via innovation numbers. | But NEAT is for topology evolution, not fixed-topology weight-only GA. |
**Key finding:** Every modern neuroevolution paper that works with fixed-topology networks drops crossover. Fogel & Stayton (1994, cited by Such et al.) showed crossover is often ineffective for simulated evolutionary optimization. For ANN weight vectors, crossover tends to be destructive because individual weights are not independent genes — they form functional units (layers, pathways) where mixing two different solutions creates non-functional hybrids.
**Verdict: No crossover. Mutation-only. Uniform crossover of ANN weights is harmful — it breaks co-adapted weight configurations. If recombination is desired, use CMA-ES/xNES-style weighted mean of top solutions, which is mathematically sound.**
---
## 5. Elitism
**Recommendation: Yes, keep top 1 unchanged (single elite).**
| Source | Elitism? | Notes |
|--------|---------|-------|
| Uber Deep GA (Such et al. 2017) | **Yes, 1 elite.** "The Nth individual is an unmodified copy of the best individual from the previous generation." Additionally, top 10 re-evaluated 30 times to find the true elite. | Single elite with robust re-evaluation. |
| NRA (Le Clei & Bellec 2022) | **Yes, elitism tested and beneficial.** Population sizes labeled "(elite)" consistently outperform non-elite variants in all figures. | Elitism was the single most impactful improvement for small populations. |
| CMA-ES | Elitist variants exist (mu+lambda). Standard CMA-ES is (mu,lambda) — non-elitist. | Non-elitist CMA-ES relies on distribution adaptation, not individual survival. |
**Key finding:** For simple truncation GAs, elitism (keeping top 1) prevents regression and is universally recommended. The NRA paper shows that adding elitism to even a population of 16 dramatically improves results. Uber's Deep GA uses elitism with robust re-evaluation (30 episodes to confirm the elite).
**Verdict: Keep top 1 elite unchanged. In noisy evaluation environments (Robocode), re-evaluate the top few candidates multiple times to find the true elite, following Uber's approach.**
---
## 6. Generations to Convergence
**Recommendation: 100-1,500 generations for ~750 weights, depending on the algorithm.**
| Source | Network size | Generations | Notes |
|--------|-------------|-------------|-------|
| Uber Deep GA (Such et al. 2017) | 4M params | 348-1,834 gens (at 1k pop) | Many games: best-in-run found in 1-29 gens |
| World Models CMA-ES (Ha & Schmidhuber 2018) | 867 params | ~1,800 gens | CMA-ES, pop=64, solved CarRacing |
| NRA (Le Clei & Bellec 2022) | 3-322 params (dynamic) | 100-5,000 gens | Simple tasks: <300 gens. Complex (Ant/Humanoid): 5,000+ |
| Playing Atari 6 Neurons (Cuccu et al. 2018) | ~3k connections | 100 gens | Extremely tight budget, still achieved competitive results |
| Challenges paper (Muller & Glasmachers 2018) | 1,352-2,349 weights | 100k-300k evals | LM-MA-ES, ~150k evals for bipedal walker convergence |
| Evolving Generalists (Triebold & Yaman 2023) | 728 weights (Ant) | 5,000 gens max | xNES, some tasks solved in <100 gens |
**Key finding:** For ~750 weights with a simple truncation GA (pop=100), expect 200-500 generations for a well-tuned sigma. CMA-ES/xNES may converge faster in generations but each generation is more expensive. The Challenges paper warns that halving the distance to the optimum requires O(d) samples, so for d=750, expect ~750 evaluations per halving step.
For Evo_Bot's online evolution during matches: each Robocode round can evaluate one individual. With 35-round matches (typical), ~5 generations of pop=7 per match, or ~2 generations of pop=15. Convergence within a single match is unlikely; evolution must persist across matches via weight persistence.
**Verdict: Budget 500-2,000 generations. With pop=100, that's 50k-200k evaluations. Online evolution will need many matches to converge — weight persistence is essential.**
---
## 7. CMA-ES vs Simple GA vs Tournament Selection
**Recommendation: CMA-ES or xNES for ~750 weights. Simple GA as a simpler fallback.**
| Algorithm | Sweet spot | Pros | Cons | Source |
|-----------|-----------|------|------|--------|
| **CMA-ES** | d <= 1,000 (ideal), up to ~2,000 (practical) | Self-adapts sigma and covariance, best convergence rate, handles ill-conditioned landscapes | O(d^2) memory/time per generation, needs O(d^2) evals for full covariance learning | Muller & Glasmachers 2018, Ha & Schmidhuber 2018 |
| **LM-MA-ES** | d = 1,000-10,000 | O(d) complexity, adapts fastest-evolving subspace, strong on ~2k weights | More complex to implement | Muller & Glasmachers 2018 |
| **xNES** | d <= 1,000 | Natural gradient, self-adapting, elegant | Similar scaling limits to CMA-ES | Cuccu et al. 2018, Triebold & Yaman 2023 |
| **Simple truncation GA** | Any d | Dead simple, trivially parallel, no internal state beyond population | Needs manual sigma tuning, no adaptation, converges slowly | Such et al. 2017, Le Clei & Bellec 2022 |
| **Canonical (mu,lambda)-ES** | Any d | Step-size adaptation via CSA, simple | mu tuning matters; mu=50 worked well in Chrabaszcz 2018 | Chrabaszcz et al. 2018 |
| **Tournament selection** | Traditional GA context | Tunable selection pressure | No advantage over truncation for ANN weights | Not specifically tested in any of the 13 papers |
**Key finding at d=750:** CMA-ES is in its sweet spot. The World Models paper (Ha & Schmidhuber 2018) used CMA-ES with pop=64 on 867-1,088 params and solved complex control tasks. The Evolving Generalists paper (Triebold & Yaman 2023) used xNES on 728 weights (Ant controller) with default population sizes. The Challenges paper (Muller & Glasmachers 2018) explicitly shows CMA-ES and LM-MA-ES outperforming simple ES on problems with 769-2,738 weights.
However, CMA-ES requires O(d^2) = O(560k) memory for the covariance matrix at d=750. This is manageable but not trivial for an online Robocode bot. A simpler option is a (mu,lambda)-ES with CSA for step-size adaptation.
**Verdict: CMA-ES or xNES is the best fit for 750 weights. If implementation complexity is a concern, a truncation GA with adaptive sigma (or even fixed sigma=0.005) is the pragmatic choice. Tournament selection offers no advantage.**
---
## Summary: Recommended Parameters for Evo_Bot TOPO_Gun
| Parameter | Current assumption | Recommendation | Rationale |
|-----------|-------------------|----------------|-----------|
| Population | 300 | 64-200 | 300 is oversized for ~750 weights; 64 (CMA-ES) to 200 (truncation GA) |
| Mutation | 5% of weights, gaussian sigma=0.1 | ALL weights, sigma=0.005-0.01 | Every paper mutates all weights. sigma=0.1 is too large. |
| Selection | Top 20% | Top 20-50% | 20% is fine; 50% if pop is small |
| Crossover | Uniform | None | Uniformly dropped in all modern neuroevolution papers |
| Elitism | Not specified | Top 1, re-evaluated | Single elite prevents regression; re-evaluate to handle noise |
| Algorithm | Simple GA | CMA-ES or truncation GA + CSA | CMA-ES is in its sweet spot at d=750; simple GA works but converges slower |
| Generations | Not specified | 500-2,000 | Online evolution needs many matches for convergence |
---
## Sources
1. Such et al. 2017 — "Deep Neuroevolution: Genetic Algorithms are a Competitive Alternative" (Uber AI Labs)
2. Salimans et al. 2017 — "Evolution Strategies as a Scalable Alternative to Reinforcement Learning" (OpenAI)
3. Chrabaszcz et al. 2018 — "Back to Basics: Benchmarking Canonical Evolution Strategies for Playing Atari"
4. Muller & Glasmachers 2018 — "Challenges in High-dimensional Reinforcement Learning with Evolution Strategies"
5. Ha & Schmidhuber 2018 — "Recurrent World Models Facilitate Policy Evolution" (World Models)
6. Cuccu et al. 2018 — "Playing Atari with Six Neurons"
7. Le Clei & Bellec 2022 — "Neuroevolution of Recurrent Architectures on Control Tasks"
8. Triebold & Yaman 2023 — "Evolving Generalist Controllers to Handle a Wide Range of Morphological Variations"
9. Stanley & Miikkulainen 2011 — "Competitive Coevolution through Evolutionary Complexification" (NEAT)