TiGa-RCE commited on
Commit
08c6e6e
·
verified ·
1 Parent(s): db11104

Add one-click full14 reward selection and enforce catalog maximum

Browse files

Preserve existing reward values/types/order; append missing metrics with manual Add defaults. Unified reward controls for max, duplicate, busy and frozen-evaluation states. Native/worker/training settings and all policies unchanged. Previous revision: db111049b8962b41399fdce87458dcfe5b78240e

Files changed (6) hide show
  1. FRONTEND_SOURCE.json +9 -9
  2. README.md +4 -0
  3. RELEASE_MANIFEST.json +5 -5
  4. client.js +2 -1
  5. help.html +1 -0
  6. index.html +25 -7
FRONTEND_SOURCE.json CHANGED
@@ -5,25 +5,25 @@
5
  "source": "Runner r14 single-engine browser UI; selected .track tile renderer added from simulator atlas",
6
  "source_sha256": "65d1e5f2c4e19b4951b828e82510005ab99a5270cf3dcb9f4eaa0fa1af8c40f8"
7
  },
8
- "client_sha256": "acc63231b6575a2138972ac506595ab4344e5000a152d1e2a13efe6e31dc9194",
9
  "identity_note": "The historical source fields describe the base frontend; local packaged file hashes describe this candidate's derived frontend.",
10
- "local_customization_id": "aits-driving-around-repository-rename-20261024",
11
- "output_sha256": "a2880dd93b52fe17bad4a69ff11854458b3453a79244247c14b1ce4f6a26770e",
12
  "packaged_frontend_files": {
13
  "agent-manifest.json": "7187c85327d588cb3b81f5ddf60bae099567a656bcceac8147ecb2227e07f561",
14
  "agent-tools.js": "af4af7d4dab229f5988daf49f630e08879025b37b561a5c93669a6667ab8fdf4",
15
  "agents.html": "c2c6889e50056c68c921559ccf95267a541622471e2ccec55099d70989ee8110",
16
  "assets/track-atlas.json": "d6168fa2bbcf3e387bae2c61aa4912ad4b556038d2c34adeb4a11716139918ea",
17
- "client.js": "acc63231b6575a2138972ac506595ab4344e5000a152d1e2a13efe6e31dc9194",
18
- "help.html": "a12d5ad5e7d347267cab465f44e7ac709731f30982f8424e9f5d83c561881964",
19
  "hf-auth.js": "3f3938bbc0935e4e9230c094029ef46e03beea74be199887c50e053e03dafcfe",
20
- "index.html": "a2880dd93b52fe17bad4a69ff11854458b3453a79244247c14b1ce4f6a26770e",
21
  "track-library.js": "351ec4c026b49420ce963f4310cb000d9e987fd28e6dac5ecd0176509340f6e6",
22
  "track-map.js": "1bbd528a5c593bf630698b5e35f939e86df569c79b668445c9d6155c362a87b0",
23
  "track-mask.js": "75b20e989b8d192d32c96dff24c5e6f0bf3fdf5bb53302accbd4fc6884eb09f7"
24
  },
25
- "packaged_frontend_sha256": "b044d716fced564d01d27f8ffe95ebcccb194dda44e3270ead207583109dfd6e",
26
  "packaged_frontend_sha256_format": "SHA-256 of UTF-8 JSON for packaged_frontend_files, sorted keys, compact separators, no trailing newline",
27
- "parent_local_customization_id": "driving-around-native-mask-route-20261024",
28
- "status": "Repository link rename and prominent engine/game attribution; native, driving and training behavior unchanged. Qualification and current-link readback recorded separately."
29
  }
 
5
  "source": "Runner r14 single-engine browser UI; selected .track tile renderer added from simulator atlas",
6
  "source_sha256": "65d1e5f2c4e19b4951b828e82510005ab99a5270cf3dcb9f4eaa0fa1af8c40f8"
7
  },
8
+ "client_sha256": "12746c2334f4342ed7e1a3182b085e02f17bc57d41d762f7f1ad612b45c240dd",
9
  "identity_note": "The historical source fields describe the base frontend; local packaged file hashes describe this candidate's derived frontend.",
10
+ "local_customization_id": "aits-full-reward-selection-20261024",
11
+ "output_sha256": "102f19a9c28397f76595f1a289a221065664635655295a103826b34d65ae95c1",
12
  "packaged_frontend_files": {
13
  "agent-manifest.json": "7187c85327d588cb3b81f5ddf60bae099567a656bcceac8147ecb2227e07f561",
14
  "agent-tools.js": "af4af7d4dab229f5988daf49f630e08879025b37b561a5c93669a6667ab8fdf4",
15
  "agents.html": "c2c6889e50056c68c921559ccf95267a541622471e2ccec55099d70989ee8110",
16
  "assets/track-atlas.json": "d6168fa2bbcf3e387bae2c61aa4912ad4b556038d2c34adeb4a11716139918ea",
17
+ "client.js": "12746c2334f4342ed7e1a3182b085e02f17bc57d41d762f7f1ad612b45c240dd",
18
+ "help.html": "41c5d99f2e378a16bde25e1c6bc1f7c85031f91414b499e826f6bea712b1f92d",
19
  "hf-auth.js": "3f3938bbc0935e4e9230c094029ef46e03beea74be199887c50e053e03dafcfe",
20
+ "index.html": "102f19a9c28397f76595f1a289a221065664635655295a103826b34d65ae95c1",
21
  "track-library.js": "351ec4c026b49420ce963f4310cb000d9e987fd28e6dac5ecd0176509340f6e6",
22
  "track-map.js": "1bbd528a5c593bf630698b5e35f939e86df569c79b668445c9d6155c362a87b0",
23
  "track-mask.js": "75b20e989b8d192d32c96dff24c5e6f0bf3fdf5bb53302accbd4fc6884eb09f7"
24
  },
25
+ "packaged_frontend_sha256": "8988293c4cf333b6b4bdf50666cf7c3f155fccff35a3b9e7eb00367e4e8b2a91",
26
  "packaged_frontend_sha256_format": "SHA-256 of UTF-8 JSON for packaged_frontend_files, sorted keys, compact separators, no trailing newline",
27
+ "parent_local_customization_id": "aits-driving-around-repository-rename-20261024",
28
+ "status": "Explicit full-catalog reward selection with14metric maximum and unified busy/evaluation controls. Existing reward values and backend/native semantics preserved; qualification recorded separately."
29
  }
README.md CHANGED
@@ -49,6 +49,10 @@ Recipes and batches are saved in this browser by default. After connecting an ex
49
 
50
  The packaged worker supports Formula, Rally, Snowmobile, and Truck. Networks can use up to 20 inputs (13 raycasts plus 7 other sensors), 12 total layers, and 16 neurons per hidden layer. Formula, Rally, and Truck support up to five outputs; Snowmobile supports up to four and cannot use Handbrake. Fresh networks for every vehicle require Steering and Acceleration. Formula imports retain previously supported ordered output subsets; Rally, Snowmobile, and Truck imports must include Steering and Acceleration and follow their vehicle-specific output rules. Supported sensors use original-game defaults when optional parameters are omitted. These are the vanilla worker limits, not support for arbitrary modified-game networks. See [TRACK_LIBRARY.md](TRACK_LIBRARY.md) for track scope and provenance, and [WINNERS_AND_EVALUATION.md](WINNERS_AND_EVALUATION.md) for result interpretation.
51
 
 
 
 
 
52
  ## Visual replay and debugging
53
 
54
  Record a selected policy for 5 or 15 seconds, then play, pause, scrub, or step through actual native samples. The car black box shows measured motion, contacts, boost, raw/applied controls and the sensor inputs consumed by the policy at its recorded decision tick. Playback starts paused, the camera is manual, and optional pose smoothing does not interpolate telemetry. Recordings can be downloaded and reopened. A GPU-trained policy is replayed in a fresh CPU evaluation; this is separate from its original training generation. See [REPLAY_BLACKBOX.md](REPLAY_BLACKBOX.md) for capture phases and limits.
 
49
 
50
  The packaged worker supports Formula, Rally, Snowmobile, and Truck. Networks can use up to 20 inputs (13 raycasts plus 7 other sensors), 12 total layers, and 16 neurons per hidden layer. Formula, Rally, and Truck support up to five outputs; Snowmobile supports up to four and cannot use Handbrake. Fresh networks for every vehicle require Steering and Acceleration. Formula imports retain previously supported ordered output subsets; Rally, Snowmobile, and Truck imports must include Steering and Acceleration and follow their vehicle-specific output rules. Supported sensors use original-game defaults when optional parameters are omitted. These are the vanilla worker limits, not support for arbitrary modified-game networks. See [TRACK_LIBRARY.md](TRACK_LIBRARY.md) for track scope and provenance, and [WINNERS_AND_EVALUATION.md](WINNERS_AND_EVALUATION.md) for result interpretation.
51
 
52
+ ## Reward selection
53
+
54
+ Select any of the 14 supported reward metrics, or use **Add all 14 metrics** to add the complete set at once. The counter shows the selected distinct metrics and disables Add at the catalog maximum. Existing rows, weights and aggregation choices are preserved; newly added metrics use the same default aggregation and +1 weight as **Add reward**. Adjust weights and signs before training. Duplicate metrics must be corrected before adding all, saving or running. The five-row center preset is an example mix, not a reward limit.
55
+
56
  ## Visual replay and debugging
57
 
58
  Record a selected policy for 5 or 15 seconds, then play, pause, scrub, or step through actual native samples. The car black box shows measured motion, contacts, boost, raw/applied controls and the sensor inputs consumed by the policy at its recorded decision tick. Playback starts paused, the camera is manual, and optional pose smoothing does not interpolate telemetry. Recordings can be downloaded and reopened. A GPU-trained policy is replayed in a fresh CPU evaluation; this is separate from its original training generation. See [REPLAY_BLACKBOX.md](REPLAY_BLACKBOX.md) for capture phases and limits.
RELEASE_MANIFEST.json CHANGED
@@ -7,11 +7,11 @@
7
  "CUDA_REFERENCE.md": "f7453c3254d244f8b0de41fde5851cb025159f5cea4a4d1a5d8208a955550c6c",
8
  "DATA_POLICY.md": "fc5978528abe1c21c14882417ac8a706e04a0831bbd3fdb3de8c64442c1a7b3d",
9
  "FEEDBACK.md": "ad6d3f9f16ca1d85ac61e762b549de64c657be0d758300391b12d474bdbedbde",
10
- "FRONTEND_SOURCE.json": "e352bbd289fb931b88176d510e671c1c96300300cad886dfd7271f3151f0c36e",
11
  "GODOT_LICENSE": "fb2b3bb82c60f1e76841ba2122eb902f8e39c151476fe21ce10c9a05d1045304",
12
  "IMPORTER_BUILD_RECEIPT.json": "4ba5c533cc617105478e4d85c83f456a7dfa68b1388049624619871ba9e42e9d",
13
  "LICENSE": "53d5851b6440feeae36f72a5f57fcab37858c48b7084765ece4b0c54b9a28ed8",
14
- "README.md": "4fdd7d893ea5b5ff5ab927412bbd6ee124aec41f28b3c94a9063cc58cf2cdf59",
15
  "REPLAY_BLACKBOX.md": "a94c6b7e13273535746f63666254a359ef1dc18908711b8a4a33f2bf92dd82a8",
16
  "STORAGE_BOUNDARY.md": "1d0cab6c82723f1879d7b437bab6b0d21248436bce161f3fbfe6c0e83b9e87a1",
17
  "TRACK_LIBRARY.md": "be64c91633ef6696233df537afeef4c5175b2d55daac9336758d9eb3d24763c9",
@@ -65,13 +65,13 @@
65
  "builtin_tracks/tracks/Replica R08 Demolition Derby.track": "6f11a9ffbcb477c9d19964566b1b612d2a94675605246e93e21f27bbbdb357ef",
66
  "builtin_tracks/tracks/Replica R09 Eau Noir.track": "3ec0a78478cca1a4f0d2d98ac81f40783928adf271b70f0c32d295a6da2cdaa9",
67
  "builtin_tracks/tracks/Replica The Champions Trial.track": "a3bfb76422affc774b44c739b0636d08acb901fa7615b9adcb52c069f79834e1",
68
- "client.js": "acc63231b6575a2138972ac506595ab4344e5000a152d1e2a13efe6e31dc9194",
69
  "delivery.py": "6bdba2e87c3e19a41440cb16103695af27105f8bafbae38948bbb1b936ed0c1b",
70
  "gpu_tickets.py": "20928fcf1ca24648809ad8712c929377bdcc8b5e6b4387bfff3503ec5711928b",
71
- "help.html": "a12d5ad5e7d347267cab465f44e7ac709731f30982f8424e9f5d83c561881964",
72
  "hf-auth.js": "3f3938bbc0935e4e9230c094029ef46e03beea74be199887c50e053e03dafcfe",
73
  "hf_identity.py": "913fb1e089af21f376e8359a20cbc838d5bcd7971def6989a4382d53c1127b37",
74
- "index.html": "a2880dd93b52fe17bad4a69ff11854458b3453a79244247c14b1ce4f6a26770e",
75
  "native/analysis.patch": "42f79de67cb0734cffb2a7582f5479cab42ab82675884b8b42b9f158d46a98e8",
76
  "native/blackbox-build-receipt.json": "de3eea6c45a5ae4897287a97b0e2822fdae552a0655250bf940cef20d9b60d96",
77
  "native/blackbox-source.tar.gz": "56b43430c68b95bf7ac0d8312975c366c920f99b1653d790fb15b59453728e28",
 
7
  "CUDA_REFERENCE.md": "f7453c3254d244f8b0de41fde5851cb025159f5cea4a4d1a5d8208a955550c6c",
8
  "DATA_POLICY.md": "fc5978528abe1c21c14882417ac8a706e04a0831bbd3fdb3de8c64442c1a7b3d",
9
  "FEEDBACK.md": "ad6d3f9f16ca1d85ac61e762b549de64c657be0d758300391b12d474bdbedbde",
10
+ "FRONTEND_SOURCE.json": "1aded69faceb11b64ac78172d9a4064098b9d3e05ea37a6e31a5d3e3d3f3f7f7",
11
  "GODOT_LICENSE": "fb2b3bb82c60f1e76841ba2122eb902f8e39c151476fe21ce10c9a05d1045304",
12
  "IMPORTER_BUILD_RECEIPT.json": "4ba5c533cc617105478e4d85c83f456a7dfa68b1388049624619871ba9e42e9d",
13
  "LICENSE": "53d5851b6440feeae36f72a5f57fcab37858c48b7084765ece4b0c54b9a28ed8",
14
+ "README.md": "09f07538a4b0c32fa0c59f5c6d8c4f620f29f7d5001b93c79d71e10c87e01a35",
15
  "REPLAY_BLACKBOX.md": "a94c6b7e13273535746f63666254a359ef1dc18908711b8a4a33f2bf92dd82a8",
16
  "STORAGE_BOUNDARY.md": "1d0cab6c82723f1879d7b437bab6b0d21248436bce161f3fbfe6c0e83b9e87a1",
17
  "TRACK_LIBRARY.md": "be64c91633ef6696233df537afeef4c5175b2d55daac9336758d9eb3d24763c9",
 
65
  "builtin_tracks/tracks/Replica R08 Demolition Derby.track": "6f11a9ffbcb477c9d19964566b1b612d2a94675605246e93e21f27bbbdb357ef",
66
  "builtin_tracks/tracks/Replica R09 Eau Noir.track": "3ec0a78478cca1a4f0d2d98ac81f40783928adf271b70f0c32d295a6da2cdaa9",
67
  "builtin_tracks/tracks/Replica The Champions Trial.track": "a3bfb76422affc774b44c739b0636d08acb901fa7615b9adcb52c069f79834e1",
68
+ "client.js": "12746c2334f4342ed7e1a3182b085e02f17bc57d41d762f7f1ad612b45c240dd",
69
  "delivery.py": "6bdba2e87c3e19a41440cb16103695af27105f8bafbae38948bbb1b936ed0c1b",
70
  "gpu_tickets.py": "20928fcf1ca24648809ad8712c929377bdcc8b5e6b4387bfff3503ec5711928b",
71
+ "help.html": "41c5d99f2e378a16bde25e1c6bc1f7c85031f91414b499e826f6bea712b1f92d",
72
  "hf-auth.js": "3f3938bbc0935e4e9230c094029ef46e03beea74be199887c50e053e03dafcfe",
73
  "hf_identity.py": "913fb1e089af21f376e8359a20cbc838d5bcd7971def6989a4382d53c1127b37",
74
+ "index.html": "102f19a9c28397f76595f1a289a221065664635655295a103826b34d65ae95c1",
75
  "native/analysis.patch": "42f79de67cb0734cffb2a7582f5479cab42ab82675884b8b42b9f158d46a98e8",
76
  "native/blackbox-build-receipt.json": "de3eea6c45a5ae4897287a97b0e2822fdae552a0655250bf940cef20d9b60d96",
77
  "native/blackbox-source.tar.gz": "56b43430c68b95bf7ac0d8312975c366c920f99b1653d790fb15b59453728e28",
client.js CHANGED
@@ -189,10 +189,11 @@ function openReplayWorkspace({focusRecord=false}={}){
189
  target.tabIndex=-1;target.scrollIntoView({block:'start'});target.focus({preventScroll:true});
190
  }
191
  if($('result_replay'))$('result_replay').onclick=()=>openReplayWorkspace({focusRecord:true});
192
- const trainingOnlyControls=['population','generations','mutation','mutation_end','schedule','adaptive_mutation','selection','parents','elite','crossover','decay','preset','reward_preset','add','rewards'];
193
  function syncRunModeUI(unavailable=busy||bundleRestoring||bucketOperation){
194
  const evaluation=$('run_mode')?.value==='evaluate';
195
  for(const id of trainingOnlyControls){const node=$(id);if(node)node.disabled=unavailable||evaluation;}
 
196
  if($('native_settings'))$('native_settings').hidden=evaluation;
197
  if($('reward_settings'))$('reward_settings').hidden=evaluation;
198
  if($('freeze_settings'))$('freeze_settings').hidden=evaluation;
 
189
  target.tabIndex=-1;target.scrollIntoView({block:'start'});target.focus({preventScroll:true});
190
  }
191
  if($('result_replay'))$('result_replay').onclick=()=>openReplayWorkspace({focusRecord:true});
192
+ const trainingOnlyControls=['population','generations','mutation','mutation_end','schedule','adaptive_mutation','selection','parents','elite','crossover','decay','preset'];
193
  function syncRunModeUI(unavailable=busy||bundleRestoring||bucketOperation){
194
  const evaluation=$('run_mode')?.value==='evaluate';
195
  for(const id of trainingOnlyControls){const node=$(id);if(node)node.disabled=unavailable||evaluation;}
196
+ window.setRewardControlsBusy?.(unavailable||evaluation);
197
  if($('native_settings'))$('native_settings').hidden=evaluation;
198
  if($('reward_settings'))$('reward_settings').hidden=evaluation;
199
  if($('freeze_settings'))$('freeze_settings').hidden=evaluation;
help.html CHANGED
@@ -114,6 +114,7 @@
114
  <p class="small">These checks are bounded predictions for replica conditions, not original-game awards or Steam leaderboard results.</p>
115
  </section>
116
 
 
117
  <section class="section" id="suggested-line"><h2>Compare the recorded drive with a suggested route</h2><p>On Run &amp; results, use <strong>Calculate suggested route</strong>. Dashed yellow shows the mask-derived suggestion and green is the recorded car path. Road shape comes from the native part polygons. Dark shapes show static walls. Record or open a drive, then play, scrub or step through its measured car positions and black box.</p><p>The suggestion searches an 8-unit mask derived from native part polygons, preserves connected openings through split lanes, then checks every final segment for a 32 game-unit static wall margin. It does not model vehicle size, grip, braking or boost, so it is not a fastest-lap prediction or a guarantee that the car can follow it. Distance to the suggestion is a geometric measurement, not a driving score. Broken or infeasible routes report unavailable. This calculation runs in your browser and does not train or simulate a car.</p></section>
118
  <section class="section" id="freeze">
119
  <h2>Selective freezing</h2>
 
114
  <p class="small">These checks are bounded predictions for replica conditions, not original-game awards or Steam leaderboard results.</p>
115
  </section>
116
 
117
+ <section class="section" id="all-rewards"><h2>Select the full reward set</h2><p>Training &amp; recipes supports all 14 distinct reward metrics at once. Use <strong>Add all 14 metrics</strong> to add missing metrics without replacing existing weights, aggregation choices or row order. New metrics start with the same default aggregation and +1 weight as Add reward; review the signs and weights before training. The counter shows the selected distinct metrics, and Add stops at the supported maximum. If a metric appears twice, correct or remove the duplicate first.</p><p>The five-row center preset is one example mix. You can extend it to the full set. Reward editing is disabled during a run or frozen evaluation.</p></section>
118
  <section class="section" id="suggested-line"><h2>Compare the recorded drive with a suggested route</h2><p>On Run &amp; results, use <strong>Calculate suggested route</strong>. Dashed yellow shows the mask-derived suggestion and green is the recorded car path. Road shape comes from the native part polygons. Dark shapes show static walls. Record or open a drive, then play, scrub or step through its measured car positions and black box.</p><p>The suggestion searches an 8-unit mask derived from native part polygons, preserves connected openings through split lanes, then checks every final segment for a 32 game-unit static wall margin. It does not model vehicle size, grip, braking or boost, so it is not a fastest-lap prediction or a guarantee that the car can follow it. Distance to the suggestion is a geometric measurement, not a driving score. Broken or infeasible routes report unavailable. This calculation runs in your browser and does not train or simulate a car.</p></section>
119
  <section class="section" id="freeze">
120
  <h2>Selective freezing</h2>
index.html CHANGED
@@ -333,7 +333,7 @@
333
  <details id="freeze_settings"><summary>Selective freezing · optional</summary><p class="hint">Keep selected sensor inputs, hidden layers, or output controls unchanged while training. Choices follow the selected network and are saved with recipes. Frozen evaluation always uses an empty freeze selection.</p><div id="freeze_status" class="hint" role="status">Select a network to choose components.</div><div id="freeze_controls" class="row"></div></details>
334
  <details id="reward_settings"><summary>Rewards · presets and up to fourteen metrics</summary>
335
  <label for="reward_preset">Reward preset</label><select id="reward_preset" disabled><option value="custom">Custom</option><option value="distance" selected>First laps · distance only (+100 total score)</option><option value="center">Example mix · center, clearance, collisions</option><option value="clean">Clean driving · distance + collision penalty</option><option value="lap">Lap refinement · best lap only</option></select>
336
- <div id="rewards"></div><button id="add" disabled>+ Add reward</button><small>Weights −100…+100; absolute shares normalize to 100%. Fourteen distinct metrics maximum. The current vehicle runner accepts these metrics; choose them for the behavior you want to train.</small>
337
  </details></div>
338
  <details id="run_settings"><summary>Run settings · timing and elimination</summary><div class="row"><div><span class="hint">Fixed simulation timing</span><input id="control_hz" type="hidden" value="60"><output class="fixed-rate" id="fixed_timing">60 Hz physics · 60 Hz decisions<br>One controller update on every physics tick. Fixed for every new run.</output></div><div><label for="workers">Parallel simulations</label><select id="workers" disabled><option>1</option><option selected>1</option><option>2</option><option>3</option><option>4</option></select></div></div><small>Physics runs at 60 Hz. CPU jobs can use 1–4 worker threads.</small><div id="seed_group"><label for="seed">Random seed</label><input id="seed" type="number" min="0" max="2147483647" value="42105" disabled></div><label><input id="collision" type="checkbox" disabled> Eliminate on wall contact</label><label><input id="idle" type="checkbox" disabled> Eliminate when stuck</label><label for="clean_min_laps">Minimum forward laps for a clean-lap winner</label><input id="clean_min_laps" type="number" min="1" max="100" value="2"><small>Winner classification only. Zero physical contacts are required. This does not change your reward recipe.</small></details></div></div></section>
339
  <section id="batch" class="pane" role="tabpanel" aria-labelledby="tab-batch" hidden><div class="card"><div class="section-heading"><h2>Training batches</h2><a href="/help#batches" target="_blank" rel="noopener">How batches work ↗</a></div><p class="section-intro">Arrange saved recipes into a sequence of up to 20 training steps. Use the same recipe more than once to keep training with those settings.</p><div class="batch-layout"><div>
@@ -535,7 +535,7 @@
535
  })(typeof window!=='undefined'?window:globalThis);
536
  </script><script>(() => {
537
  const q=id=>document.getElementById(id),api=window.RecipeLibrary;
538
- let refreshWorkspace=()=>{},libraryBusy=false;
539
  const message=text=>{q('recipe_feedback').textContent=text;q('batches_feedback').textContent=text;};
540
  const fields=['population','generations','mutation','seed','mutation_end','schedule','selection','parents','elite','crossover','decay','adaptive_mutation','control_hz','workers','collision','idle'];
541
  const booleans=['adaptive_mutation','collision','idle'];
@@ -567,6 +567,18 @@
567
  window.setFreezeNetwork=network=>{freezeNetwork=network?{shape:[...network.NeuralNetwork.Shape],sensors:network.Sensors.map(x=>({...x})),outputs:[...network.Outputs]}:null;renderFreeze();};
568
  q('freeze_controls').addEventListener('change',event=>{const input=event.target;if(!input.matches('input[data-freeze-kind]'))return;const kind=input.dataset.freezeKind,id=Number(input.value);selectedFreeze[kind]=input.checked?[...selectedFreeze[kind],id].sort((a,b)=>a-b):selectedFreeze[kind].filter(x=>x!==id);q('preset').value='custom';renderFreeze();});
569
  function rewards(){return [...q('rewards').children].map(row=>({metric:row.children[0].value,type:row.children[1].value,weight:Number(row.children[2].value)}));}
 
 
 
 
 
 
 
 
 
 
 
 
570
  function rewardRow(r){
571
  const row=document.createElement('div');row.className='reward';
572
  const metric=document.createElement('select'),type=document.createElement('select'),weight=document.createElement('input'),remove=document.createElement('button');
@@ -574,10 +586,10 @@
574
  for(const name of api.metrics)metric.add(new Option(name.replaceAll('_',' '),name));metric.value=r.metric;
575
  for(const name of ['default','average'])type.add(new Option(name,name));type.value=r.type;
576
  weight.type='number';weight.min=-100;weight.max=100;weight.step=1;weight.value=r.weight;
577
- const custom=()=>{q('reward_preset').value='custom';};
578
  metric.onchange=custom;type.onchange=custom;weight.oninput=custom;
579
  remove.textContent='×';remove.setAttribute('aria-label','Remove reward');remove.onclick=()=>{row.remove();custom();};
580
- row.append(metric,type,weight,remove);q('rewards').append(row);
581
  }
582
  function currentSettings(){
583
  const data={};for(const name of fields)data[name]=booleans.includes(name)?q(name).checked:strings.includes(name)?q(name).value:Number(q(name).value);
@@ -606,7 +618,7 @@
606
  for(const kind of ['layers','inputs','outputs'])selectedFreeze[kind]=[...data.freeze[kind]];renderFreeze();
607
  q('recipe_draft').value=JSON.stringify(data,null,2);updateWorkBudget();refreshWorkspace();
608
  }
609
- const editable=[...fields,'training_ticks','add','reward_preset','preset','starter_settings_reset'];
610
  for(const id of editable)q(id).disabled=false;
611
  q('end_rate_group').hidden=false;
612
  const firstRunSettings={...api.defaults,population:32,generations:20,ticks:900,mutation:0.3,mutation_end:0.3,parents:1,elite:1,decay:0,workers:1,collision:false,idle:false,rewards:[{metric:'total_score',type:'default',weight:100}]};
@@ -634,14 +646,20 @@
634
  window.setTrainingBusy=(settingsBusy,controlsBusy=settingsBusy)=>{
635
  libraryBusy=controlsBusy;
636
  for(const id of editable)q(id).disabled=settingsBusy;
637
- for(const node of q('rewards').querySelectorAll('input,select,button'))node.disabled=settingsBusy;
638
  for(const id of ['recipe_save','recipe_load','recipe_apply_draft','recipe_slot','recipe_name','recipe_import','recipe_clear','batch_save','batch_load','batch_slot','batch_name','batch_recipe_sequence','carry','queue_add','batch_add_recipe'])q(id).disabled=controlsBusy;
639
  refreshWorkspace(false);
640
  };
641
  for(const id of ['population','generations','training_ticks'])q(id).addEventListener('input',updateWorkBudget);
642
  q('run_mode')?.addEventListener('change',updateWorkBudget);
643
  q('backend')?.addEventListener('change',updateWorkBudget);
644
- q('add').onclick=()=>{if(q('rewards').children.length>=api.metrics.length)return;const used=rewards().map(r=>r.metric);rewardRow({metric:api.metrics.find(m=>!used.includes(m)),type:'default',weight:1});q('reward_preset').value='custom';};
 
 
 
 
 
 
645
  q('reward_preset').onchange=()=>{
646
  const key=q('reward_preset').value;if(key==='custom')return;
647
  const rows={distance:[['total_score',100]],center:[['total_score',100],['total_distance_from_center',-60],['total_distance_from_wall',10],['collision_count',-100],['best_lap_performance',5]],clean:[['total_score',100],['collision_count',-30]],lap:[['best_lap_performance',100]]};
 
333
  <details id="freeze_settings"><summary>Selective freezing · optional</summary><p class="hint">Keep selected sensor inputs, hidden layers, or output controls unchanged while training. Choices follow the selected network and are saved with recipes. Frozen evaluation always uses an empty freeze selection.</p><div id="freeze_status" class="hint" role="status">Select a network to choose components.</div><div id="freeze_controls" class="row"></div></details>
334
  <details id="reward_settings"><summary>Rewards · presets and up to fourteen metrics</summary>
335
  <label for="reward_preset">Reward preset</label><select id="reward_preset" disabled><option value="custom">Custom</option><option value="distance" selected>First laps · distance only (+100 total score)</option><option value="center">Example mix · center, clearance, collisions</option><option value="clean">Clean driving · distance + collision penalty</option><option value="lap">Lap refinement · best lap only</option></select>
336
+ <div id="rewards"></div><div class="toolbar"><button id="add" type="button" disabled>+ Add reward</button><button id="add_all_rewards" type="button" disabled>Add all 14 metrics</button></div><p id="reward_count" class="hint" role="status" aria-live="polite"></p><small>Weights −100…+100; absolute shares normalize to 100%. One row per metric, up to the full supported catalog. Adding all keeps existing rewards unchanged; new metrics start at +1. Adjust signs and weights for the driving you want to train.</small>
337
  </details></div>
338
  <details id="run_settings"><summary>Run settings · timing and elimination</summary><div class="row"><div><span class="hint">Fixed simulation timing</span><input id="control_hz" type="hidden" value="60"><output class="fixed-rate" id="fixed_timing">60 Hz physics · 60 Hz decisions<br>One controller update on every physics tick. Fixed for every new run.</output></div><div><label for="workers">Parallel simulations</label><select id="workers" disabled><option>1</option><option selected>1</option><option>2</option><option>3</option><option>4</option></select></div></div><small>Physics runs at 60 Hz. CPU jobs can use 1–4 worker threads.</small><div id="seed_group"><label for="seed">Random seed</label><input id="seed" type="number" min="0" max="2147483647" value="42105" disabled></div><label><input id="collision" type="checkbox" disabled> Eliminate on wall contact</label><label><input id="idle" type="checkbox" disabled> Eliminate when stuck</label><label for="clean_min_laps">Minimum forward laps for a clean-lap winner</label><input id="clean_min_laps" type="number" min="1" max="100" value="2"><small>Winner classification only. Zero physical contacts are required. This does not change your reward recipe.</small></details></div></div></section>
339
  <section id="batch" class="pane" role="tabpanel" aria-labelledby="tab-batch" hidden><div class="card"><div class="section-heading"><h2>Training batches</h2><a href="/help#batches" target="_blank" rel="noopener">How batches work ↗</a></div><p class="section-intro">Arrange saved recipes into a sequence of up to 20 training steps. Use the same recipe more than once to keep training with those settings.</p><div class="batch-layout"><div>
 
535
  })(typeof window!=='undefined'?window:globalThis);
536
  </script><script>(() => {
537
  const q=id=>document.getElementById(id),api=window.RecipeLibrary;
538
+ let refreshWorkspace=()=>{},libraryBusy=false,rewardControlsBusy=false;
539
  const message=text=>{q('recipe_feedback').textContent=text;q('batches_feedback').textContent=text;};
540
  const fields=['population','generations','mutation','seed','mutation_end','schedule','selection','parents','elite','crossover','decay','adaptive_mutation','control_hz','workers','collision','idle'];
541
  const booleans=['adaptive_mutation','collision','idle'];
 
567
  window.setFreezeNetwork=network=>{freezeNetwork=network?{shape:[...network.NeuralNetwork.Shape],sensors:network.Sensors.map(x=>({...x})),outputs:[...network.Outputs]}:null;renderFreeze();};
568
  q('freeze_controls').addEventListener('change',event=>{const input=event.target;if(!input.matches('input[data-freeze-kind]'))return;const kind=input.dataset.freezeKind,id=Number(input.value);selectedFreeze[kind]=input.checked?[...selectedFreeze[kind],id].sort((a,b)=>a-b):selectedFreeze[kind].filter(x=>x!==id);q('preset').value='custom';renderFreeze();});
569
  function rewards(){return [...q('rewards').children].map(row=>({metric:row.children[0].value,type:row.children[1].value,weight:Number(row.children[2].value)}));}
570
+ function syncRewardControls(){
571
+ const rows=rewards(),used=new Set(rows.map(r=>r.metric)),maximum=api.metrics.length;
572
+ const unavailable=rewardControlsBusy||q('run_mode')?.value==='evaluate',duplicates=used.size!==rows.length;
573
+ q('add').disabled=unavailable||rows.length>=maximum;
574
+ q('add_all_rewards').disabled=unavailable||duplicates||rows.length>=maximum;
575
+ q('reward_preset').disabled=unavailable;
576
+ q('add_all_rewards').textContent='Add all '+maximum+' metrics';
577
+ for(const node of q('rewards').querySelectorAll('input,select,button'))node.disabled=unavailable;
578
+ const text=used.size+' / '+maximum+' distinct reward metrics selected. '+(duplicates?'A metric is selected more than once. Choose each metric once before adding all.':used.size===maximum?'All supported metrics are included.':'Add individual rewards or add all remaining metrics at once.');
579
+ if(q('reward_count').textContent!==text)q('reward_count').textContent=text;
580
+ }
581
+ window.setRewardControlsBusy=unavailable=>{rewardControlsBusy=!!unavailable;syncRewardControls();};
582
  function rewardRow(r){
583
  const row=document.createElement('div');row.className='reward';
584
  const metric=document.createElement('select'),type=document.createElement('select'),weight=document.createElement('input'),remove=document.createElement('button');
 
586
  for(const name of api.metrics)metric.add(new Option(name.replaceAll('_',' '),name));metric.value=r.metric;
587
  for(const name of ['default','average'])type.add(new Option(name,name));type.value=r.type;
588
  weight.type='number';weight.min=-100;weight.max=100;weight.step=1;weight.value=r.weight;
589
+ const custom=()=>{q('reward_preset').value='custom';syncRewardControls();};
590
  metric.onchange=custom;type.onchange=custom;weight.oninput=custom;
591
  remove.textContent='×';remove.setAttribute('aria-label','Remove reward');remove.onclick=()=>{row.remove();custom();};
592
+ row.append(metric,type,weight,remove);q('rewards').append(row);syncRewardControls();
593
  }
594
  function currentSettings(){
595
  const data={};for(const name of fields)data[name]=booleans.includes(name)?q(name).checked:strings.includes(name)?q(name).value:Number(q(name).value);
 
618
  for(const kind of ['layers','inputs','outputs'])selectedFreeze[kind]=[...data.freeze[kind]];renderFreeze();
619
  q('recipe_draft').value=JSON.stringify(data,null,2);updateWorkBudget();refreshWorkspace();
620
  }
621
+ const editable=[...fields,'training_ticks','preset','starter_settings_reset'];
622
  for(const id of editable)q(id).disabled=false;
623
  q('end_rate_group').hidden=false;
624
  const firstRunSettings={...api.defaults,population:32,generations:20,ticks:900,mutation:0.3,mutation_end:0.3,parents:1,elite:1,decay:0,workers:1,collision:false,idle:false,rewards:[{metric:'total_score',type:'default',weight:100}]};
 
646
  window.setTrainingBusy=(settingsBusy,controlsBusy=settingsBusy)=>{
647
  libraryBusy=controlsBusy;
648
  for(const id of editable)q(id).disabled=settingsBusy;
649
+ window.setRewardControlsBusy(settingsBusy);
650
  for(const id of ['recipe_save','recipe_load','recipe_apply_draft','recipe_slot','recipe_name','recipe_import','recipe_clear','batch_save','batch_load','batch_slot','batch_name','batch_recipe_sequence','carry','queue_add','batch_add_recipe'])q(id).disabled=controlsBusy;
651
  refreshWorkspace(false);
652
  };
653
  for(const id of ['population','generations','training_ticks'])q(id).addEventListener('input',updateWorkBudget);
654
  q('run_mode')?.addEventListener('change',updateWorkBudget);
655
  q('backend')?.addEventListener('change',updateWorkBudget);
656
+ q('add').onclick=()=>{if(rewardControlsBusy||q('run_mode')?.value==='evaluate'||q('rewards').children.length>=api.metrics.length)return;const used=rewards().map(r=>r.metric);rewardRow({metric:api.metrics.find(m=>!used.includes(m)),type:'default',weight:1});q('reward_preset').value='custom';};
657
+ q('add_all_rewards').onclick=()=>{
658
+ const rows=rewards(),used=new Set(rows.map(r=>r.metric));
659
+ if(rewardControlsBusy||q('run_mode')?.value==='evaluate'||used.size!==rows.length||rows.length>=api.metrics.length)return;
660
+ for(const metric of api.metrics)if(!used.has(metric))rewardRow({metric,type:'default',weight:1});
661
+ q('reward_preset').value='custom';syncRewardControls();
662
+ };
663
  q('reward_preset').onchange=()=>{
664
  const key=q('reward_preset').value;if(key==='custom')return;
665
  const rows={distance:[['total_score',100]],center:[['total_score',100],['total_distance_from_center',-60],['total_distance_from_wall',10],['collision_count',-100],['best_lap_performance',5]],clean:[['total_score',100],['collision_count',-30]],lap:[['best_lap_performance',100]]};