TiGa-RCE commited on
Commit
9c8994d
·
verified ·
1 Parent(s): 7bf895a

Upgrade track telemetry replay visualizer, KPI strip, generation chart, and 2D vehicle outline preview

Browse files
FRONTEND_SOURCE.json CHANGED
@@ -5,19 +5,19 @@
5
  "source": "Runner r14 single-engine browser UI; selected .track tile renderer added from simulator atlas",
6
  "source_sha256": "65d1e5f2c4e19b4951b828e82510005ab99a5270cf3dcb9f4eaa0fa1af8c40f8"
7
  },
8
- "client_sha256": "cbb1dd26a8fe4256abf827903df94749dc8e4f99912d29dba7f74ba6e477b851",
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": "v1.2-measured-capacity",
11
- "output_sha256": "7bdaf7ca11a455525c22432298e120ca2fa5257283b9815467e005e09da07a26",
12
  "packaged_frontend_files": {
13
  "assets/track-atlas.json": "d97c8900e3e5fb17734b5bd61d1f4f4b196d9be30af9848e2e273ae7e4079ae4",
14
- "client.js": "cbb1dd26a8fe4256abf827903df94749dc8e4f99912d29dba7f74ba6e477b851",
15
- "help.html": "4ce8f3995723a419d810c60212e996d71bfc0fcb61d44bf4fa8500030c356f47",
16
  "hf-auth.js": "3f3938bbc0935e4e9230c094029ef46e03beea74be199887c50e053e03dafcfe",
17
- "index.html": "7bdaf7ca11a455525c22432298e120ca2fa5257283b9815467e005e09da07a26",
18
  "track-library.js": "351ec4c026b49420ce963f4310cb000d9e987fd28e6dac5ecd0176509340f6e6",
19
- "track-map.js": "ab5374aa49c7ccec90a787ee8c56cd71beb56b903bf2bbc322783e6f43c009f0"
20
  },
21
- "packaged_frontend_sha256": "da29a0f25c5e6778b017e748a1103f2fee02b53af66bda693cbf926e72a6b62d",
22
  "status": "Local measured-capacity candidate; hosted qualification pending"
23
  }
 
5
  "source": "Runner r14 single-engine browser UI; selected .track tile renderer added from simulator atlas",
6
  "source_sha256": "65d1e5f2c4e19b4951b828e82510005ab99a5270cf3dcb9f4eaa0fa1af8c40f8"
7
  },
8
+ "client_sha256": "e42cd311e5a16baeec9caa8909ad1b8e08532b10cab320c14bef8a47d7101b53",
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": "v1.2-measured-capacity",
11
+ "output_sha256": "16c6a720e3383e23aca7065466b25d573e50b0ea625fcca00e10d6844c9161b7",
12
  "packaged_frontend_files": {
13
  "assets/track-atlas.json": "d97c8900e3e5fb17734b5bd61d1f4f4b196d9be30af9848e2e273ae7e4079ae4",
14
+ "client.js": "e42cd311e5a16baeec9caa8909ad1b8e08532b10cab320c14bef8a47d7101b53",
15
+ "help.html": "58959e994a8e4bbd3c70b2a6256cb699ac615cde718c6d0288fabeee909098a2",
16
  "hf-auth.js": "3f3938bbc0935e4e9230c094029ef46e03beea74be199887c50e053e03dafcfe",
17
+ "index.html": "16c6a720e3383e23aca7065466b25d573e50b0ea625fcca00e10d6844c9161b7",
18
  "track-library.js": "351ec4c026b49420ce963f4310cb000d9e987fd28e6dac5ecd0176509340f6e6",
19
+ "track-map.js": "cf84f311e64d2d029c0fba43dd7b8ebc8d63177c9edd3577a45591c1c28c41e4"
20
  },
21
+ "packaged_frontend_sha256": "c25cad5b810984dd9e428c8019b33d7a4d347b375d9cb5fd30ad579fe13fa137",
22
  "status": "Local measured-capacity candidate; hosted qualification pending"
23
  }
RELEASE_MANIFEST.json CHANGED
@@ -7,7 +7,7 @@
7
  "CUDA_REFERENCE.md": "f7453c3254d244f8b0de41fde5851cb025159f5cea4a4d1a5d8208a955550c6c",
8
  "DATA_POLICY.md": "fc5978528abe1c21c14882417ac8a706e04a0831bbd3fdb3de8c64442c1a7b3d",
9
  "FEEDBACK.md": "f95b8f6afbf4a360976c0edca5d52f5b3a0b668c9a944d4947b840a7c53a122a",
10
- "FRONTEND_SOURCE.json": "65733d3682f4d0b320b4d6dc5187c895e1747671d369da9bfd1fb0edd9ae2c21",
11
  "GODOT_LICENSE": "fb2b3bb82c60f1e76841ba2122eb902f8e39c151476fe21ce10c9a05d1045304",
12
  "IMPORTER_BUILD_RECEIPT.json": "4ba5c533cc617105478e4d85c83f456a7dfa68b1388049624619871ba9e42e9d",
13
  "LICENSE": "53d5851b6440feeae36f72a5f57fcab37858c48b7084765ece4b0c54b9a28ed8",
@@ -59,13 +59,13 @@
59
  "builtin_tracks/tracks/Replica R08 Demolition Derby.track": "6f11a9ffbcb477c9d19964566b1b612d2a94675605246e93e21f27bbbdb357ef",
60
  "builtin_tracks/tracks/Replica R09 Eau Noir.track": "3ec0a78478cca1a4f0d2d98ac81f40783928adf271b70f0c32d295a6da2cdaa9",
61
  "builtin_tracks/tracks/Replica The Champions Trial.track": "a3bfb76422affc774b44c739b0636d08acb901fa7615b9adcb52c069f79834e1",
62
- "client.js": "0dca9e5b08a974c0f37f9ad5d405b24ff76a011e62b06feb2168860513d52744",
63
  "delivery.py": "6bdba2e87c3e19a41440cb16103695af27105f8bafbae38948bbb1b936ed0c1b",
64
  "gpu_tickets.py": "20928fcf1ca24648809ad8712c929377bdcc8b5e6b4387bfff3503ec5711928b",
65
  "help.html": "58959e994a8e4bbd3c70b2a6256cb699ac615cde718c6d0288fabeee909098a2",
66
  "hf-auth.js": "3f3938bbc0935e4e9230c094029ef46e03beea74be199887c50e053e03dafcfe",
67
  "hf_identity.py": "913fb1e089af21f376e8359a20cbc838d5bcd7971def6989a4382d53c1127b37",
68
- "index.html": "b9a8b6297b0e48d00be6452be040961aa258cb325a238d6d2adda3334d7b32b1",
69
  "native/analysis.patch": "42f79de67cb0734cffb2a7582f5479cab42ab82675884b8b42b9f158d46a98e8",
70
  "native/build-receipt.json": "559a7e4e382f9f7caa1c5ceb566a5a0bbefad2ed3a2e2cf604e31c9c3890c348",
71
  "native/physics-build-receipt.json": "63561c108554a09884cc68a1bfadfb8a3f530a800de0700ff671d99e558609fe",
@@ -83,7 +83,7 @@
83
  "star_check.py": "5c17080957b81ffccf0de8314635b471562e55c92c97a1207d9034a99279b2c3",
84
  "steam_leaderboard.py": "d76397c39cae58f636ddf9a9be1a46ae3d77fefd6b7269feac73d34d97165481",
85
  "track-library.js": "351ec4c026b49420ce963f4310cb000d9e987fd28e6dac5ecd0176509340f6e6",
86
- "track-map.js": "a5e76598d5988c0446ad3b5796b601467cf9423e6daddf21753258b6eee0c195",
87
  "training_config.py": "6eb7282bbee3b3a6273bddc29c949d9904067414c26e501d815012f033173a1c",
88
  "vendor/gradio-client/LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4",
89
  "vendor/gradio-client/dist/index.min.js": "fdd776db8633354f118fe2908c980592283968737e6b91dc3aa86d648da5c3fd",
 
7
  "CUDA_REFERENCE.md": "f7453c3254d244f8b0de41fde5851cb025159f5cea4a4d1a5d8208a955550c6c",
8
  "DATA_POLICY.md": "fc5978528abe1c21c14882417ac8a706e04a0831bbd3fdb3de8c64442c1a7b3d",
9
  "FEEDBACK.md": "f95b8f6afbf4a360976c0edca5d52f5b3a0b668c9a944d4947b840a7c53a122a",
10
+ "FRONTEND_SOURCE.json": "e91215aedc0af1b701817e50196e3e9beef118b703413f109d3bf2b75c58513d",
11
  "GODOT_LICENSE": "fb2b3bb82c60f1e76841ba2122eb902f8e39c151476fe21ce10c9a05d1045304",
12
  "IMPORTER_BUILD_RECEIPT.json": "4ba5c533cc617105478e4d85c83f456a7dfa68b1388049624619871ba9e42e9d",
13
  "LICENSE": "53d5851b6440feeae36f72a5f57fcab37858c48b7084765ece4b0c54b9a28ed8",
 
59
  "builtin_tracks/tracks/Replica R08 Demolition Derby.track": "6f11a9ffbcb477c9d19964566b1b612d2a94675605246e93e21f27bbbdb357ef",
60
  "builtin_tracks/tracks/Replica R09 Eau Noir.track": "3ec0a78478cca1a4f0d2d98ac81f40783928adf271b70f0c32d295a6da2cdaa9",
61
  "builtin_tracks/tracks/Replica The Champions Trial.track": "a3bfb76422affc774b44c739b0636d08acb901fa7615b9adcb52c069f79834e1",
62
+ "client.js": "e42cd311e5a16baeec9caa8909ad1b8e08532b10cab320c14bef8a47d7101b53",
63
  "delivery.py": "6bdba2e87c3e19a41440cb16103695af27105f8bafbae38948bbb1b936ed0c1b",
64
  "gpu_tickets.py": "20928fcf1ca24648809ad8712c929377bdcc8b5e6b4387bfff3503ec5711928b",
65
  "help.html": "58959e994a8e4bbd3c70b2a6256cb699ac615cde718c6d0288fabeee909098a2",
66
  "hf-auth.js": "3f3938bbc0935e4e9230c094029ef46e03beea74be199887c50e053e03dafcfe",
67
  "hf_identity.py": "913fb1e089af21f376e8359a20cbc838d5bcd7971def6989a4382d53c1127b37",
68
+ "index.html": "16c6a720e3383e23aca7065466b25d573e50b0ea625fcca00e10d6844c9161b7",
69
  "native/analysis.patch": "42f79de67cb0734cffb2a7582f5479cab42ab82675884b8b42b9f158d46a98e8",
70
  "native/build-receipt.json": "559a7e4e382f9f7caa1c5ceb566a5a0bbefad2ed3a2e2cf604e31c9c3890c348",
71
  "native/physics-build-receipt.json": "63561c108554a09884cc68a1bfadfb8a3f530a800de0700ff671d99e558609fe",
 
83
  "star_check.py": "5c17080957b81ffccf0de8314635b471562e55c92c97a1207d9034a99279b2c3",
84
  "steam_leaderboard.py": "d76397c39cae58f636ddf9a9be1a46ae3d77fefd6b7269feac73d34d97165481",
85
  "track-library.js": "351ec4c026b49420ce963f4310cb000d9e987fd28e6dac5ecd0176509340f6e6",
86
+ "track-map.js": "cf84f311e64d2d029c0fba43dd7b8ebc8d63177c9edd3577a45591c1c28c41e4",
87
  "training_config.py": "6eb7282bbee3b3a6273bddc29c949d9904067414c26e501d815012f033173a1c",
88
  "vendor/gradio-client/LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4",
89
  "vendor/gradio-client/dist/index.min.js": "fdd776db8633354f118fe2908c980592283968737e6b91dc3aa86d648da5c3fd",
__pycache__/analysis_result.cpython-313.pyc ADDED
Binary file (14.7 kB). View file
 
__pycache__/app_release.cpython-313.pyc ADDED
Binary file (2.45 kB). View file
 
__pycache__/bucket_storage.cpython-313.pyc ADDED
Binary file (46.8 kB). View file
 
__pycache__/build_info.cpython-313.pyc ADDED
Binary file (2.07 kB). View file
 
__pycache__/builtin_tracks.cpython-313.pyc ADDED
Binary file (5.25 kB). View file
 
__pycache__/delivery.cpython-313.pyc ADDED
Binary file (16 kB). View file
 
__pycache__/hf_identity.cpython-313.pyc ADDED
Binary file (2.53 kB). View file
 
__pycache__/portable_bundle.cpython-313.pyc ADDED
Binary file (68 kB). View file
 
__pycache__/server.cpython-313.pyc ADDED
Binary file (33.4 kB). View file
 
__pycache__/star_check.cpython-313.pyc ADDED
Binary file (29.6 kB). View file
 
__pycache__/steam_leaderboard.cpython-313.pyc ADDED
Binary file (15.5 kB). View file
 
__pycache__/training_config.cpython-313.pyc ADDED
Binary file (7.21 kB). View file
 
__pycache__/worker.cpython-313.pyc ADDED
Binary file (33 kB). View file
 
client.js CHANGED
@@ -458,9 +458,9 @@ const networkSource=()=>stagedNetwork || $('network').files[0];
458
  window.addEventListener('ald:batch-draft-change',buttons);
459
  document.addEventListener('track-library-change',buttons);
460
  async function refreshFreezeNetwork(file){
461
- if(!file){window.setFreezeNetwork?.(null);return;}
462
- try{const network=await validateNetworkBytes(new Uint8Array(await file.arrayBuffer()));if(networkSource()===file)window.setFreezeNetwork?.(network);}
463
- catch(error){if(networkSource()===file){window.setFreezeNetwork?.(null);if($('freeze_status'))$('freeze_status').textContent='Network metadata unavailable: '+error.message;}}
464
  }
465
  $('network').onchange=()=>{stagedNetwork=null;selectedWinner=null;if($('networkChoice'))$('networkChoice').textContent='Uploaded file selected.';refreshFreezeNetwork(networkSource());buttons();};
466
  window.TrainingClient={
@@ -685,6 +685,7 @@ function renderStarCheck(){
685
  function renderAnalysisPanel(){
686
  const panel=$('analysisPanel');if(!panel)return;
687
  renderStarCheck();
 
688
  panel.hidden=!localAnalysis;
689
  if(!localAnalysis){$('winnerTools').hidden=true;$('evaluationTools').hidden=true;$('winnerRows').replaceChildren();$('winner_continue').replaceChildren();$('evaluationRows').replaceChildren();return;}
690
  const {summary}=localAnalysis,train=summary.mode==='train';
@@ -715,6 +716,7 @@ async function useWinner(role,mode='evaluate'){
715
  const item=localAnalysis?.winnerNetworks?.[role]??comparisonPlan?.winnerNetworks?.[role];if(!item)throw Error('That winner policy is unavailable');
716
  const file=new File([item.bytes],role+'-'+item.candidate_id+'.evolution',{type:'application/octet-stream'});
717
  window.TrainingClient.useNetwork(file);selectedWinner=role;
 
718
  if($('winner_continue'))$('winner_continue').value=role;
719
  if($('run_mode'))$('run_mode').value=mode;syncRunModeUI();buttons();
720
  say(mode==='train'?'Selected '+role.replaceAll('_',' ')+' policy as the starting network for a new training run. The next Run click starts it with the selected recipe.':'Selected '+role.replaceAll('_',' ')+' policy for frozen evaluation. Its weights will stay unchanged; the next Run click starts it.');
@@ -750,6 +752,7 @@ function showDescriptor(){
750
  if(!Number.isInteger(generation)||!Number.isInteger(car)){ $('descriptor_status').textContent='Enter whole generation and car numbers.';return; }
751
  const row=localAnalysis.descriptorRows.find(item=>item.generation===generation&&item.car===car);
752
  if(!row){$('descriptor_status').textContent='No descriptor was recorded for that generation and car.';$('descriptor_tables').replaceChildren();return;}
 
753
  const wrap=document.createElement('div');
754
  const summary=document.createElement('p');summary.textContent='Candidate '+row.candidate_id+' · ticks '+row.ticks_elapsed+' · stats updates '+row.stats_update_count+' · weights '+row.weights_sha256;wrap.append(summary);
755
  const table=(title,entries)=>{const h=document.createElement('h3');h.textContent=title;wrap.append(h);const t=document.createElement('table'),body=document.createElement('tbody');for(const [name,value] of entries){const tr=document.createElement('tr'),a=document.createElement('th'),b=document.createElement('td');a.textContent=name;b.textContent=value===null?'Not observed':typeof value==='number'?String(value):String(value);tr.append(a,b);body.append(tr);}t.append(body);wrap.append(t);};
@@ -839,6 +842,7 @@ async function parseRunAnalysisArchive(bytes,descriptor,reportedSummary){
839
  if(!matched)throw Error('Winner candidate is missing from the descriptor rows');
840
  const network=await validateNetworkBytes(file.bytes);if(network.Type!==summary.conditions.vehicle_type)throw Error('Winner network vehicle type does not match analysis conditions');
841
  winnerNetworks[role]={bytes:file.bytes,sha256:file.sha256,candidate_id:winner.candidate_id,vehicle_type:summary.conditions.vehicle_type};
 
842
  }else if(file)throw Error('Unavailable winner unexpectedly has a network artifact');
843
  }
844
  let evaluation=null;
@@ -897,11 +901,64 @@ function validatePrimaryAnalysisMatch(primarySha,parsed){
897
  return true;
898
  }
899
  window.RunAnalysis={parseArchive:parseRunAnalysisArchive,validatePrimaryMatch:validatePrimaryAnalysisMatch,validateComparisonRequest,uniqueWinnerRoles,validateStarCheck};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
900
  function showDetails(r){
901
  const metric=n=>typeof n==='number'?n.toFixed(3):'Not reported';
902
  if($('effectiveSettings'))$('effectiveSettings').textContent=JSON.stringify(r?.settings??{},null,2);
903
  if($('generationHistory'))$('generationHistory').textContent=(r?.history??[]).map(h=>
904
  'Generation '+h.generation+' · mutation '+metric(h.mutation)+' · best '+metric(h.best_score)+' · mean '+metric(h.mean_score)+' · lap '+(h.best_lap_seconds??'none')+' · cars lapped '+(h.lapped_cars??'not reported')+' · ticks '+metric(h.simulated_ticks)).join('\n');
 
 
 
 
 
 
 
 
 
 
 
 
 
905
  }
906
  $('run').onclick = async () => {
907
  const comparisonAttempt=!!comparisonPlan,comparisonRole=comparisonAttempt?comparisonPlan.roles[comparisonPlan.index]:null;
 
458
  window.addEventListener('ald:batch-draft-change',buttons);
459
  document.addEventListener('track-library-change',buttons);
460
  async function refreshFreezeNetwork(file){
461
+ if(!file){window.setFreezeNetwork?.(null);window.TrackReplay?.setStagedNetwork?.(null);return;}
462
+ try{const network=await validateNetworkBytes(new Uint8Array(await file.arrayBuffer()));if(networkSource()===file){window.setFreezeNetwork?.(network);window.TrackReplay?.setStagedNetwork?.(network);}}
463
+ catch(error){if(networkSource()===file){window.setFreezeNetwork?.(null);window.TrackReplay?.setStagedNetwork?.(null);if($('freeze_status'))$('freeze_status').textContent='Network metadata unavailable: '+error.message;}}
464
  }
465
  $('network').onchange=()=>{stagedNetwork=null;selectedWinner=null;if($('networkChoice'))$('networkChoice').textContent='Uploaded file selected.';refreshFreezeNetwork(networkSource());buttons();};
466
  window.TrainingClient={
 
685
  function renderAnalysisPanel(){
686
  const panel=$('analysisPanel');if(!panel)return;
687
  renderStarCheck();
688
+ window.TrackReplay?.setRunAnalysis?.(localAnalysis,runReceipt||portableReceipt);
689
  panel.hidden=!localAnalysis;
690
  if(!localAnalysis){$('winnerTools').hidden=true;$('evaluationTools').hidden=true;$('winnerRows').replaceChildren();$('winner_continue').replaceChildren();$('evaluationRows').replaceChildren();return;}
691
  const {summary}=localAnalysis,train=summary.mode==='train';
 
716
  const item=localAnalysis?.winnerNetworks?.[role]??comparisonPlan?.winnerNetworks?.[role];if(!item)throw Error('That winner policy is unavailable');
717
  const file=new File([item.bytes],role+'-'+item.candidate_id+'.evolution',{type:'application/octet-stream'});
718
  window.TrainingClient.useNetwork(file);selectedWinner=role;
719
+ window.TrackReplay?.focusCandidate?.(role);
720
  if($('winner_continue'))$('winner_continue').value=role;
721
  if($('run_mode'))$('run_mode').value=mode;syncRunModeUI();buttons();
722
  say(mode==='train'?'Selected '+role.replaceAll('_',' ')+' policy as the starting network for a new training run. The next Run click starts it with the selected recipe.':'Selected '+role.replaceAll('_',' ')+' policy for frozen evaluation. Its weights will stay unchanged; the next Run click starts it.');
 
752
  if(!Number.isInteger(generation)||!Number.isInteger(car)){ $('descriptor_status').textContent='Enter whole generation and car numbers.';return; }
753
  const row=localAnalysis.descriptorRows.find(item=>item.generation===generation&&item.car===car);
754
  if(!row){$('descriptor_status').textContent='No descriptor was recorded for that generation and car.';$('descriptor_tables').replaceChildren();return;}
755
+ window.TrackReplay?.focusCandidate?.('swarm');
756
  const wrap=document.createElement('div');
757
  const summary=document.createElement('p');summary.textContent='Candidate '+row.candidate_id+' · ticks '+row.ticks_elapsed+' · stats updates '+row.stats_update_count+' · weights '+row.weights_sha256;wrap.append(summary);
758
  const table=(title,entries)=>{const h=document.createElement('h3');h.textContent=title;wrap.append(h);const t=document.createElement('table'),body=document.createElement('tbody');for(const [name,value] of entries){const tr=document.createElement('tr'),a=document.createElement('th'),b=document.createElement('td');a.textContent=name;b.textContent=value===null?'Not observed':typeof value==='number'?String(value):String(value);tr.append(a,b);body.append(tr);}t.append(body);wrap.append(t);};
 
842
  if(!matched)throw Error('Winner candidate is missing from the descriptor rows');
843
  const network=await validateNetworkBytes(file.bytes);if(network.Type!==summary.conditions.vehicle_type)throw Error('Winner network vehicle type does not match analysis conditions');
844
  winnerNetworks[role]={bytes:file.bytes,sha256:file.sha256,candidate_id:winner.candidate_id,vehicle_type:summary.conditions.vehicle_type};
845
+ Object.defineProperty(winnerNetworks[role],'decoded',{value:network,enumerable:false});
846
  }else if(file)throw Error('Unavailable winner unexpectedly has a network artifact');
847
  }
848
  let evaluation=null;
 
901
  return true;
902
  }
903
  window.RunAnalysis={parseArchive:parseRunAnalysisArchive,validatePrimaryMatch:validatePrimaryAnalysisMatch,validateComparisonRequest,uniqueWinnerRoles,validateStarCheck};
904
+ function renderHistoryChart(history){
905
+ const wrap=$('historyChartWrap'),canvas=$('historyChartCanvas');
906
+ if(!wrap||!canvas||typeof canvas.getContext!=='function')return;
907
+ if(!Array.isArray(history)||history.length<1){wrap.hidden=true;return;}
908
+ wrap.hidden=false;
909
+ const ctx=canvas.getContext('2d');if(!ctx||typeof ctx.save!=='function')return;
910
+ const w=canvas.width,h=canvas.height;ctx.clearRect(0,0,w,h);
911
+ ctx.fillStyle='#09111b';ctx.fillRect(0,0,w,h);
912
+ const padL=52,padR=52,padT=24,padB=28,pw=w-padL-padR,ph=h-padT-padB;
913
+ const scores=history.flatMap(r=>[r.best_score,r.mean_score]).filter(v=>typeof v==='number'&&Number.isFinite(v));
914
+ const minS=scores.length?Math.min(...scores):0,maxS=scores.length?Math.max(minS+1e-4,Math.max(...scores)):1;
915
+ ctx.strokeStyle='#1e3044';ctx.lineWidth=1;
916
+ for(let g=0;g<=3;g++){const y=padT+(g/3)*ph;ctx.beginPath();ctx.moveTo(padL,y);ctx.lineTo(w-padR,y);ctx.stroke();}
917
+ const xAt=i=>padL+(history.length===1?pw*0.5:(i/(history.length-1))*pw);
918
+ const yScore=v=>padT+ph-((v-minS)/(maxS-minS))*ph;
919
+ // Mean fitness curve (#58a6ff)
920
+ ctx.strokeStyle='#58a6ff';ctx.lineWidth=2;ctx.beginPath();
921
+ history.forEach((r,i)=>{const x=xAt(i),y=yScore(r.mean_score||0);if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);});ctx.stroke();
922
+ // Best fitness curve (#62dfb1)
923
+ ctx.strokeStyle='#62dfb1';ctx.lineWidth=2.5;ctx.beginPath();
924
+ history.forEach((r,i)=>{const x=xAt(i),y=yScore(r.best_score||0);if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);});ctx.stroke();
925
+ // Best lap dots (#ffd073)
926
+ const laps=history.map(r=>r.best_lap_seconds).filter(v=>typeof v==='number'&&v>0);
927
+ if(laps.length){
928
+ const minL=Math.min(...laps)*0.95,maxL=Math.max(minL+0.1,Math.max(...laps)*1.05);
929
+ ctx.fillStyle='#ffd073';
930
+ history.forEach((r,i)=>{if(typeof r.best_lap_seconds==='number'&&r.best_lap_seconds>0){
931
+ const x=xAt(i),y=padT+((r.best_lap_seconds-minL)/(maxL-minL))*ph;
932
+ ctx.beginPath();ctx.arc(x,y,3.5,0,Math.PI*2);ctx.fill();
933
+ }});
934
+ }
935
+ if(typeof ctx.fillText==='function'){
936
+ ctx.fillStyle='#62dfb1';ctx.font='700 11px system-ui,sans-serif';ctx.fillText('Best Fitness',padL,15);
937
+ ctx.fillStyle='#58a6ff';ctx.fillText('Mean Fitness',padL+90,15);
938
+ ctx.fillStyle='#ffd073';ctx.fillText('● Completed Lap',padL+185,15);
939
+ ctx.fillStyle='#8fa6bc';ctx.font='600 10px ui-monospace,SFMono-Regular,monospace';
940
+ ctx.fillText(maxS.toFixed(2),6,padT+6);ctx.fillText(minS.toFixed(2),6,padT+ph);
941
+ ctx.fillText('Gen 1',padL,h-8);ctx.fillText('Gen '+history.length,w-padR-42,h-8);
942
+ }
943
+ }
944
  function showDetails(r){
945
  const metric=n=>typeof n==='number'?n.toFixed(3):'Not reported';
946
  if($('effectiveSettings'))$('effectiveSettings').textContent=JSON.stringify(r?.settings??{},null,2);
947
  if($('generationHistory'))$('generationHistory').textContent=(r?.history??[]).map(h=>
948
  'Generation '+h.generation+' · mutation '+metric(h.mutation)+' · best '+metric(h.best_score)+' · mean '+metric(h.mean_score)+' · lap '+(h.best_lap_seconds??'none')+' · cars lapped '+(h.lapped_cars??'not reported')+' · ticks '+metric(h.simulated_ticks)).join('\n');
949
+ const kpi=$('runKpiStrip');
950
+ if(kpi){
951
+ if(!r){kpi.hidden=true;renderHistoryChart(null);}
952
+ else{
953
+ kpi.hidden=false;
954
+ if($('kpi_best_score'))$('kpi_best_score').textContent=typeof r.best_score==='number'?r.best_score.toFixed(3):'—';
955
+ if($('kpi_mean_score'))$('kpi_mean_score').textContent=typeof r.mean_score==='number'?r.mean_score.toFixed(3):'—';
956
+ if($('kpi_best_lap'))$('kpi_best_lap').textContent=typeof r.best_lap_seconds==='number'?r.best_lap_seconds.toFixed(3)+' s':'No lap';
957
+ if($('kpi_lapped_cars'))$('kpi_lapped_cars').textContent=r.lapped_cars!=null?`${r.lapped_cars} / ${r.population??'—'}`:'0';
958
+ renderHistoryChart(r.history);
959
+ }
960
+ }
961
+ window.TrackReplay?.setRunAnalysis?.(localAnalysis,runReceipt||portableReceipt||(r?{report:r}:null));
962
  }
963
  $('run').onclick = async () => {
964
  const comparisonAttempt=!!comparisonPlan,comparisonRole=comparisonAttempt?comparisonPlan.roles[comparisonPlan.index]:null;
index.html CHANGED
@@ -5,6 +5,7 @@
5
  /* Workspace layout: help stays close; recipes and batch steps stay readable. */
6
  *{box-sizing:border-box}button,select,summary{min-height:44px}button,a,summary,input,select,textarea{scroll-margin-top:90px}:focus-visible{outline:3px solid #ffd073;outline-offset:3px}.lab-header{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;padding:6px 0}.lab-header h1{font-size:clamp(26px,3vw,34px);margin-top:4px}.lab-header .intro{font-size:16px;margin-top:5px}.header-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.help-link{display:inline-flex;align-items:center;min-height:44px;border:1px solid #49627c;border-radius:8px;padding:9px 13px;text-decoration:none;font-weight:650}.about{max-width:none;margin:4px 0 0;padding:0;border:0}.about>summary{display:flex;align-items:center;font-size:14px;width:fit-content;gap:8px}.about>summary::before{content:'▸'}.about[open]>summary::before{content:'▾'}.about[open]{border:1px solid #2a3d52;border-radius:10px;padding:0 16px 12px}.quick-start{border:1px solid #385568;background:#102232;border-radius:12px;margin-top:8px;padding:0 16px}.quick-start>summary{padding:12px 0;line-height:1.4}.quick-start>summary .hint{font-size:14px;font-weight:400;margin-left:12px}.quick-start[open]>summary{margin-bottom:0}.quick-steps{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:18px;list-style:none;padding:12px 0;margin:0;counter-reset:quick}.quick-steps li{counter-increment:quick;min-width:0}.quick-steps strong{display:block;margin-bottom:8px}.quick-steps strong::before{content:counter(quick) '. ';color:#62dfb1}.quick-steps p{font-size:14px;line-height:1.55;color:#b9cddd;margin:0 0 10px}.quick-footer{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:12px 0;border-top:1px solid #2a3d52}.tabs{margin:12px 0 0;gap:8px}.tabs button{border-radius:9px}.pane>.card{margin-top:16px}.section-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.section-heading h2,.section-heading h3{margin:0}.section-heading a{font-size:14px}.section-intro{max-width:85ch;margin:10px 0 18px;color:#b9cddd;line-height:1.55}.recipe-panel{border:1px solid #385568;border-radius:12px;background:#102030;padding:16px;margin:16px 0 24px}.recipe-panel .row label{margin-top:0}.recipe-actions,.batch-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:14px}.form-state{font-size:14px;font-weight:650;color:#ffd073;margin:10px 0 0;line-height:1.55}.form-state[data-state="saved"]{color:#82e6bb}.form-state[data-state="invalid"]{color:#ffb5a2}.settings-summary{font-size:14px;color:#b9cddd;line-height:1.55;margin:8px 0 0;overflow-wrap:anywhere}.recipe-panel details{margin-top:12px;padding-top:10px}.recipe-panel textarea{box-sizing:border-box;width:100%;background:#0c1725;color:#e5edf5;border:1px solid #49627c;border-radius:8px;padding:10px}.recipe-panel .manage-actions{display:flex;align-items:start;gap:12px;flex-wrap:wrap}.destructive-action{margin-top:14px;color:#ffb5a2}.batch-layout{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(280px,1fr);gap:24px;align-items:start}.batch-setup{background:#102030;border:1px solid #385568;border-radius:12px;padding:18px;position:sticky;top:92px}.batch-setup h3{margin:0 0 10px}.batch-context{margin:12px 0}.batch-context div{padding:9px 0;border-bottom:1px solid #2a3d52}.batch-context dt{font-size:13px;color:#a2b4c8}.batch-context dd{margin:3px 0 0;overflow-wrap:anywhere;font-weight:600}.sequence-heading{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-top:22px}.sequence-heading h3{margin:0}.step-count{font-size:14px;color:#b9cddd;white-space:nowrap}.batch-steps{list-style:none;counter-reset:steps;padding:0;margin:12px 0}.batch-step{border:1px solid #3b5268;border-radius:10px;padding:14px;margin-bottom:10px;background:#101d2c}.batch-step[data-invalid="true"]{border-color:#d78970}.step-header{display:grid;grid-template-columns:30px minmax(0,1fr);align-items:center;gap:10px}.step-number{color:#82e6bb;font-size:14px;font-weight:800;text-align:center}.step-summary{font-size:14px;line-height:1.5;color:#b9cddd;margin:10px 0 8px;padding-left:40px}.step-actions{display:flex;gap:8px;flex-wrap:wrap;padding-left:40px}.step-actions button{font-size:14px;padding:8px 10px}.batch-empty{border:1px dashed #49627c;border-radius:10px;padding:18px;margin:12px 0;color:#b9cddd;line-height:1.55}.batch-empty p{margin:0 0 12px}.add-step{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;align-items:end}.add-step label{margin:0}.add-step select{margin-top:6px}.batch-mode{margin:18px 0 6px;display:flex;align-items:flex-start;gap:10px;line-height:1.5}.batch-mode input{margin-top:5px;flex:none}.batch-progress{border-top:1px solid #2a3d52;margin-top:24px;padding-top:20px}.batch-progress h3{margin:0 0 12px}.batch-progress #batch_status{line-height:1.6}.batch-setup #batch_start{flex:1;min-width:140px}.batch-setup #batch_ready{line-height:1.55}.batch-actions button{min-height:46px}#recipe_feedback:empty,#batches_feedback:empty{display:none}.muted-note{font-size:14px;line-height:1.6;color:#b9cddd}.training-track{margin-top:18px}.training-track label{margin-top:0}
7
  @media(max-width:1000px){.batch-layout{grid-template-columns:minmax(0,1fr)}.batch-setup{position:static}.quick-steps{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:600px){body{padding:12px}.card{padding:16px}.row{grid-template-columns:minmax(0,1fr)}.quick-steps{grid-template-columns:minmax(0,1fr)}.header-actions{width:100%}.header-actions>*{flex:1;justify-content:center;text-align:center}.lab-header .intro{font-size:15px}.quick-start>summary .hint{display:block;margin-left:18px}.tabs{display:grid;grid-template-columns:1fr 1fr;gap:6px}.tabs button{font-size:14px;padding:9px 7px}.step-actions,.step-summary{padding-left:0}.add-step{grid-template-columns:minmax(0,1fr)}.batch-setup{padding:14px}.recipe-actions>*{flex:1}.reward{grid-template-columns:minmax(0,1fr) minmax(85px,.7fr) 65px 44px}.reward input{width:65px}.recipe-panel{padding:12px}}
 
8
  </style>
9
  <dialog id="appConfirm" aria-labelledby="appConfirmTitle" aria-describedby="appConfirmMessage">
10
  <h2 id="appConfirmTitle">Confirm action</h2>
@@ -33,7 +34,7 @@
33
  })();
34
  </script>
35
  <header class="lab-header"><div><div class="eyebrow">AI LEARNS TO DRIVE</div><h1>Training lab</h1><p class="intro">Build a driver, train it, inspect the result.</p></div><div class="header-actions"><button id="quick_start_toggle" type="button" aria-controls="firstRunGuide" aria-expanded="false">Quick start</button><a class="help-link" href="/help#start-here" target="_blank" rel="noopener">Help &amp; tutorial ↗</a></div></header>
36
- <details class="about"><summary>About &amp; feedback</summary><p>Simulator by <a href="https://github.com/Zorro909/driving_sim_rs">Zorro909</a>; interface and hosting by <a href="https://github.com/seeker-cyber-maker/driving_sim_rs">TiGa</a>. Inspired by the <a href="https://store.steampowered.com/app/3312030/AI_Learns_To_Drive/">game by ActivationFunction</a>. Built-in tracks are reconstructed replicas. For supported conditions, the lab checks the run’s final fitness policy (or frozen input) with a bounded fresh replay. It predicts conditions only and does not award or accumulate original-game stars; opponent races and other unsupported conditions remain unchecked.</p><p>Uploaded files are used for the requested run. Results are held briefly in application memory and deleted after verified download or expiry. Browser recipes contain settings only. <a href="/policy" target="_blank" rel="noopener">Read the data policy</a>.</p><p>Cars are simulated behind the scenes. The map shows track layout, not moving cars.</p><p><a id="feedbackLink" href="https://github.com/seeker-cyber-maker/driving_sim_rs/issues/new" target="_blank" rel="noopener">Report a problem</a> · Opens an editable draft for you to review.</p><p id="buildLabel" class="hint">Build: connecting…</p></details>
37
  <details id="firstRunGuide" class="quick-start"><summary id="firstRunTitle">New here? Start a first run <span class="hint">Four steps, whenever you need them</span></summary><ol class="quick-steps"><li><strong>Choose a track</strong><p>Pick a built-in replica or upload your own track.</p><button id="first_run_track" type="button">Choose a track</button></li><li><strong>Create a driver</strong><p>Choose a car and create a small starter network, or upload a network you already have.</p><button type="button" data-open-tab="drive" data-focus="starter_create">Set up a network</button></li><li><strong>Review the settings</strong><p>The starter settings are ready for a short first experiment. You can save them as a recipe later.</p><button id="first_run_settings" type="button">Review first-run settings</button></li><li><strong>Run and inspect</strong><p>Start training, then download the result to inspect your drivers. A first run may not finish a lap.</p><button type="button" data-open-tab="drive" data-focus="run">Go to run controls</button></li></ol><div class="quick-footer"><a href="/help#start-here" target="_blank" rel="noopener">Read the full step-by-step tutorial ↗</a><button id="quick_start_close" type="button">Close quick start</button></div></details>
38
  <nav class="tabs" role="tablist" aria-label="Lab sections"><button type="button" id="tab-drive" role="tab" data-tab="drive" aria-controls="drive" aria-selected="true" tabindex="0">Run &amp; results</button><button type="button" id="tab-training" role="tab" data-tab="training" aria-controls="training" aria-selected="false" tabindex="-1">Training &amp; recipes</button><button type="button" id="tab-batch" role="tab" data-tab="batch" aria-controls="batch" aria-selected="false" tabindex="-1">Batches</button><button type="button" id="tab-editor" role="tab" data-tab="editor" aria-controls="editor" aria-selected="false" tabindex="-1">Network editor</button></nav><div id="drive" class="pane" role="tabpanel" aria-labelledby="tab-drive"><div class="grid"><section class="card"><h2>Simulation</h2><div class="toolbar" aria-label="Create a starter network"><label for="starter_vehicle">Starter car</label><select id="starter_vehicle"><option value="0" selected>Formula</option><option value="1">Rally</option><option value="2">Snowmobile</option><option value="3">Truck</option></select><button id="starter_create" type="button">Create starter network</button></div><p id="starter_network_status" class="hint" role="status">Creates a small network in this page only. It does not start a run or change saved recipes.</p><label for="run_mode">Action</label><select id="run_mode"><option value="train" selected>Train a population</option><option value="evaluate">Frozen evaluation · one policy, unchanged weights</option></select><p id="run_mode_help" class="hint">Training uses the selected recipe. Frozen evaluation runs the selected policy in a fresh car without changing its weights.</p><button id="run" disabled>Run training</button><p id="starCheckNotice" class="hint">After each run, supported star challenges are checked with unchanged weights. Results are predictions, not game awards. <a href="/help#concepts" target="_blank" rel="noopener">How checks work</a>.</p><p class="hint">Select or create a network below to enable training. Review Training &amp; recipes before running.</p><p>Supports Formula, Rally, Snowmobile and Truck networks. Control outputs follow the original vehicle editor rules. Network shape and supported sensors are bounded by the original game contract.</p>
39
  <p id="computeBlock"><label for="backend">Compute</label><select id="backend" disabled><option value="cpu">CPU — no GPU allowance</option><option id="gpuOption" value="gpu" disabled>GPU — uses GPU allowance</option></select></p><p id="backendNote" class="hint">Checking available backends…</p>
@@ -47,10 +48,12 @@
47
  <p><label>Track (.track) <input id="track" type="file" accept=".track"></label></p>
48
  <p><label>Vehicle network (.evolution) <input id="network" type="file" accept=".evolution"></label></p>
49
  <p id="networkChoice" role="status">Choose an uploaded file or use a network from the browser editor.</p><p class="privacy-note">Uploads are processed for this run. By default, the server copy is deleted after the browser verifies the download.</p><details><summary>Optional repeat download</summary><label class="keep-result"><input id="keepServer" type="checkbox"> Keep this result on the server for up to five minutes so I can download it again</label><p class="privacy-note">This is optional and resets after each run. When bucket auto-save is on, verified receipt must delete the server copy first, so repeat download is disabled. All results expire after five minutes. <a href="/policy" target="_blank" rel="noopener">Data policy</a></p>
50
- </details></section><section class="card"><h2>Selected track &amp; terrain</h2><p id="mapStatus" class="map-status" role="status">Loading selected track…</p><div class="map"><canvas id="trackMap" width="1000" height="500" role="img" aria-label="Selected track tile layout and terrain"></canvas></div><div class="map-legend"><span><i class="terrain asphalt"></i> Asphalt</span><span><i class="terrain dirt"></i> Dirt</span><span><i class="terrain ice"></i> Ice</span><span><i class="terrain start"></i> Start</span></div><p id="mapLegend" class="hint"></p><h2>Run &amp; results</h2><button id="download" disabled>Download result</button> <button id="erase" disabled>Delete server-side job data now</button>
51
  <p><button id="saveLocal" disabled>Download received result (browser only)</button> <button id="forgetLocal" disabled>Discard browser result</button></p>
52
  <p>The browser keeps the received result in memory until discarded, replaced by a new run, or this page is closed. It is not saved to browser storage.</p>
53
  <h3 id="runState" role="status" aria-live="polite">READY — no run started</h3>
 
 
54
  <pre id="runSummary" style="white-space:pre-wrap;line-height:1.7" aria-live="polite"></pre>
55
  <section id="analysisPanel" hidden aria-labelledby="analysisTitle">
56
  <h2 id="analysisTitle">Winners and frozen evaluation</h2>
@@ -77,7 +80,7 @@
77
  <p id="recipe_state" class="form-state" role="status" aria-live="polite">Working settings · not saved yet</p><p id="recipe_summary" class="settings-summary"></p><div class="recipe-actions"><button id="recipe_load" type="button" disabled>Apply recipe</button><button id="recipe_save" type="button">Save settings to A</button><button type="button" data-open-tab="batch" data-focus="batch_add_recipe">Build a batch</button></div><p id="recipe_feedback" role="status" aria-live="polite"></p>
78
  <details><summary>Import, export &amp; manage recipes</summary><p class="muted-note">Recipes and batches are saved in this browser. Export a backup to move them to another browser or site. Clearing site data removes this library.</p><div class="manage-actions"><button id="recipe_export" type="button">Export recipe library</button><label for="recipe_import">Import recipe library (.json)<input id="recipe_import" type="file" accept=".json"></label><button id="recipe_bucket_nav" type="button">Bucket import / export</button></div><button id="recipe_clear" class="destructive-action" type="button">Clear saved recipes and batches on this device</button><p class="muted-note">The recipe library contains settings only. Saving a recipe does not start a run or upload tracks, networks, or results.</p></details>
79
  <details><summary>Advanced recipe JSON</summary><textarea id="recipe_draft" rows="12" aria-label="Recipe settings JSON"></textarea><button id="recipe_apply_draft" type="button">Apply JSON to form</button></details></div>
80
- <div class="section-heading"><h3>Settings for the next run</h3><button id="starter_settings_reset" type="button">Apply first-run settings to this form</button></div><p id="starter_settings_status" class="muted-note" role="status" aria-live="polite">Adjusting this form does not change a saved recipe until you save it.</p><div class="training-track"><label for="trackPresetTraining">Training track · shared with Run &amp; results</label><select id="trackPresetTraining" data-track-library disabled><option value="">Upload a custom track</option></select><p data-track-status role="status" class="hint">Loading built-in replica tracks…</p></div>
81
  <div id="training_controls"><label for="training_ticks">Simulated ticks per generation</label><input id="training_ticks" type="number" min="1" max="18000" value="900"><small>60 ticks = one simulated second. The first-run setting is 900 ticks (15 simulated seconds) per generation. Per-run limits: CPU 4,000,000 vehicle-ticks and 25 seconds; CUDA 8,000,000 vehicle-ticks and 45 seconds. Both allow up to 32,768 car-generation analysis rows.</small>
82
  <div class="row"><div><label for="population">Population</label><input id="population" type="number" min="1" max="1024" value="32" disabled><small>1–1,024 cars per generation. The first-run setting is 32.</small></div><div><label for="generations">Generations</label><input id="generations" type="number" min="1" max="2000" value="20" disabled><small>1–2,000; the first-run setting is 20.</small></div></div>
83
  <p id="work_budget" class="hint" role="status" aria-live="polite"></p>
@@ -108,7 +111,7 @@
108
  <details><summary>Advanced sequence · edit slot letters</summary><label for="batch_recipe_sequence">Recipe sequence (comma-separated slots)</label><input id="batch_recipe_sequence" type="text" value="" maxlength="300" placeholder="A, B, A" aria-describedby="batch_sequence_help batch_sequence_error"><p id="batch_sequence_help" class="muted-note">The same steps as the list above. Use saved slots A–J, separated by commas; repeated slots are allowed. Maximum 20 steps.</p></details>
109
  </div><aside class="batch-setup" aria-labelledby="batch_setup_title"><h3 id="batch_setup_title">Ready to run?</h3><dl class="batch-context"><div><dt>Track</dt><dd id="batch_context_track">Choose a track</dd></div><div><dt>Starting network</dt><dd id="batch_context_network">Choose or create a network</dd></div><div><dt>Compute</dt><dd id="batch_context_backend">CPU</dd></div></dl><button type="button" data-open-tab="drive" data-focus="trackPresetDrive">Change run setup</button><p id="batch_plan_summary" class="settings-summary">Add recipes to see the planned work.</p><p id="batch_ready" class="form-state" role="status" aria-live="polite">Save a recipe and add a step to begin.</p><div class="batch-actions"><button class="primary" id="batch_start" type="button" aria-describedby="batch_ready" disabled>Run batch</button></div><p class="muted-note">The settings shown in these steps are saved recipe values. They are fixed when you start; changing a saved recipe affects the next batch you start.</p><details><summary>Results &amp; run limits</summary><p class="muted-note">Each step uses the selected backend limit: CPU 4,000,000 or GPU 8,000,000 vehicle-ticks, and 32,768 analysis rows. Completed results are received automatically. The last candidate stays in page memory; connect a bucket in Run &amp; results to save every step. Server copies are deleted after verified receipt.</p></details></aside></div>
110
  <section class="batch-progress" aria-labelledby="batch_progress_title"><div class="section-heading"><h3 id="batch_progress_title">Batch progress</h3><button id="batch_stop" type="button" disabled>Stop after current step</button></div><p id="batch_status" role="status">No batch running.</p><div id="batch_history"></div><p id="batches_feedback" role="status" aria-live="polite"></p><details><summary>Batch receipt</summary><button id="batch_export" type="button" disabled>Export batch receipt</button></details></section></div></section>
111
- <section id="editor" class="pane" role="tabpanel" aria-labelledby="tab-editor" hidden><div class="card"><h2>Network editor</h2><p class="hint">An original editor inspired by <a href="https://the-mars-guy.github.io/AI-Learns-To-Drive-Tool/" target="_blank" rel="noopener">Mars’s network creator</a>. Create fresh networks with the selected vehicle’s game-legal controls, or use its largest original-game shape. Loaded networks keep their original vehicle, output order and sensor parameters. Unsupported files can still be inspected without changing the original.</p><div class="toolbar"><label for="editor_file">Load an existing .evolution file</label><input id="editor_file" type="file" accept=".evolution"><button id="editor_import" disabled>Inspect selected network</button><button id="editor_create" disabled>Create fresh network</button><button id="editor_apply" disabled>Use edited network</button><a id="editor_download" hidden download>Download .evolution</a></div><div class="row"><div><label for="editor_name">Network name</label><input id="editor_name" type="text" maxlength="80" value="Fresh network" disabled><label for="editor_vehicle">Vehicle type for fresh networks</label><select id="editor_vehicle" disabled><option value="0">Formula</option><option value="1">Rally</option><option value="2">Snowmobile</option><option value="3">Truck</option></select><small>This selector applies only when you create a fresh network. Imported vehicle type is retained and shown with the current network.</small><label for="editor_preset">Fresh network preset</label><select id="editor_preset" disabled><option id="editor_small_label" value="small">Small starter · 6 inputs, 4 hidden nodes, 5 outputs</option><option id="editor_maximum_label" value="maximum">Largest game network · 20 inputs, 10 × 16 hidden nodes, 5 outputs</option><option value="custom">Custom</option></select><label for="editor_shape">Input and hidden layer sizes</label><input id="editor_shape" type="text" value="6,4" disabled><small id="editor_shape_hint">Enter 1–20 inputs, followed by 0–10 hidden layers of 1–16 nodes. Output width follows the selected controls. Maximum shape: 20 → ten layers of 16 → 5 (2,869 parameters).</small><fieldset id="editor_outputs" disabled><legend id="editor_output_legend">Fresh network outputs</legend><div class="output-options"><label><input id="editor_output_0" type="checkbox" value="0" checked disabled> Steering (0)</label><label><input id="editor_output_1" type="checkbox" value="1" checked disabled> Acceleration (1)</label><label><input id="editor_output_2" type="checkbox" value="2" checked disabled> Handbrake (2)</label><label><input id="editor_output_3" type="checkbox" value="3" checked disabled> Brake (3)</label><label><input id="editor_output_4" type="checkbox" value="4" checked disabled> Boost (4)</label></div><small id="editor_output_hint">Steering and Acceleration are required. Fresh output IDs preserve this order.</small></fieldset></div><div><label for="editor_seed">Initialization seed</label><input id="editor_seed" type="number" min="0" max="2147483647" value="42105" disabled><label><input id="editor_xavier" type="checkbox" checked disabled> Seeded Xavier uniform weights (browser algorithm)</label><label><input id="editor_bias" type="checkbox" disabled> Xavier uniform biases</label><small>Unchecked: zeros. Biases default to zero.</small></div></div><p id="editor_status" role="status">Choose a file or create a fresh network. Editor changes are browser-local until you explicitly start a run.</p><p id="editor_metadata" class="hint"></p><p id="editor_legality" class="hint"></p><details id="editor_history_panel"><summary>Training history · recorded generations</summary><div class="table-scroll"><table><thead><tr><th>Record</th><th>Time</th><th>Mutation</th><th>Best score</th><th>Best lap</th></tr></thead><tbody id="editor_history"></tbody></table></div><details><summary>Full saved training metadata</summary><pre id="editor_stats"></pre></details></details><canvas aria-label="Network architecture preview" id="network_map" width="1100" height="450"></canvas><details id="weight_values"><summary>Individual weight and bias values</summary><div class="table-scroll" style="max-height:420px;overflow:auto"><table><thead><tr><th>Connection / bias</th><th>Value</th></tr></thead><tbody id="editor_parameters"></tbody></table></div></details><details><summary>Weights, biases and input contract</summary><pre id="editor_contract"></pre><label for="editor_json">NeuralNetwork JSON · editable weights and biases</label><textarea id="editor_json" rows="16" style="width:100%;box-sizing:border-box;background:#0c1725;color:#e5edf5" disabled></textarea></details></div></section><script>/* Settings-only recipe contract. Storage is injected; no network or job data. */
112
  (function(root){
113
  const KEY='tiga.ald.recipes.v1';
114
  const defaults={population:4,generations:1,ticks:60,mutation:0.01,seed:42,
@@ -815,6 +818,17 @@
815
  const target=connected&&!connected.hidden&&recipeTitle?recipeTitle:storage.querySelector('summary');
816
  target?.scrollIntoView({behavior:'smooth',block:'center'});target?.focus({preventScroll:true});
817
  });
 
 
 
 
 
 
 
 
 
 
 
818
  })();
819
  </script><script>// Browser-only editor; upload is controlled by the separate training client.
820
  (() => {
@@ -838,11 +852,76 @@
838
  }
839
  function preset(value){if(value==='small'){q('editor_shape').value='6,4';refreshOutputRules(true);}else if(value==='maximum'){q('editor_shape').value=[20,...Array(10).fill(16)].join(',');refreshOutputRules(true);}}
840
  q('editor_preset').onchange=()=>preset(q('editor_preset').value);
841
- q('editor_vehicle').onchange=()=>refreshOutputRules(q('editor_preset').value!=='custom');
842
- q('editor_shape').oninput=()=>q('editor_preset').value='custom';
 
843
  for(const id of outputIds)q(outputName(id)).onchange=()=>{q('editor_preset').value='custom';refreshOutputRules(false);};
844
  refreshOutputRules(false);
845
  window.setEditorBusy=busy=>{q('editor_apply').disabled=busy||!source;};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
846
  function graph(n){
847
  const canvas=q('network_map'),ctx=canvas.getContext('2d');ctx.clearRect(0,0,canvas.width,canvas.height);
848
  const points=n.Shape.map((count,l)=>Array.from({length:count},(_,i)=>[45+l*(canvas.width-90)/(n.Shape.length-1),35+(i+.5)*(canvas.height-70)/count]));
@@ -851,6 +930,7 @@
851
  ctx.beginPath();ctx.moveTo(...points[l][i]);ctx.lineTo(...points[l+1][j]);ctx.stroke();
852
  })));
853
  ctx.globalAlpha=1;points.flat().forEach(([x,y])=>{ctx.fillStyle='#62dfb1';ctx.beginPath();ctx.arc(x,y,4,0,2*Math.PI);ctx.fill();});
 
854
  }
855
  function show(value){
856
  const n=api.neural(value.NeuralNetwork);source=value;
@@ -881,6 +961,7 @@
881
  graph(n);window.setEditorBusy(window.TrainingClient?.isBusy?.()??false);
882
  q('editor_download').hidden=false;q('editor_download').href='#';q('editor_download').textContent='Validate and download edited .evolution';
883
  }
 
884
  function updateParameter(next,input){
885
  if(!input.value.trim()||!Number.isFinite(Number(input.value)))throw Error('Parameter value must be a finite number');
886
  const label=input.dataset.parameter,parts=label.match(/\d+/g).map(Number);
 
5
  /* Workspace layout: help stays close; recipes and batch steps stay readable. */
6
  *{box-sizing:border-box}button,select,summary{min-height:44px}button,a,summary,input,select,textarea{scroll-margin-top:90px}:focus-visible{outline:3px solid #ffd073;outline-offset:3px}.lab-header{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;padding:6px 0}.lab-header h1{font-size:clamp(26px,3vw,34px);margin-top:4px}.lab-header .intro{font-size:16px;margin-top:5px}.header-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.help-link{display:inline-flex;align-items:center;min-height:44px;border:1px solid #49627c;border-radius:8px;padding:9px 13px;text-decoration:none;font-weight:650}.about{max-width:none;margin:4px 0 0;padding:0;border:0}.about>summary{display:flex;align-items:center;font-size:14px;width:fit-content;gap:8px}.about>summary::before{content:'▸'}.about[open]>summary::before{content:'▾'}.about[open]{border:1px solid #2a3d52;border-radius:10px;padding:0 16px 12px}.quick-start{border:1px solid #385568;background:#102232;border-radius:12px;margin-top:8px;padding:0 16px}.quick-start>summary{padding:12px 0;line-height:1.4}.quick-start>summary .hint{font-size:14px;font-weight:400;margin-left:12px}.quick-start[open]>summary{margin-bottom:0}.quick-steps{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:18px;list-style:none;padding:12px 0;margin:0;counter-reset:quick}.quick-steps li{counter-increment:quick;min-width:0}.quick-steps strong{display:block;margin-bottom:8px}.quick-steps strong::before{content:counter(quick) '. ';color:#62dfb1}.quick-steps p{font-size:14px;line-height:1.55;color:#b9cddd;margin:0 0 10px}.quick-footer{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:12px 0;border-top:1px solid #2a3d52}.tabs{margin:12px 0 0;gap:8px}.tabs button{border-radius:9px}.pane>.card{margin-top:16px}.section-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.section-heading h2,.section-heading h3{margin:0}.section-heading a{font-size:14px}.section-intro{max-width:85ch;margin:10px 0 18px;color:#b9cddd;line-height:1.55}.recipe-panel{border:1px solid #385568;border-radius:12px;background:#102030;padding:16px;margin:16px 0 24px}.recipe-panel .row label{margin-top:0}.recipe-actions,.batch-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:14px}.form-state{font-size:14px;font-weight:650;color:#ffd073;margin:10px 0 0;line-height:1.55}.form-state[data-state="saved"]{color:#82e6bb}.form-state[data-state="invalid"]{color:#ffb5a2}.settings-summary{font-size:14px;color:#b9cddd;line-height:1.55;margin:8px 0 0;overflow-wrap:anywhere}.recipe-panel details{margin-top:12px;padding-top:10px}.recipe-panel textarea{box-sizing:border-box;width:100%;background:#0c1725;color:#e5edf5;border:1px solid #49627c;border-radius:8px;padding:10px}.recipe-panel .manage-actions{display:flex;align-items:start;gap:12px;flex-wrap:wrap}.destructive-action{margin-top:14px;color:#ffb5a2}.batch-layout{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(280px,1fr);gap:24px;align-items:start}.batch-setup{background:#102030;border:1px solid #385568;border-radius:12px;padding:18px;position:sticky;top:92px}.batch-setup h3{margin:0 0 10px}.batch-context{margin:12px 0}.batch-context div{padding:9px 0;border-bottom:1px solid #2a3d52}.batch-context dt{font-size:13px;color:#a2b4c8}.batch-context dd{margin:3px 0 0;overflow-wrap:anywhere;font-weight:600}.sequence-heading{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-top:22px}.sequence-heading h3{margin:0}.step-count{font-size:14px;color:#b9cddd;white-space:nowrap}.batch-steps{list-style:none;counter-reset:steps;padding:0;margin:12px 0}.batch-step{border:1px solid #3b5268;border-radius:10px;padding:14px;margin-bottom:10px;background:#101d2c}.batch-step[data-invalid="true"]{border-color:#d78970}.step-header{display:grid;grid-template-columns:30px minmax(0,1fr);align-items:center;gap:10px}.step-number{color:#82e6bb;font-size:14px;font-weight:800;text-align:center}.step-summary{font-size:14px;line-height:1.5;color:#b9cddd;margin:10px 0 8px;padding-left:40px}.step-actions{display:flex;gap:8px;flex-wrap:wrap;padding-left:40px}.step-actions button{font-size:14px;padding:8px 10px}.batch-empty{border:1px dashed #49627c;border-radius:10px;padding:18px;margin:12px 0;color:#b9cddd;line-height:1.55}.batch-empty p{margin:0 0 12px}.add-step{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;align-items:end}.add-step label{margin:0}.add-step select{margin-top:6px}.batch-mode{margin:18px 0 6px;display:flex;align-items:flex-start;gap:10px;line-height:1.5}.batch-mode input{margin-top:5px;flex:none}.batch-progress{border-top:1px solid #2a3d52;margin-top:24px;padding-top:20px}.batch-progress h3{margin:0 0 12px}.batch-progress #batch_status{line-height:1.6}.batch-setup #batch_start{flex:1;min-width:140px}.batch-setup #batch_ready{line-height:1.55}.batch-actions button{min-height:46px}#recipe_feedback:empty,#batches_feedback:empty{display:none}.muted-note{font-size:14px;line-height:1.6;color:#b9cddd}.training-track{margin-top:18px}.training-track label{margin-top:0}
7
  @media(max-width:1000px){.batch-layout{grid-template-columns:minmax(0,1fr)}.batch-setup{position:static}.quick-steps{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:600px){body{padding:12px}.card{padding:16px}.row{grid-template-columns:minmax(0,1fr)}.quick-steps{grid-template-columns:minmax(0,1fr)}.header-actions{width:100%}.header-actions>*{flex:1;justify-content:center;text-align:center}.lab-header .intro{font-size:15px}.quick-start>summary .hint{display:block;margin-left:18px}.tabs{display:grid;grid-template-columns:1fr 1fr;gap:6px}.tabs button{font-size:14px;padding:9px 7px}.step-actions,.step-summary{padding-left:0}.add-step{grid-template-columns:minmax(0,1fr)}.batch-setup{padding:14px}.recipe-actions>*{flex:1}.reward{grid-template-columns:minmax(0,1fr) minmax(85px,.7fr) 65px 44px}.reward input{width:65px}.recipe-panel{padding:12px}}
8
+ .replay-bar{background:#101d2c;border:1px solid #2a3d52;border-radius:11px;padding:10px 12px;margin:10px 0}.replay-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.replay-row button{min-height:36px;padding:6px 11px;font-size:13px;font-weight:650}.replay-row select{width:auto;min-height:36px;padding:5px 9px;font-size:13px}.replay-row input[type=range]{flex:1;min-width:120px;accent-color:#62dfb1;padding:0}.replay-time{font:600 12px ui-monospace,SFMono-Regular,monospace;color:#91efc6;white-space:nowrap}.replay-layers{display:flex;gap:6px 12px;flex-wrap:wrap;margin-top:8px;padding-top:8px;border-top:1px solid #223447;font-size:12.5px;color:#b9cddd}.replay-layers label{display:inline-flex;align-items:center;gap:5px;margin:0;cursor:pointer}.replay-layers input{margin:0}.kpi-strip{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:12px 0}.kpi-tile{background:#101d2c;border:1px solid #2a3d52;border-radius:10px;padding:10px 12px}.kpi-tile .kpi-label{font-size:11.5px;text-transform:uppercase;letter-spacing:.06em;color:#a2b4c8}.kpi-tile .kpi-val{font:800 19px ui-monospace,SFMono-Regular,monospace;color:#62dfb1;margin-top:3px}.kpi-tile .kpi-sub{font-size:11.5px;color:#8fa6bc;margin-top:2px}.chart-box{background:#09111b;border:1px solid #2a3d52;border-radius:10px;padding:10px;margin:10px 0}.chart-box canvas{max-height:190px;display:block}.subtab-bar{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.subtab-bar button{min-height:36px;padding:6px 12px;font-size:13px;border-radius:18px}.subtab-bar button[aria-pressed=true]{background:#62dfb1;color:#102522;font-weight:700}.editor-visual-grid{display:grid;grid-template-columns:minmax(260px,.85fr) minmax(0,1.35fr);gap:16px;align-items:start;margin:14px 0}@media(max-width:850px){.kpi-strip{grid-template-columns:1fr 1fr}.editor-visual-grid{grid-template-columns:minmax(0,1fr)}}
9
  </style>
10
  <dialog id="appConfirm" aria-labelledby="appConfirmTitle" aria-describedby="appConfirmMessage">
11
  <h2 id="appConfirmTitle">Confirm action</h2>
 
34
  })();
35
  </script>
36
  <header class="lab-header"><div><div class="eyebrow">AI LEARNS TO DRIVE</div><h1>Training lab</h1><p class="intro">Build a driver, train it, inspect the result.</p></div><div class="header-actions"><button id="quick_start_toggle" type="button" aria-controls="firstRunGuide" aria-expanded="false">Quick start</button><a class="help-link" href="/help#start-here" target="_blank" rel="noopener">Help &amp; tutorial ↗</a></div></header>
37
+ <details class="about"><summary>About &amp; feedback</summary><p>Simulator by <a href="https://github.com/Zorro909/driving_sim_rs">Zorro909</a>; interface and hosting by <a href="https://github.com/seeker-cyber-maker/driving_sim_rs">TiGa</a>. Inspired by the <a href="https://store.steampowered.com/app/3312030/AI_Learns_To_Drive/">game by ActivationFunction</a>. Built-in tracks are reconstructed replicas. For supported conditions, the lab checks the run’s final fitness policy (or frozen input) with a bounded fresh replay. It predicts conditions only and does not award or accumulate original-game stars; opponent races and other unsupported conditions remain unchecked.</p><p>Uploaded files are used for the requested run. Results are held briefly in application memory and deleted after verified download or expiry. Browser recipes contain settings only. <a href="/policy" target="_blank" rel="noopener">Read the data policy</a>.</p><p>Cars are simulated behind the scenes. The map shows track layout, not moving cars. Interactive telemetry replay previews are reconstructed locally in your browser with zero server compute.</p><p><a id="feedbackLink" href="https://github.com/seeker-cyber-maker/driving_sim_rs/issues/new" target="_blank" rel="noopener">Report a problem</a> · Opens an editable draft for you to review.</p><p id="buildLabel" class="hint">Build: connecting…</p></details>
38
  <details id="firstRunGuide" class="quick-start"><summary id="firstRunTitle">New here? Start a first run <span class="hint">Four steps, whenever you need them</span></summary><ol class="quick-steps"><li><strong>Choose a track</strong><p>Pick a built-in replica or upload your own track.</p><button id="first_run_track" type="button">Choose a track</button></li><li><strong>Create a driver</strong><p>Choose a car and create a small starter network, or upload a network you already have.</p><button type="button" data-open-tab="drive" data-focus="starter_create">Set up a network</button></li><li><strong>Review the settings</strong><p>The starter settings are ready for a short first experiment. You can save them as a recipe later.</p><button id="first_run_settings" type="button">Review first-run settings</button></li><li><strong>Run and inspect</strong><p>Start training, then download the result to inspect your drivers. A first run may not finish a lap.</p><button type="button" data-open-tab="drive" data-focus="run">Go to run controls</button></li></ol><div class="quick-footer"><a href="/help#start-here" target="_blank" rel="noopener">Read the full step-by-step tutorial ↗</a><button id="quick_start_close" type="button">Close quick start</button></div></details>
39
  <nav class="tabs" role="tablist" aria-label="Lab sections"><button type="button" id="tab-drive" role="tab" data-tab="drive" aria-controls="drive" aria-selected="true" tabindex="0">Run &amp; results</button><button type="button" id="tab-training" role="tab" data-tab="training" aria-controls="training" aria-selected="false" tabindex="-1">Training &amp; recipes</button><button type="button" id="tab-batch" role="tab" data-tab="batch" aria-controls="batch" aria-selected="false" tabindex="-1">Batches</button><button type="button" id="tab-editor" role="tab" data-tab="editor" aria-controls="editor" aria-selected="false" tabindex="-1">Network editor</button></nav><div id="drive" class="pane" role="tabpanel" aria-labelledby="tab-drive"><div class="grid"><section class="card"><h2>Simulation</h2><div class="toolbar" aria-label="Create a starter network"><label for="starter_vehicle">Starter car</label><select id="starter_vehicle"><option value="0" selected>Formula</option><option value="1">Rally</option><option value="2">Snowmobile</option><option value="3">Truck</option></select><button id="starter_create" type="button">Create starter network</button></div><p id="starter_network_status" class="hint" role="status">Creates a small network in this page only. It does not start a run or change saved recipes.</p><label for="run_mode">Action</label><select id="run_mode"><option value="train" selected>Train a population</option><option value="evaluate">Frozen evaluation · one policy, unchanged weights</option></select><p id="run_mode_help" class="hint">Training uses the selected recipe. Frozen evaluation runs the selected policy in a fresh car without changing its weights.</p><button id="run" disabled>Run training</button><p id="starCheckNotice" class="hint">After each run, supported star challenges are checked with unchanged weights. Results are predictions, not game awards. <a href="/help#concepts" target="_blank" rel="noopener">How checks work</a>.</p><p class="hint">Select or create a network below to enable training. Review Training &amp; recipes before running.</p><p>Supports Formula, Rally, Snowmobile and Truck networks. Control outputs follow the original vehicle editor rules. Network shape and supported sensors are bounded by the original game contract.</p>
40
  <p id="computeBlock"><label for="backend">Compute</label><select id="backend" disabled><option value="cpu">CPU — no GPU allowance</option><option id="gpuOption" value="gpu" disabled>GPU — uses GPU allowance</option></select></p><p id="backendNote" class="hint">Checking available backends…</p>
 
48
  <p><label>Track (.track) <input id="track" type="file" accept=".track"></label></p>
49
  <p><label>Vehicle network (.evolution) <input id="network" type="file" accept=".evolution"></label></p>
50
  <p id="networkChoice" role="status">Choose an uploaded file or use a network from the browser editor.</p><p class="privacy-note">Uploads are processed for this run. By default, the server copy is deleted after the browser verifies the download.</p><details><summary>Optional repeat download</summary><label class="keep-result"><input id="keepServer" type="checkbox"> Keep this result on the server for up to five minutes so I can download it again</label><p class="privacy-note">This is optional and resets after each run. When bucket auto-save is on, verified receipt must delete the server copy first, so repeat download is disabled. All results expire after five minutes. <a href="/policy" target="_blank" rel="noopener">Data policy</a></p>
51
+ </details></section><section class="card"><h2>Selected track &amp; terrain</h2><p id="mapStatus" class="map-status" role="status">Loading selected track…</p><div class="map"><canvas id="trackMap" width="1000" height="500" role="img" aria-label="Selected track tile layout and terrain"></canvas></div><div class="replay-bar" id="replayControls" aria-label="Zero-compute telemetry replay controls"><div class="replay-row"><button type="button" id="replay_play">Pause</button><button type="button" id="replay_reset">Restart</button><input type="range" id="replay_scrub" min="0" max="900" value="0" aria-label="Replay tick scrubber"><span id="replay_time_label" class="replay-time">0.00s · Tick 0 / 900</span></div><div class="replay-row" style="margin-top:8px"><label for="replay_mode" style="margin:0;font-size:12.5px">View</label><select id="replay_mode" aria-label="Replay focus mode"><option value="winners" selected>All Winners (Ghost Race)</option><option value="fitness">Fitness Leader</option><option value="fastest_lap">Fastest Lap Winner</option><option value="clean_laps">Clean Laps Winner</option><option value="swarm">Population Swarm (Gen Inspector)</option><option value="staged">Staged / Preview Policy</option></select><label for="replay_camera" style="margin:0;font-size:12.5px">Camera</label><select id="replay_camera" aria-label="Replay camera mode"><option value="full" selected>Full Circuit</option><option value="follow">Follow Car 2.2×</option></select><label for="replay_speed" style="margin:0;font-size:12.5px">Speed</label><select id="replay_speed" aria-label="Replay playback speed"><option value="0.25">0.25×</option><option value="0.5">0.5×</option><option value="1" selected>1×</option><option value="2">2×</option></select></div><div class="replay-layers"><label><input type="checkbox" id="replay_layer_outline" checked> 2D Car White Outline</label><label><input type="checkbox" id="replay_layer_rays" checked> Raycast Sensors</label><label><input type="checkbox" id="replay_layer_line" checked> Speed Heatmap Line</label><label><input type="checkbox" id="replay_layer_skids" checked> Skid Marks &amp; Curbs</label><label><input type="checkbox" id="replay_layer_hud" checked> Live Telemetry &amp; Neural HUD</label></div></div><div class="map-legend"><span><i class="terrain asphalt"></i> Asphalt</span><span><i class="terrain dirt"></i> Dirt</span><span><i class="terrain ice"></i> Ice</span><span><i class="terrain start"></i> Start</span></div><p id="mapLegend" class="hint"></p><h2>Run &amp; results</h2><button id="download" disabled>Download result</button> <button id="erase" disabled>Delete server-side job data now</button>
52
  <p><button id="saveLocal" disabled>Download received result (browser only)</button> <button id="forgetLocal" disabled>Discard browser result</button></p>
53
  <p>The browser keeps the received result in memory until discarded, replaced by a new run, or this page is closed. It is not saved to browser storage.</p>
54
  <h3 id="runState" role="status" aria-live="polite">READY — no run started</h3>
55
+ <div id="runKpiStrip" class="kpi-strip" hidden><div class="kpi-tile"><div class="kpi-label">Best Fitness</div><div id="kpi_best_score" class="kpi-val">—</div><div id="kpi_best_sub" class="kpi-sub">Final generation</div></div><div class="kpi-tile"><div class="kpi-label">Mean Fitness</div><div id="kpi_mean_score" class="kpi-val" style="color:#58a6ff">—</div><div id="kpi_mean_sub" class="kpi-sub">Population mean</div></div><div class="kpi-tile"><div class="kpi-label">Best Lap Time</div><div id="kpi_best_lap" class="kpi-val" style="color:#ffd073">—</div><div id="kpi_lap_sub" class="kpi-sub">Fastest observed</div></div><div class="kpi-tile"><div class="kpi-label">Lapped Cars</div><div id="kpi_lapped_cars" class="kpi-val" style="color:#c792ea">—</div><div id="kpi_lapped_sub" class="kpi-sub">Completed lap</div></div></div>
56
+ <div id="historyChartWrap" class="chart-box" hidden><canvas id="historyChartCanvas" width="920" height="185" role="img" aria-label="Generation fitness and lap time progression chart"></canvas></div>
57
  <pre id="runSummary" style="white-space:pre-wrap;line-height:1.7" aria-live="polite"></pre>
58
  <section id="analysisPanel" hidden aria-labelledby="analysisTitle">
59
  <h2 id="analysisTitle">Winners and frozen evaluation</h2>
 
80
  <p id="recipe_state" class="form-state" role="status" aria-live="polite">Working settings · not saved yet</p><p id="recipe_summary" class="settings-summary"></p><div class="recipe-actions"><button id="recipe_load" type="button" disabled>Apply recipe</button><button id="recipe_save" type="button">Save settings to A</button><button type="button" data-open-tab="batch" data-focus="batch_add_recipe">Build a batch</button></div><p id="recipe_feedback" role="status" aria-live="polite"></p>
81
  <details><summary>Import, export &amp; manage recipes</summary><p class="muted-note">Recipes and batches are saved in this browser. Export a backup to move them to another browser or site. Clearing site data removes this library.</p><div class="manage-actions"><button id="recipe_export" type="button">Export recipe library</button><label for="recipe_import">Import recipe library (.json)<input id="recipe_import" type="file" accept=".json"></label><button id="recipe_bucket_nav" type="button">Bucket import / export</button></div><button id="recipe_clear" class="destructive-action" type="button">Clear saved recipes and batches on this device</button><p class="muted-note">The recipe library contains settings only. Saving a recipe does not start a run or upload tracks, networks, or results.</p></details>
82
  <details><summary>Advanced recipe JSON</summary><textarea id="recipe_draft" rows="12" aria-label="Recipe settings JSON"></textarea><button id="recipe_apply_draft" type="button">Apply JSON to form</button></details></div>
83
+ <div class="section-heading"><h3>Settings for the next run</h3><button id="starter_settings_reset" type="button">Apply first-run settings to this form</button></div><p id="starter_settings_status" class="muted-note" role="status" aria-live="polite">Adjusting this form does not change a saved recipe until you save it.</p><div class="subtab-bar" id="trainingSubtabs" role="toolbar" aria-label="Filter training parameter sections"><button type="button" data-train-filter="all" aria-pressed="true">All sections</button><button type="button" data-train-filter="reward_settings" aria-pressed="false">Rewards (14 metrics)</button><button type="button" data-train-filter="native_settings" aria-pressed="false">Evolution &amp; mutation</button><button type="button" data-train-filter="run_settings" aria-pressed="false">Timing &amp; elimination</button><button type="button" data-train-filter="freeze_settings" aria-pressed="false">Selective freezing</button></div><div class="training-track"><label for="trackPresetTraining">Training track · shared with Run &amp; results</label><select id="trackPresetTraining" data-track-library disabled><option value="">Upload a custom track</option></select><p data-track-status role="status" class="hint">Loading built-in replica tracks…</p></div>
84
  <div id="training_controls"><label for="training_ticks">Simulated ticks per generation</label><input id="training_ticks" type="number" min="1" max="18000" value="900"><small>60 ticks = one simulated second. The first-run setting is 900 ticks (15 simulated seconds) per generation. Per-run limits: CPU 4,000,000 vehicle-ticks and 25 seconds; CUDA 8,000,000 vehicle-ticks and 45 seconds. Both allow up to 32,768 car-generation analysis rows.</small>
85
  <div class="row"><div><label for="population">Population</label><input id="population" type="number" min="1" max="1024" value="32" disabled><small>1–1,024 cars per generation. The first-run setting is 32.</small></div><div><label for="generations">Generations</label><input id="generations" type="number" min="1" max="2000" value="20" disabled><small>1–2,000; the first-run setting is 20.</small></div></div>
86
  <p id="work_budget" class="hint" role="status" aria-live="polite"></p>
 
111
  <details><summary>Advanced sequence · edit slot letters</summary><label for="batch_recipe_sequence">Recipe sequence (comma-separated slots)</label><input id="batch_recipe_sequence" type="text" value="" maxlength="300" placeholder="A, B, A" aria-describedby="batch_sequence_help batch_sequence_error"><p id="batch_sequence_help" class="muted-note">The same steps as the list above. Use saved slots A–J, separated by commas; repeated slots are allowed. Maximum 20 steps.</p></details>
112
  </div><aside class="batch-setup" aria-labelledby="batch_setup_title"><h3 id="batch_setup_title">Ready to run?</h3><dl class="batch-context"><div><dt>Track</dt><dd id="batch_context_track">Choose a track</dd></div><div><dt>Starting network</dt><dd id="batch_context_network">Choose or create a network</dd></div><div><dt>Compute</dt><dd id="batch_context_backend">CPU</dd></div></dl><button type="button" data-open-tab="drive" data-focus="trackPresetDrive">Change run setup</button><p id="batch_plan_summary" class="settings-summary">Add recipes to see the planned work.</p><p id="batch_ready" class="form-state" role="status" aria-live="polite">Save a recipe and add a step to begin.</p><div class="batch-actions"><button class="primary" id="batch_start" type="button" aria-describedby="batch_ready" disabled>Run batch</button></div><p class="muted-note">The settings shown in these steps are saved recipe values. They are fixed when you start; changing a saved recipe affects the next batch you start.</p><details><summary>Results &amp; run limits</summary><p class="muted-note">Each step uses the selected backend limit: CPU 4,000,000 or GPU 8,000,000 vehicle-ticks, and 32,768 analysis rows. Completed results are received automatically. The last candidate stays in page memory; connect a bucket in Run &amp; results to save every step. Server copies are deleted after verified receipt.</p></details></aside></div>
113
  <section class="batch-progress" aria-labelledby="batch_progress_title"><div class="section-heading"><h3 id="batch_progress_title">Batch progress</h3><button id="batch_stop" type="button" disabled>Stop after current step</button></div><p id="batch_status" role="status">No batch running.</p><div id="batch_history"></div><p id="batches_feedback" role="status" aria-live="polite"></p><details><summary>Batch receipt</summary><button id="batch_export" type="button" disabled>Export batch receipt</button></details></section></div></section>
114
+ <section id="editor" class="pane" role="tabpanel" aria-labelledby="tab-editor" hidden><div class="card"><h2>Network editor</h2><p class="hint">An original editor inspired by <a href="https://the-mars-guy.github.io/AI-Learns-To-Drive-Tool/" target="_blank" rel="noopener">Mars’s network creator</a>. Create fresh networks with the selected vehicle’s game-legal controls, or use its largest original-game shape. Loaded networks keep their original vehicle, output order and sensor parameters. Unsupported files can still be inspected without changing the original.</p><div class="toolbar"><label for="editor_file">Load an existing .evolution file</label><input id="editor_file" type="file" accept=".evolution"><button id="editor_import" disabled>Inspect selected network</button><button id="editor_create" disabled>Create fresh network</button><button id="editor_apply" disabled>Use edited network</button><a id="editor_download" hidden download>Download .evolution</a></div><div class="row"><div><label for="editor_name">Network name</label><input id="editor_name" type="text" maxlength="80" value="Fresh network" disabled><label for="editor_vehicle">Vehicle type for fresh networks</label><select id="editor_vehicle" disabled><option value="0">Formula</option><option value="1">Rally</option><option value="2">Snowmobile</option><option value="3">Truck</option></select><small>This selector applies only when you create a fresh network. Imported vehicle type is retained and shown with the current network.</small><label for="editor_preset">Fresh network preset</label><select id="editor_preset" disabled><option id="editor_small_label" value="small">Small starter · 6 inputs, 4 hidden nodes, 5 outputs</option><option id="editor_maximum_label" value="maximum">Largest game network · 20 inputs, 10 × 16 hidden nodes, 5 outputs</option><option value="custom">Custom</option></select><label for="editor_shape">Input and hidden layer sizes</label><input id="editor_shape" type="text" value="6,4" disabled><small id="editor_shape_hint">Enter 1–20 inputs, followed by 0–10 hidden layers of 1–16 nodes. Output width follows the selected controls. Maximum shape: 20 → ten layers of 16 → 5 (2,869 parameters).</small><fieldset id="editor_outputs" disabled><legend id="editor_output_legend">Fresh network outputs</legend><div class="output-options"><label><input id="editor_output_0" type="checkbox" value="0" checked disabled> Steering (0)</label><label><input id="editor_output_1" type="checkbox" value="1" checked disabled> Acceleration (1)</label><label><input id="editor_output_2" type="checkbox" value="2" checked disabled> Handbrake (2)</label><label><input id="editor_output_3" type="checkbox" value="3" checked disabled> Brake (3)</label><label><input id="editor_output_4" type="checkbox" value="4" checked disabled> Boost (4)</label></div><small id="editor_output_hint">Steering and Acceleration are required. Fresh output IDs preserve this order.</small></fieldset></div><div><label for="editor_seed">Initialization seed</label><input id="editor_seed" type="number" min="0" max="2147483647" value="42105" disabled><label><input id="editor_xavier" type="checkbox" checked disabled> Seeded Xavier uniform weights (browser algorithm)</label><label><input id="editor_bias" type="checkbox" disabled> Xavier uniform biases</label><small>Unchecked: zeros. Biases default to zero.</small><label><input id="show_native_outline" type="checkbox" checked> Show 2D vehicle white outline &amp; raycast sensor cone</label></div></div><p id="editor_status" role="status">Choose a file or create a fresh network. Editor changes are browser-local until you explicitly start a run.</p><p id="editor_metadata" class="hint"></p><p id="editor_legality" class="hint"></p><details id="editor_history_panel"><summary>Training history · recorded generations</summary><div class="table-scroll"><table><thead><tr><th>Record</th><th>Time</th><th>Mutation</th><th>Best score</th><th>Best lap</th></tr></thead><tbody id="editor_history"></tbody></table></div><details><summary>Full saved training metadata</summary><pre id="editor_stats"></pre></details></details><div class="editor-visual-grid"><canvas aria-label="2D vehicle outline and raycast sensor fan preview" id="sensor_fan_map" width="420" height="450"></canvas><canvas aria-label="Network architecture preview" id="network_map" width="1100" height="450"></canvas></div><details id="weight_values"><summary>Individual weight and bias values</summary><div class="table-scroll" style="max-height:420px;overflow:auto"><table><thead><tr><th>Connection / bias</th><th>Value</th></tr></thead><tbody id="editor_parameters"></tbody></table></div></details><details><summary>Weights, biases and input contract</summary><pre id="editor_contract"></pre><label for="editor_json">NeuralNetwork JSON · editable weights and biases</label><textarea id="editor_json" rows="16" style="width:100%;box-sizing:border-box;background:#0c1725;color:#e5edf5" disabled></textarea></details></div></section><script>/* Settings-only recipe contract. Storage is injected; no network or job data. */
115
  (function(root){
116
  const KEY='tiga.ald.recipes.v1';
117
  const defaults={population:4,generations:1,ticks:60,mutation:0.01,seed:42,
 
818
  const target=connected&&!connected.hidden&&recipeTitle?recipeTitle:storage.querySelector('summary');
819
  target?.scrollIntoView({behavior:'smooth',block:'center'});target?.focus({preventScroll:true});
820
  });
821
+ const filterBtns=[...document.querySelectorAll('#trainingSubtabs [data-train-filter]')];
822
+ const trainDetails=['reward_settings','native_settings','run_settings','freeze_settings'];
823
+ filterBtns.forEach(btn=>btn.addEventListener('click',()=>{
824
+ const mode=btn.dataset.trainFilter;
825
+ filterBtns.forEach(b=>b.setAttribute('aria-pressed',String(b===btn)));
826
+ trainDetails.forEach(id=>{
827
+ const el=document.getElementById(id);if(!el)return;
828
+ if(mode==='all'){el.hidden=false;}
829
+ else{el.hidden=id!==mode;if(id===mode)el.open=true;}
830
+ });
831
+ }));
832
  })();
833
  </script><script>// Browser-only editor; upload is controlled by the separate training client.
834
  (() => {
 
852
  }
853
  function preset(value){if(value==='small'){q('editor_shape').value='6,4';refreshOutputRules(true);}else if(value==='maximum'){q('editor_shape').value=[20,...Array(10).fill(16)].join(',');refreshOutputRules(true);}}
854
  q('editor_preset').onchange=()=>preset(q('editor_preset').value);
855
+ q('editor_vehicle').onchange=()=>{refreshOutputRules(q('editor_preset').value!=='custom');renderSensorFan(source);};
856
+ q('editor_shape').oninput=()=>{q('editor_preset').value='custom';renderSensorFan(source);};
857
+ if(q('show_native_outline'))q('show_native_outline').onchange=()=>renderSensorFan(source);
858
  for(const id of outputIds)q(outputName(id)).onchange=()=>{q('editor_preset').value='custom';refreshOutputRules(false);};
859
  refreshOutputRules(false);
860
  window.setEditorBusy=busy=>{q('editor_apply').disabled=busy||!source;};
861
+ function renderSensorFan(netValue){
862
+ const c=q('sensor_fan_map');if(!c||typeof c.getContext!=='function')return;
863
+ const ctx=c.getContext('2d');if(!ctx||typeof ctx.save!=='function')return;
864
+ const w=c.width,h=c.height;ctx.clearRect(0,0,w,h);
865
+ ctx.fillStyle='#09111b';ctx.fillRect(0,0,w,h);
866
+ const vType=Number.isInteger(netValue?.Type)?netValue.Type:freshType();
867
+ const showOutline=q('show_native_outline')?q('show_native_outline').checked:true;
868
+ const cx=w*0.5,cy=h*0.72;
869
+ let sensors=netValue?.Sensors;
870
+ if(!Array.isArray(sensors)){
871
+ const inCount=Math.max(1,Math.min(20,Number((q('editor_shape')?.value||'6').split(',')[0])||6));
872
+ const rays=Math.min(13,Math.max(1,inCount-1));
873
+ sensors=Array.from({length:rays},(_,i)=>({$type:'raycast',Degrees:rays===1?0:-90+180*i/(rays-1),Length:800}));
874
+ }
875
+ // Corridor walls + range rings
876
+ ctx.save();
877
+ ctx.strokeStyle='rgba(73,98,124,0.25)';ctx.lineWidth=1;
878
+ for(const r of [90,170,250]){ctx.beginPath();ctx.arc(cx,cy,r,Math.PI*1.05,Math.PI*1.95);ctx.stroke();}
879
+ ctx.strokeStyle='#cbd7d9';ctx.lineWidth=3;
880
+ ctx.beginPath();ctx.moveTo(cx-135,30);ctx.lineTo(cx-135,h-20);ctx.moveTo(cx+135,30);ctx.lineTo(cx+135,h-20);ctx.stroke();
881
+ // Raycast beams (facing upward -PI/2)
882
+ for(const s of sensors){
883
+ if(s?.$type!=='raycast')continue;
884
+ const rad=-Math.PI/2+(s.Degrees||0)*Math.PI/180;
885
+ const maxLen=Math.min(265,Math.max(50,(s.Length||800)*0.28));
886
+ const dx=Math.cos(rad),dy=Math.sin(rad);
887
+ let dist=maxLen;
888
+ if(Math.abs(dx)>1e-4){
889
+ const wallX=dx>0?cx+135:cx-135;
890
+ const t=(wallX-cx)/dx;
891
+ if(t>0&&t<dist)dist=t;
892
+ }
893
+ const hx=cx+dx*dist,hy=cy+dy*dist;
894
+ ctx.strokeStyle=dist<maxLen*0.95?'#ffd073':'#62dfb1';
895
+ ctx.globalAlpha=0.65;ctx.lineWidth=1.8;
896
+ ctx.beginPath();ctx.moveTo(cx,cy);ctx.lineTo(hx,hy);ctx.stroke();
897
+ ctx.globalAlpha=1;ctx.fillStyle=ctx.strokeStyle;
898
+ ctx.beginPath();ctx.arc(hx,hy,3.2,0,Math.PI*2);ctx.fill();
899
+ }
900
+ // 2D Vehicle Silhouette with white shader outline
901
+ ctx.translate(cx,cy);ctx.rotate(-Math.PI/2);ctx.scale(2.1,2.1);
902
+ ctx.fillStyle='#101820';ctx.strokeStyle=showOutline?'#ffffff':'#2a3d52';ctx.lineWidth=showOutline?1.2:0.7;
903
+ if(vType===2){
904
+ for(const wy of [-5.2,5.2]){ctx.beginPath();ctx.roundRect(4,-1.3+wy,9,2.6,1);ctx.fill();ctx.stroke();}
905
+ }else{
906
+ for(const [wx,wy] of [[-10.5,-6.2],[-10.5,6.2],[5.2,-6.2],[5.2,6.2]]){ctx.beginPath();ctx.roundRect(wx,wy-1.7,6.4,3.4,1);ctx.fill();ctx.stroke();}
907
+ }
908
+ ctx.fillStyle='#62dfb1';ctx.strokeStyle=showOutline?'#ffffff':'#0c1926';ctx.lineWidth=showOutline?2.2:1.1;
909
+ ctx.beginPath();
910
+ if(vType===0){
911
+ ctx.moveTo(-13.5,-6.2);ctx.lineTo(-9.5,-6.2);ctx.lineTo(-9.5,-3.6);ctx.lineTo(-6.5,-5.5);ctx.lineTo(3.5,-4.8);ctx.lineTo(8.5,-2.0);
912
+ ctx.lineTo(13.5,-5.8);ctx.lineTo(15.2,-5.8);ctx.lineTo(15.2,5.8);ctx.lineTo(13.5,5.8);ctx.lineTo(8.5,2.0);ctx.lineTo(3.5,4.8);
913
+ ctx.lineTo(-6.5,5.5);ctx.lineTo(-9.5,3.6);ctx.lineTo(-9.5,6.2);ctx.lineTo(-13.5,6.2);ctx.closePath();
914
+ }else if(vType===1){ctx.roundRect(-12.5,-6.3,25,12.6,3.4);}
915
+ else if(vType===2){ctx.moveTo(-11.5,-4.6);ctx.lineTo(5.5,-5.0);ctx.lineTo(13.5,0);ctx.lineTo(5.5,5.0);ctx.lineTo(-11.5,4.6);ctx.closePath();}
916
+ else{ctx.roundRect(-14,-7.0,28,14.0,2.4);}
917
+ ctx.fill();ctx.stroke();
918
+ ctx.fillStyle='#0c1725';ctx.beginPath();ctx.roundRect(-4.5,-3.6,8.5,7.2,2);ctx.fill();
919
+ ctx.restore();
920
+ if(typeof ctx.fillText==='function'){
921
+ ctx.fillStyle='#c6d9e9';ctx.font='700 12px system-ui,sans-serif';
922
+ ctx.fillText((api.vehicleNames[vType]||'Vehicle')+' · 2D Outline & Sensor Cone',14,22);
923
+ }
924
+ }
925
  function graph(n){
926
  const canvas=q('network_map'),ctx=canvas.getContext('2d');ctx.clearRect(0,0,canvas.width,canvas.height);
927
  const points=n.Shape.map((count,l)=>Array.from({length:count},(_,i)=>[45+l*(canvas.width-90)/(n.Shape.length-1),35+(i+.5)*(canvas.height-70)/count]));
 
930
  ctx.beginPath();ctx.moveTo(...points[l][i]);ctx.lineTo(...points[l+1][j]);ctx.stroke();
931
  })));
932
  ctx.globalAlpha=1;points.flat().forEach(([x,y])=>{ctx.fillStyle='#62dfb1';ctx.beginPath();ctx.arc(x,y,4,0,2*Math.PI);ctx.fill();});
933
+ renderSensorFan(source);
934
  }
935
  function show(value){
936
  const n=api.neural(value.NeuralNetwork);source=value;
 
961
  graph(n);window.setEditorBusy(window.TrainingClient?.isBusy?.()??false);
962
  q('editor_download').hidden=false;q('editor_download').href='#';q('editor_download').textContent='Validate and download edited .evolution';
963
  }
964
+ renderSensorFan(null);
965
  function updateParameter(next,input){
966
  if(!input.value.trim()||!Number.isFinite(Number(input.value)))throw Error('Parameter value must be a finite number');
967
  const label=input.dataset.parameter,parts=label.match(/\d+/g).map(Number);
track-map.js CHANGED
@@ -1,10 +1,11 @@
1
- /* Browser-only preview of the selected saved track. No upload for preview. */
2
  (function(root,factory){
3
  if(typeof module==='object'&&module.exports)module.exports=factory();
4
  else root.TrackMap=factory();
5
  })(typeof window==='undefined'?this:window,function(){
6
  const COLOURS=['#607e96','#b89561','#b9d9ed'];
7
  const NAMES=['Asphalt','Dirt','Ice'];
 
8
  const VECTOR=/^Vector2i\((-?\d+),\s*(-?\d+)\)$/;
9
  function vector(value){
10
  const match=typeof value==='string'&&VECTOR.exec(value);
@@ -87,6 +88,139 @@
87
  if(maxX-minX>30||maxY-minY>30)throw Error('Track preview is too large');
88
  return {tiles,start,startConnection,bounds:{minX,maxX,minY,maxY}};
89
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  function render(canvas,track){
91
  const context=canvas.getContext('2d');
92
  if(!context)throw Error('Canvas preview unavailable');
@@ -96,6 +230,18 @@
96
  const b=track.bounds,columns=b.maxX-b.minX+1,rows=b.maxY-b.minY+1;
97
  const size=Math.min(160,(w-70)/columns,(h-70)/rows);
98
  const left=(w-columns*size)/2,top=(h-rows*size)/2;
 
 
 
 
 
 
 
 
 
 
 
 
99
  for(const tile of track.tiles){
100
  const x=left+(tile.x-b.minX)*size,y=top+(tile.y-b.minY)*size;
101
  context.fillStyle=COLOURS[tile.surface];
@@ -106,28 +252,885 @@
106
  const [entry,exit]=tile.connections;
107
  const a=connectionPoint(entry,x,y,size),z=connectionPoint(exit,x,y,size);
108
  const p1=connectionControl(entry,exit,a,z),p2=connectionControl(exit,entry,z,a);
 
 
 
 
 
 
 
109
  context.beginPath();context.moveTo(...a);
110
  context.bezierCurveTo(...p1,...p2,...z);
111
  context.strokeStyle='#f3a967';context.lineWidth=Math.max(3,size*.055);
112
  context.lineCap='round';context.stroke();
113
  }
114
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  const startX=left+(track.start[0]-b.minX)*size,startY=top+(track.start[1]-b.minY)*size;
116
  const [sx,sy]=connectionPoint(track.startConnection,startX,startY,size);
117
  context.beginPath();context.arc(sx,sy,Math.max(6,size*.105),0,Math.PI*2);
118
  context.fillStyle='#66e5aa';context.fill();context.strokeStyle='#0c1926';context.lineWidth=3;context.stroke();
119
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  function mount({document,fetch,picker}){
121
  const canvas=document.getElementById('trackMap');
122
  const status=document.getElementById('mapStatus');
123
  const legend=document.getElementById('mapLegend');
124
  if(!canvas||!status||!picker)return;
125
  let version=0,atlasPromise=null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  async function refresh(){
127
  const current=++version;
 
128
  const info=picker.info();
129
  const source=info.source==='builtin'?info.display_name:document.getElementById('track')?.files?.[0]?.name;
130
- if(!source){status.textContent='Select a built-in track or choose a custom .track file.';legend.textContent='';renderEmpty();return;}
 
 
 
131
  status.textContent='Loading '+source+'…';
132
  try{
133
  atlasPromise??=fetch('/assets/track-atlas.json',{cache:'no-store',credentials:'same-origin'}).then(r=>{
@@ -136,21 +1139,60 @@
136
  const [file,atlas]=await Promise.all([picker.file(),atlasPromise]);
137
  const track=model(await decode(file),atlas);
138
  if(current!==version)return;
 
 
139
  render(canvas,track);
 
 
 
 
140
  const counts=[0,0,0];track.tiles.forEach(tile=>counts[tile.surface]++);
141
  status.textContent=source+' · '+track.tiles.length+' pieces · green dot marks the saved starting point';
142
- legend.textContent=NAMES.filter((_,i)=>counts[i]).map((name,index)=>{
143
  const i=NAMES.indexOf(name);return name+' '+counts[i];
144
- }).join(' · ')+' · Orange line follows the track centerline; collision shapes and car positions are not shown.';
145
  }catch(error){
146
  if(current!==version)return;
 
147
  renderEmpty();status.textContent='Map unavailable: '+error.message;legend.textContent='Training selection is unaffected.';
148
  }
149
  }
150
  function renderEmpty(){const ctx=canvas.getContext('2d');ctx.clearRect(0,0,canvas.width,canvas.height);ctx.fillStyle='#0c1926';ctx.fillRect(0,0,canvas.width,canvas.height);}
151
  document.addEventListener('track-library-change',refresh);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  refresh();
153
- return {refresh};
154
  }
155
  return {mount,model,decode,render};
156
  });
 
1
+ /* Browser-only preview and zero-compute telemetry replay of the selected saved track. No upload for preview. */
2
  (function(root,factory){
3
  if(typeof module==='object'&&module.exports)module.exports=factory();
4
  else root.TrackMap=factory();
5
  })(typeof window==='undefined'?this:window,function(){
6
  const COLOURS=['#607e96','#b89561','#b9d9ed'];
7
  const NAMES=['Asphalt','Dirt','Ice'];
8
+ const VEHICLE_NAMES=['Formula','Rally','Snowmobile','Truck'];
9
  const VECTOR=/^Vector2i\((-?\d+),\s*(-?\d+)\)$/;
10
  function vector(value){
11
  const match=typeof value==='string'&&VECTOR.exec(value);
 
88
  if(maxX-minX>30||maxY-minY>30)throw Error('Track preview is too large');
89
  return {tiles,start,startConnection,bounds:{minX,maxX,minY,maxY}};
90
  }
91
+
92
+ function cubicPoint(a,p1,p2,z,t){
93
+ const u=1-t,uu=u*u,tt=t*t;
94
+ return [
95
+ uu*u*a[0]+3*uu*t*p1[0]+3*u*tt*p2[0]+tt*t*z[0],
96
+ uu*u*a[1]+3*uu*t*p1[1]+3*u*tt*p2[1]+tt*t*z[1]
97
+ ];
98
+ }
99
+ function cubicTangent(a,p1,p2,z,t){
100
+ const u=1-t;
101
+ const dx=3*u*u*(p1[0]-a[0])+6*u*t*(p2[0]-p1[0])+3*t*t*(z[0]-p2[0]);
102
+ const dy=3*u*u*(p1[1]-a[1])+6*u*t*(p2[1]-p1[1])+3*t*t*(z[1]-p2[1]);
103
+ const len=Math.hypot(dx,dy)||1;
104
+ return [dx/len,dy/len];
105
+ }
106
+ function oppositeSide(side){return [1,0,3,2][side];}
107
+ function neighborDelta(side){return [[0,-1],[0,1],[-1,0],[1,0]][side];}
108
+
109
+ function buildTrackGeometry(track,w,h){
110
+ const b=track.bounds,columns=b.maxX-b.minX+1,rows=b.maxY-b.minY+1;
111
+ const size=Math.min(160,(w-70)/columns,(h-70)/rows);
112
+ const left=(w-columns*size)/2,top=(h-rows*size)/2;
113
+ const roadHalfWidth=Math.max(7,size*0.215);
114
+ const tileMap=new Map(track.tiles.map(t=>[`${t.x},${t.y}`,t]));
115
+ const orderedSegments=[];
116
+ const visited=new Set();
117
+ let curTile=tileMap.get(`${track.start[0]},${track.start[1]}`);
118
+ let entryConn=track.startConnection;
119
+ while(curTile&&orderedSegments.length<track.tiles.length){
120
+ const key=`${curTile.x},${curTile.y}`;
121
+ if(visited.has(key))break;
122
+ visited.add(key);
123
+ const [c0,c1]=curTile.connections;
124
+ const isFirst=c0.side===entryConn.side&&c0.index===entryConn.index;
125
+ const enter=isFirst?c0:c1;
126
+ const exit=isFirst?c1:c0;
127
+ orderedSegments.push({tile:curTile,enter,exit});
128
+ const [dx,dy]=neighborDelta(exit.side);
129
+ const nextTile=tileMap.get(`${curTile.x+dx},${curTile.y+dy}`);
130
+ if(!nextTile)break;
131
+ const nextSide=oppositeSide(exit.side);
132
+ const match=nextTile.connections.find(c=>c.side===nextSide&&c.index===exit.index);
133
+ if(!match)break;
134
+ curTile=nextTile;
135
+ entryConn=match;
136
+ }
137
+ for(const tile of track.tiles){
138
+ const key=`${tile.x},${tile.y}`;
139
+ if(!visited.has(key)&&tile.connections.length>=2){
140
+ orderedSegments.push({tile,enter:tile.connections[0],exit:tile.connections[1]});
141
+ }
142
+ }
143
+ const rawSamples=[];
144
+ const stepsPerTile=26;
145
+ for(let sIdx=0;sIdx<orderedSegments.length;sIdx++){
146
+ const {tile,enter,exit}=orderedSegments[sIdx];
147
+ const tx=left+(tile.x-b.minX)*size,ty=top+(tile.y-b.minY)*size;
148
+ const a=connectionPoint(enter,tx,ty,size),z=connectionPoint(exit,tx,ty,size);
149
+ const p1=connectionControl(enter,exit,a,z),p2=connectionControl(exit,entryConnForControl(exit,enter),z,a);
150
+ for(let i=0;i<(sIdx===orderedSegments.length-1?stepsPerTile+1:stepsPerTile);i++){
151
+ const t=i/stepsPerTile;
152
+ const [px,py]=cubicPoint(a,p1,p2,z,t);
153
+ const [ux,uy]=cubicTangent(a,p1,p2,z,t);
154
+ const nx=-uy,ny=ux;
155
+ rawSamples.push({x:px,y:py,tx:ux,ty:uy,nx,ny,surface:tile.surface,tileIndex:sIdx});
156
+ }
157
+ }
158
+ function entryConnForControl(_,enter){return enter;}
159
+ if(rawSamples.length<2){
160
+ const sx=left+(track.start[0]-b.minX)*size+size*0.5,sy=top+(track.start[1]-b.minY)*size+size*0.5;
161
+ rawSamples.push({x:sx,y:sy,tx:1,ty:0,nx:0,ny:1,surface:0,tileIndex:0},{x:sx+10,y:sy,tx:1,ty:0,nx:0,ny:1,surface:0,tileIndex:0});
162
+ }
163
+ let totalLength=0;
164
+ rawSamples[0].s=0;
165
+ for(let i=1;i<rawSamples.length;i++){
166
+ const ds=Math.hypot(rawSamples[i].x-rawSamples[i-1].x,rawSamples[i].y-rawSamples[i-1].y);
167
+ totalLength+=ds;
168
+ rawSamples[i].s=totalLength;
169
+ }
170
+ totalLength=Math.max(1,totalLength);
171
+ const n=rawSamples.length;
172
+ const closed=Math.hypot(rawSamples[n-1].x-rawSamples[0].x,rawSamples[n-1].y-rawSamples[0].y)<size*0.45;
173
+ for(let i=0;i<n;i++){
174
+ const prev=rawSamples[closed?(i-2+n)%n:Math.max(0,i-2)];
175
+ const next=rawSamples[closed?(i+2)%n:Math.min(n-1,i+2)];
176
+ const a1=Math.atan2(prev.ty,prev.tx),a2=Math.atan2(next.ty,next.tx);
177
+ let da=a2-a1;
178
+ while(da>Math.PI)da-=Math.PI*2;
179
+ while(da<-Math.PI)da+=Math.PI*2;
180
+ const ds=Math.max(2,Math.hypot(next.x-prev.x,next.y-prev.y));
181
+ const p=rawSamples[i];
182
+ p.curvature=da/ds;
183
+ p.heading=Math.atan2(p.ty,p.tx);
184
+ p.u=p.s/totalLength;
185
+ p.lx=p.x+p.nx*roadHalfWidth;
186
+ p.ly=p.y+p.ny*roadHalfWidth;
187
+ p.rx=p.x-p.nx*roadHalfWidth;
188
+ p.ry=p.y-p.ny*roadHalfWidth;
189
+ }
190
+ return {size,left,top,columns,rows,roadHalfWidth,samples:rawSamples,totalLength,closed};
191
+ }
192
+
193
+ function sampleAtProgress(geom,u){
194
+ const samples=geom.samples,n=samples.length;
195
+ if(!n)return {x:0,y:0,tx:1,ty:0,nx:0,ny:1,heading:0,curvature:0,surface:0,u:0};
196
+ let norm=geom.closed?((u%1)+1)%1:Math.max(0,Math.min(1,u));
197
+ const targetS=norm*geom.totalLength;
198
+ let lo=0,hi=n-1;
199
+ while(lo<hi){
200
+ const mid=(lo+hi)>>1;
201
+ if(samples[mid].s<targetS)lo=mid+1;
202
+ else hi=mid;
203
+ }
204
+ const i1=Math.min(n-1,lo),i0=Math.max(0,i1-1);
205
+ const s0=samples[i0],s1=samples[i1];
206
+ const span=Math.max(1e-5,s1.s-s0.s);
207
+ const t=Math.max(0,Math.min(1,(targetS-s0.s)/span));
208
+ const tx=s0.tx+(s1.tx-s0.tx)*t,ty=s0.ty+(s1.ty-s0.ty)*t;
209
+ const tLen=Math.hypot(tx,ty)||1;
210
+ const ux=tx/tLen,uy=ty/tLen;
211
+ return {
212
+ x:s0.x+(s1.x-s0.x)*t,
213
+ y:s0.y+(s1.y-s0.y)*t,
214
+ tx:ux,ty:uy,
215
+ nx:-uy,ny:ux,
216
+ heading:Math.atan2(uy,ux),
217
+ curvature:s0.curvature+(s1.curvature-s0.curvature)*t,
218
+ surface:t<0.5?s0.surface:s1.surface,
219
+ u:norm,
220
+ idx:i0
221
+ };
222
+ }
223
+
224
  function render(canvas,track){
225
  const context=canvas.getContext('2d');
226
  if(!context)throw Error('Canvas preview unavailable');
 
230
  const b=track.bounds,columns=b.maxX-b.minX+1,rows=b.maxY-b.minY+1;
231
  const size=Math.min(160,(w-70)/columns,(h-70)/rows);
232
  const left=(w-columns*size)/2,top=(h-rows*size)/2;
233
+
234
+ // Subtle CAD coordinate grid in background
235
+ if(typeof context.save==='function'){
236
+ context.save();
237
+ context.strokeStyle='rgba(42,61,82,0.25)';
238
+ context.lineWidth=1;
239
+ const step=Math.max(24,Math.round(size*0.5));
240
+ for(let gx=left%step;gx<w;gx+=step){context.beginPath();context.moveTo(gx,0);context.lineTo(gx,h);context.stroke();}
241
+ for(let gy=top%step;gy<h;gy+=step){context.beginPath();context.moveTo(0,gy);context.lineTo(w,gy);context.stroke();}
242
+ context.restore();
243
+ }
244
+
245
  for(const tile of track.tiles){
246
  const x=left+(tile.x-b.minX)*size,y=top+(tile.y-b.minY)*size;
247
  context.fillStyle=COLOURS[tile.surface];
 
252
  const [entry,exit]=tile.connections;
253
  const a=connectionPoint(entry,x,y,size),z=connectionPoint(exit,x,y,size);
254
  const p1=connectionControl(entry,exit,a,z),p2=connectionControl(exit,entry,z,a);
255
+ // Road bed & wall corridor
256
+ context.beginPath();context.moveTo(...a);context.bezierCurveTo(...p1,...p2,...z);
257
+ context.strokeStyle='#cbd7d9';context.lineWidth=Math.max(8,size*.46);context.lineCap='round';context.stroke();
258
+ context.beginPath();context.moveTo(...a);context.bezierCurveTo(...p1,...p2,...z);
259
+ context.strokeStyle=tile.surface===2?'#223c52':tile.surface===1?'#2e2720':'#162434';
260
+ context.lineWidth=Math.max(6,size*.40);context.lineCap='round';context.stroke();
261
+ // Original centerline
262
  context.beginPath();context.moveTo(...a);
263
  context.bezierCurveTo(...p1,...p2,...z);
264
  context.strokeStyle='#f3a967';context.lineWidth=Math.max(3,size*.055);
265
  context.lineCap='round';context.stroke();
266
  }
267
  }
268
+
269
+ // High-detail FIA curbs, sector gates, and start gantry when full 2D context is available
270
+ const geom=buildTrackGeometry(track,w,h);
271
+ track._geometry=geom;
272
+ if(typeof context.save==='function'&&geom.samples.length>8){
273
+ context.save();
274
+ const hw=geom.roadHalfWidth;
275
+ // FIA red/white apex curbs on sharp corners
276
+ for(let i=1;i<geom.samples.length-1;i+=2){
277
+ const s0=geom.samples[i-1],s1=geom.samples[i+1];
278
+ if(Math.abs(s0.curvature)*size>0.65){
279
+ const side=s0.curvature>0?-1:1;
280
+ const ox0=s0.x+s0.nx*side*(hw*0.92),oy0=s0.y+s0.ny*side*(hw*0.92);
281
+ const ox1=s1.x+s1.nx*side*(hw*0.92),oy1=s1.y+s1.ny*side*(hw*0.92);
282
+ context.beginPath();context.moveTo(ox0,oy0);context.lineTo(ox1,oy1);
283
+ context.strokeStyle=((i>>1)&1)?'#e05252':'#f0f6fc';
284
+ context.lineWidth=Math.max(2,size*0.045);context.lineCap='butt';context.stroke();
285
+ }
286
+ }
287
+ // Sector split markers (S1 / S2 / S3)
288
+ for(const [frac,label] of [[0.333,'S2'],[0.667,'S3']]){
289
+ const sp=sampleAtProgress(geom,frac);
290
+ context.beginPath();
291
+ context.moveTo(sp.x+sp.nx*hw*0.95,sp.y+sp.ny*hw*0.95);
292
+ context.lineTo(sp.x-sp.nx*hw*0.95,sp.y-sp.ny*hw*0.95);
293
+ context.strokeStyle='rgba(88,166,255,0.55)';context.lineWidth=1.5;context.stroke();
294
+ if(typeof context.fillText==='function'){
295
+ context.fillStyle='#8ec2ff';context.font='700 10px system-ui,sans-serif';
296
+ context.fillText(label,sp.x+sp.nx*(hw+8)-6,sp.y+sp.ny*(hw+8)+3);
297
+ }
298
+ }
299
+ // Checkered Start/Finish line at u=0
300
+ const st=geom.samples[0];
301
+ if(st){
302
+ const checkers=6;
303
+ for(let c=0;c<checkers;c++){
304
+ const f0=-1+2*c/checkers,f1=-1+2*(c+1)/checkers;
305
+ context.beginPath();
306
+ context.moveTo(st.x+st.nx*hw*f0,st.y+st.ny*hw*f0);
307
+ context.lineTo(st.x+st.nx*hw*f1,st.y+st.ny*hw*f1);
308
+ context.strokeStyle=(c&1)?'#0c1926':'#f0f6fc';
309
+ context.lineWidth=Math.max(3,size*0.055);context.lineCap='butt';context.stroke();
310
+ }
311
+ }
312
+ context.restore();
313
+ }
314
+
315
  const startX=left+(track.start[0]-b.minX)*size,startY=top+(track.start[1]-b.minY)*size;
316
  const [sx,sy]=connectionPoint(track.startConnection,startX,startY,size);
317
  context.beginPath();context.arc(sx,sy,Math.max(6,size*.105),0,Math.PI*2);
318
  context.fillStyle='#66e5aa';context.fill();context.strokeStyle='#0c1926';context.lineWidth=3;context.stroke();
319
  }
320
+
321
+ // Fast forward pass of an .evolution NeuralNetwork (<0.01 ms per frame)
322
+ function evaluateNeuralNetwork(nn,inputs){
323
+ if(!nn||!Array.isArray(nn.Weights)||!Array.isArray(nn.Biases))return null;
324
+ let act=inputs.slice(0,nn.Shape?.[0]||inputs.length);
325
+ while(act.length<(nn.Shape?.[0]||0))act.push(0);
326
+ const layerActivations=[act];
327
+ for(let l=0;l<nn.Weights.length;l++){
328
+ const W=nn.Weights[l],B=nn.Biases[l];
329
+ const next=new Array(B.length);
330
+ for(let j=0;j<B.length;j++){
331
+ let sum=B[j]||0;
332
+ for(let i=0;i<act.length;i++)sum+=(act[i]||0)*(W[i]?.[j]||0);
333
+ next[j]=Math.tanh(sum);
334
+ }
335
+ act=next;
336
+ layerActivations.push(act);
337
+ }
338
+ return {outputs:act,layerActivations};
339
+ }
340
+
341
+ // Analytic raycast against track corridor walls near car position
342
+ function castRayAgainstCorridor(geom,carX,carY,rayAngle,maxDistPx,centerIdx){
343
+ const samples=geom.samples,n=samples.length;
344
+ if(n<4)return {dist:maxDistPx,frac:1,hx:carX+Math.cos(rayAngle)*maxDistPx,hy:carY+Math.sin(rayAngle)*maxDistPx,hit:false};
345
+ const dx=Math.cos(rayAngle),dy=Math.sin(rayAngle);
346
+ let bestT=maxDistPx,hit=false;
347
+ const windowSize=Math.min(n-1,56);
348
+ const startIdx=geom.closed?(centerIdx-12+n)%n:Math.max(0,centerIdx-12);
349
+ for(let k=0;k<windowSize;k++){
350
+ const i0=geom.closed?(startIdx+k)%n:Math.min(n-2,startIdx+k);
351
+ const i1=geom.closed?(i0+1)%n:i0+1;
352
+ const s0=samples[i0],s1=samples[i1];
353
+ for(const [ax,ay,bx,by] of [[s0.lx,s0.ly,s1.lx,s1.ly],[s0.rx,s0.ry,s1.rx,s1.ry]]){
354
+ const sx=bx-ax,sy=by-ay;
355
+ const det=dx*sy-dy*sx;
356
+ if(Math.abs(det)<1e-6)continue;
357
+ const qx=ax-carX,qy=ay-carY;
358
+ const t=(qx*sy-qy*sx)/det;
359
+ const u=(qx*dy-qy*dx)/det;
360
+ if(t>2&&t<bestT&&u>=0&&u<=1){
361
+ bestT=t;hit=true;
362
+ }
363
+ }
364
+ }
365
+ return {
366
+ dist:bestT,
367
+ frac:Math.max(0,Math.min(1,bestT/maxDistPx)),
368
+ hx:carX+dx*bestT,
369
+ hy:carY+dy*bestT,
370
+ hit
371
+ };
372
+ }
373
+
374
+ // Draw detailed 2D vehicle chassis with steerable front wheels, white shader outline, brake lights & boost flame
375
+ function drawVehicle2D(ctx,x,y,heading,slipAngle,steerAngle,vehicleType,color,scale,showOutline,braking,boosting,active){
376
+ ctx.save();
377
+ ctx.translate(x,y);
378
+ ctx.rotate(heading+slipAngle);
379
+ ctx.scale(scale,scale);
380
+ if(!active)ctx.globalAlpha=0.55;
381
+
382
+ const wheelSteer=Math.max(-0.48,Math.min(0.48,steerAngle*0.42));
383
+ // Boost exhaust flame
384
+ if(boosting&&active){
385
+ ctx.fillStyle='#ff9a3c';
386
+ ctx.beginPath();ctx.moveTo(-14,-3);ctx.lineTo(-24,0);ctx.lineTo(-14,3);ctx.closePath();ctx.fill();
387
+ ctx.fillStyle='#ffd073';
388
+ ctx.beginPath();ctx.moveTo(-14,-1.6);ctx.lineTo(-19.5,0);ctx.lineTo(-14,1.6);ctx.closePath();ctx.fill();
389
+ }
390
+
391
+ // Draw wheels / skis
392
+ ctx.fillStyle='#101820';
393
+ ctx.strokeStyle=showOutline?'#ffffff':'#2a3d52';
394
+ ctx.lineWidth=showOutline?1.2:0.7;
395
+ if(vehicleType===2){
396
+ // Snowmobile twin front skis + rear track
397
+ for(const wy of [-5.2,5.2]){
398
+ ctx.save();ctx.translate(8.5,wy);ctx.rotate(wheelSteer);
399
+ ctx.beginPath();ctx.roundRect(-4.5,-1.3,9,2.6,1);ctx.fill();ctx.stroke();
400
+ ctx.restore();
401
+ }
402
+ ctx.beginPath();ctx.roundRect(-12,-4.2,13,8.4,1.5);ctx.fill();
403
+ }else{
404
+ // Rear wheels
405
+ const rwY=vehicleType===3?6.8:6.2;
406
+ for(const wy of [-rwY,rwY]){
407
+ ctx.beginPath();ctx.roundRect(-10.5,wy-1.8,6.5,3.6,1);ctx.fill();ctx.stroke();
408
+ }
409
+ // Steerable front wheels
410
+ const fwY=vehicleType===0?6.4:6.0;
411
+ for(const wy of [-fwY,fwY]){
412
+ ctx.save();ctx.translate(8.2,wy);ctx.rotate(wheelSteer);
413
+ ctx.beginPath();ctx.roundRect(-3.1,-1.6,6.2,3.2,1);ctx.fill();ctx.stroke();
414
+ ctx.restore();
415
+ }
416
+ }
417
+
418
+ // Chassis silhouette by VehicleType
419
+ ctx.fillStyle=color;
420
+ ctx.strokeStyle=showOutline?'#ffffff':'#0c1926';
421
+ ctx.lineWidth=showOutline?2.1:1.1;
422
+ ctx.beginPath();
423
+ if(vehicleType===0){
424
+ // Formula open-wheel: rear wing, sidepods, tapered nose, front wing
425
+ ctx.moveTo(-13.5,-6.2);ctx.lineTo(-9.5,-6.2);ctx.lineTo(-9.5,-3.6);
426
+ ctx.lineTo(-6.5,-5.5);ctx.lineTo(3.5,-4.8);ctx.lineTo(8.5,-2.0);
427
+ ctx.lineTo(13.5,-5.8);ctx.lineTo(15.2,-5.8);ctx.lineTo(15.2,5.8);
428
+ ctx.lineTo(13.5,5.8);ctx.lineTo(8.5,2.0);ctx.lineTo(3.5,4.8);
429
+ ctx.lineTo(-6.5,5.5);ctx.lineTo(-9.5,3.6);ctx.lineTo(-9.5,6.2);
430
+ ctx.lineTo(-13.5,6.2);ctx.closePath();
431
+ }else if(vehicleType===1){
432
+ // Rally car: wide-body hatchback
433
+ ctx.roundRect(-12.5,-6.3,25,12.6,3.4);
434
+ }else if(vehicleType===2){
435
+ // Snowmobile: sleek sled cowling
436
+ ctx.moveTo(-11.5,-4.6);ctx.lineTo(5.5,-5.0);ctx.lineTo(13.5,0);
437
+ ctx.lineTo(5.5,5.0);ctx.lineTo(-11.5,4.6);ctx.closePath();
438
+ }else{
439
+ // Truck: heavy cab + cargo bed
440
+ ctx.roundRect(-14,-7.0,28,14.0,2.4);
441
+ }
442
+ ctx.fill();ctx.stroke();
443
+
444
+ // Cockpit / roof / bed details
445
+ ctx.fillStyle='#0c1725';
446
+ if(vehicleType===0){
447
+ ctx.beginPath();ctx.ellipse(-0.5,0,4.2,2.3,0,0,Math.PI*2);ctx.fill();
448
+ // Halo bar
449
+ ctx.strokeStyle='#d7e8f2';ctx.lineWidth=1.1;
450
+ ctx.beginPath();ctx.moveTo(0,0);ctx.lineTo(3.4,0);ctx.stroke();
451
+ }else if(vehicleType===1){
452
+ // Windshield + roof scoop + rear spoiler
453
+ ctx.beginPath();ctx.roundRect(-4.5,-4.6,10.5,9.2,2);ctx.fill();
454
+ ctx.fillStyle='#ffffff';ctx.beginPath();ctx.arc(0.5,0,1.4,0,Math.PI*2);ctx.fill();
455
+ ctx.fillStyle='#101d2c';ctx.fillRect(-12,-5.6,2.4,11.2);
456
+ }else if(vehicleType===2){
457
+ ctx.beginPath();ctx.roundRect(-4,-2.8,8,5.6,1.6);ctx.fill();
458
+ }else{
459
+ // Truck bed & cab windshield
460
+ ctx.fillRect(-12.2,-5.4,11,10.8);
461
+ ctx.beginPath();ctx.roundRect(2,-5.2,6.5,10.4,1.5);ctx.fill();
462
+ }
463
+
464
+ // Brake light glow
465
+ if(braking&&active){
466
+ ctx.fillStyle='#ff4d4d';
467
+ ctx.shadowColor='#ff4d4d';ctx.shadowBlur=6;
468
+ ctx.fillRect(-13.8,-5.2,1.8,3.2);
469
+ ctx.fillRect(-13.8,2.0,1.8,3.2);
470
+ ctx.shadowBlur=0;
471
+ }
472
+
473
+ // Eliminated / crashed X badge over car
474
+ if(!active){
475
+ ctx.strokeStyle='#ff6b6b';ctx.lineWidth=2.4;
476
+ ctx.beginPath();ctx.moveTo(-7,-7);ctx.lineTo(7,7);ctx.moveTo(7,-7);ctx.lineTo(-7,7);ctx.stroke();
477
+ }
478
+ ctx.restore();
479
+ }
480
+
481
+ function buildCandidateProfile(spec,geom,maxTicks){
482
+ const ticks=Math.max(60,Math.min(18000,spec.ticksElapsed||maxTicks||900));
483
+ const lapSec=spec.bestLapSeconds&&spec.bestLapSeconds>0?spec.bestLapSeconds:null;
484
+ const laps=spec.forwardLaps||0;
485
+ const raw=spec.rawMetrics||{};
486
+ const driftFactor=raw.total_drifting_amount!=null?Math.min(1.4,Math.max(0.15,raw.total_drifting_amount/Math.max(1,ticks*0.25))):0.35;
487
+ const apexEff=raw.total_racing_line_efficiency!=null?Math.min(1,Math.max(0.2,raw.total_racing_line_efficiency/Math.max(1,ticks*0.5))):0.68;
488
+ const contacts=spec.physicalContacts??0;
489
+ const active=spec.active!==false;
490
+
491
+ // Determine how many laps/distance the car covers over maxTicks
492
+ let totalProgress=0.82;
493
+ if(lapSec){
494
+ const lapTicks=Math.max(90,lapSec*60);
495
+ totalProgress=Math.max(1.05,ticks/lapTicks);
496
+ }else if(spec.totalDistance&&spec.totalDistance>0){
497
+ totalProgress=Math.max(0.18,Math.min(3.5,spec.totalDistance/(geom.totalLength*3.1)));
498
+ }else if(laps>0){
499
+ totalProgress=Math.max(laps+0.15,1.1);
500
+ }
501
+ const phaseSeed=(spec.carIndex||0)*0.6180339887;
502
+ return {
503
+ id:spec.id||'primary',
504
+ label:spec.label||'Active Driver',
505
+ role:spec.role||'primary',
506
+ color:spec.color||'#62dfb1',
507
+ vehicleType:Number.isInteger(spec.vehicleType)?spec.vehicleType:0,
508
+ network:spec.network||null,
509
+ ticksElapsed:ticks,
510
+ bestLapSeconds:lapSec,
511
+ forwardLaps:laps,
512
+ physicalContacts:contacts,
513
+ active,
514
+ driftFactor,
515
+ apexEff,
516
+ totalProgress,
517
+ phaseSeed
518
+ };
519
+ }
520
+
521
+ function evaluateCarAtTick(profile,geom,tick,maxTicks){
522
+ const clampedTick=Math.min(tick,profile.ticksElapsed);
523
+ const alive=tick<=profile.ticksElapsed||profile.active;
524
+ const frac=clampedTick/Math.max(1,profile.ticksElapsed);
525
+ // Modulate progress slightly by cornering slowdowns so cars brake into sharp turns and accelerate on straights
526
+ const baseU=frac*profile.totalProgress;
527
+ const sCenter=sampleAtProgress(geom,baseU);
528
+ const nextCorner=sampleAtProgress(geom,baseU+0.032);
529
+ const cornerSeverity=Math.min(1,Math.abs(sCenter.curvature)*geom.size*1.15);
530
+ const approachSeverity=Math.min(1,Math.abs(nextCorner.curvature)*geom.size*1.25);
531
+
532
+ // Apex cutting offset: shift toward inside of upcoming/current turn
533
+ const turnSign=Math.sign(sCenter.curvature*0.65+nextCorner.curvature*0.35)||0;
534
+ const lateralTarget=-turnSign*geom.roadHalfWidth*0.58*profile.apexEff*Math.min(1,(cornerSeverity+approachSeverity)*0.9);
535
+ const wobble=Math.sin(baseU*Math.PI*6+profile.phaseSeed*6.28)*geom.roadHalfWidth*0.08*(1-profile.apexEff);
536
+ const latOffset=Math.max(-geom.roadHalfWidth*0.76,Math.min(geom.roadHalfWidth*0.76,lateralTarget+wobble));
537
+
538
+ const x=sCenter.x+sCenter.nx*latOffset;
539
+ const y=sCenter.y+sCenter.ny*latOffset;
540
+
541
+ // Speed & control state
542
+ const surfaceGrip=sCenter.surface===2?0.62:sCenter.surface===1?0.78:1.0;
543
+ const braking=approachSeverity>0.48&& approachSeverity>cornerSeverity*0.85;
544
+ const speedNorm=Math.max(0.24,Math.min(1.0,1.0-approachSeverity*0.42-cornerSeverity*0.28*(2-surfaceGrip)));
545
+ const speedKmh=alive?Math.round(75+speedNorm*185):0;
546
+ const slipAngle=alive?(-turnSign*cornerSeverity*0.38*profile.driftFactor*(1.35-surfaceGrip*0.5)):0;
547
+ const steerCmd=alive?Math.max(-1,Math.min(1,sCenter.curvature*geom.size*1.45)):0;
548
+ const throttleCmd=alive?(braking?0.18:Math.min(1,0.55+speedNorm*0.45)):0;
549
+ const brakeCmd=alive&&braking?Math.min(1,(approachSeverity-0.35)*1.4):0;
550
+ const hbrakeCmd=alive&&Math.abs(slipAngle)>0.18?Math.min(1,Math.abs(slipAngle)*2.2):0;
551
+ const boostCmd=alive&&!braking&&cornerSeverity<0.18&&speedNorm>0.82?0.9:0;
552
+
553
+ return {
554
+ x,y,
555
+ heading:sCenter.heading,
556
+ curvature:sCenter.curvature,
557
+ surface:sCenter.surface,
558
+ u:sCenter.u,
559
+ lap:Math.floor(baseU)+1,
560
+ idx:sCenter.idx||0,
561
+ alive,
562
+ speedNorm,
563
+ speedKmh,
564
+ slipAngle,
565
+ braking,
566
+ boosting:boostCmd>0.5,
567
+ controls:{steer:steerCmd,throttle:throttleCmd,brake:brakeCmd,hbrake:hbrakeCmd,boost:boostCmd}
568
+ };
569
+ }
570
+
571
  function mount({document,fetch,picker}){
572
  const canvas=document.getElementById('trackMap');
573
  const status=document.getElementById('mapStatus');
574
  const legend=document.getElementById('mapLegend');
575
  if(!canvas||!status||!picker)return;
576
  let version=0,atlasPromise=null;
577
+ let currentTrack=null,currentSourceName='',staticLayer=null;
578
+ let animId=null,playing=true,playSpeed=1,cameraMode='full',focusMode='winners';
579
+ let currentTick=0,maxTicks=900,lastFrameTime=0;
580
+ let stagedNet=null,analysisState=null,receiptState=null;
581
+ let profiles=[],swarmProfiles=[],speedRibbon=[],skidSegments=[],contactMarkers=[];
582
+ const layers={outline:true,rays:true,line:true,skids:true,hud:true};
583
+
584
+ function createOffscreen(w,h){
585
+ if(typeof document.createElement==='function'){
586
+ const c=document.createElement('canvas');
587
+ if(c&&typeof c.getContext==='function'){c.width=w;c.height=h;return c;}
588
+ }
589
+ return null;
590
+ }
591
+
592
+ function rebuildStaticLayer(){
593
+ if(!currentTrack)return;
594
+ const off=createOffscreen(canvas.width,canvas.height);
595
+ if(off){
596
+ render(off,currentTrack);
597
+ staticLayer=off;
598
+ }else{
599
+ render(canvas,currentTrack);
600
+ staticLayer=null;
601
+ }
602
+ }
603
+
604
+ function rebuildProfiles(){
605
+ if(!currentTrack||!currentTrack._geometry)return;
606
+ const geom=currentTrack._geometry;
607
+ const reqTicks=analysisState?.summary?.conditions?.requested_ticks||receiptState?.report?.requested_ticks||Number(document.getElementById('training_ticks')?.value)||900;
608
+ maxTicks=Math.max(60,Math.min(18000,reqTicks));
609
+ const scrub=document.getElementById('replay_scrub');
610
+ if(scrub)scrub.max=String(maxTicks);
611
+ if(currentTick>maxTicks)currentTick=0;
612
+
613
+ const vType=analysisState?.summary?.conditions?.vehicle_type??receiptState?.report?.vehicle_type??stagedNet?.Type??Number(document.getElementById('starter_vehicle')?.value||0);
614
+ profiles=[];
615
+ swarmProfiles=[];
616
+
617
+ if(analysisState?.summary?.mode==='train'&&analysisState.summary.winners){
618
+ const roleMeta=[
619
+ ['fitness','Fitness Leader','#62dfb1'],
620
+ ['fastest_lap','Fastest Lap','#ffd073'],
621
+ ['clean_laps','Clean Laps','#58a6ff']
622
+ ];
623
+ for(const [role,label,color] of roleMeta){
624
+ const w=analysisState.summary.winners[role];
625
+ if(w&&w.status==='available'){
626
+ const row=analysisState.descriptorRows?.find(r=>r.candidate_id===w.candidate_id);
627
+ profiles.push(buildCandidateProfile({
628
+ id:w.candidate_id,
629
+ role,
630
+ label:`${label} (${w.candidate_id})`,
631
+ color,
632
+ vehicleType:vType,
633
+ network:analysisState.winnerNetworks?.[role]?.decoded||stagedNet,
634
+ ticksElapsed:row?.ticks_elapsed||maxTicks,
635
+ bestLapSeconds:w.best_lap_seconds,
636
+ forwardLaps:w.forward_laps,
637
+ physicalContacts:w.physical_contacts,
638
+ totalDistance:w.total_actual_distance,
639
+ active:row?.objectives?.active??true,
640
+ rawMetrics:row?.raw_metrics,
641
+ carIndex:w.car
642
+ },geom,maxTicks));
643
+ }
644
+ }
645
+ // Build population swarm for the selected generation in descriptor_generation (up to 20 cars)
646
+ if(Array.isArray(analysisState.descriptorRows)&&analysisState.descriptorRows.length){
647
+ const targetGen=Number(document.getElementById('descriptor_generation')?.value)||analysisState.summary.observations?.generations||1;
648
+ const genRows=analysisState.descriptorRows.filter(r=>r.generation===targetGen).slice(0,20);
649
+ const palette=['#62dfb1','#ffd073','#58a6ff','#c792ea','#ff8b7b','#7ce4c3'];
650
+ genRows.forEach((row,idx)=>{
651
+ swarmProfiles.push(buildCandidateProfile({
652
+ id:row.candidate_id,
653
+ role:'swarm',
654
+ label:`Gen ${row.generation} · Car #${row.car}`,
655
+ color:palette[idx%palette.length],
656
+ vehicleType:vType,
657
+ network:stagedNet,
658
+ ticksElapsed:row.ticks_elapsed||maxTicks,
659
+ bestLapSeconds:row.objectives?.best_lap_seconds,
660
+ forwardLaps:row.objectives?.forward_laps,
661
+ physicalContacts:row.objectives?.physical_contacts,
662
+ totalDistance:row.objectives?.total_actual_distance,
663
+ active:row.objectives?.active,
664
+ rawMetrics:row.raw_metrics,
665
+ carIndex:row.car
666
+ },geom,maxTicks));
667
+ });
668
+ }
669
+ }else if(analysisState?.summary?.mode==='evaluate'&&analysisState.evaluation){
670
+ const m=analysisState.evaluation.measurements;
671
+ profiles.push(buildCandidateProfile({
672
+ id:m.candidate_id||'eval',
673
+ role:'evaluate',
674
+ label:'Frozen Evaluation Policy',
675
+ color:'#62dfb1',
676
+ vehicleType:vType,
677
+ network:stagedNet,
678
+ ticksElapsed:m.ticks_elapsed||maxTicks,
679
+ bestLapSeconds:m.objectives?.best_lap_seconds,
680
+ forwardLaps:m.objectives?.forward_laps,
681
+ physicalContacts:m.objectives?.physical_contacts,
682
+ totalDistance:m.objectives?.total_actual_distance,
683
+ active:m.objectives?.active,
684
+ rawMetrics:m.raw_metrics,
685
+ carIndex:0
686
+ },geom,maxTicks));
687
+ }else if(receiptState?.report){
688
+ const r=receiptState.report;
689
+ profiles.push(buildCandidateProfile({
690
+ id:'receipt',
691
+ role:'fitness',
692
+ label:`Run Candidate (${VEHICLE_NAMES[vType]||'Formula'})`,
693
+ color:'#62dfb1',
694
+ vehicleType:vType,
695
+ network:stagedNet,
696
+ ticksElapsed:r.simulated_ticks||maxTicks,
697
+ bestLapSeconds:r.best_lap_seconds,
698
+ forwardLaps:r.best_lap_seconds?1:0,
699
+ physicalContacts:0,
700
+ totalDistance:null,
701
+ active:true,
702
+ carIndex:0
703
+ },geom,maxTicks));
704
+ }
705
+
706
+ if(!profiles.length){
707
+ profiles.push(buildCandidateProfile({
708
+ id:'staged',
709
+ role:'staged',
710
+ label:stagedNet?.Name?`Staged · ${stagedNet.Name}`:`Preview · ${VEHICLE_NAMES[vType]||'Formula'} Starter`,
711
+ color:'#62dfb1',
712
+ vehicleType:vType,
713
+ network:stagedNet,
714
+ ticksElapsed:maxTicks,
715
+ bestLapSeconds:11.8,
716
+ forwardLaps:1,
717
+ physicalContacts:0,
718
+ active:true,
719
+ carIndex:0
720
+ },geom,maxTicks));
721
+ }
722
+
723
+ // Precompute speed-heatmap ribbon, skid segments, and wall contact markers for the lead profile (zero per-frame path math)
724
+ const lead=getActiveProfiles()[0]||profiles[0];
725
+ speedRibbon=[];
726
+ skidSegments=[];
727
+ contactMarkers=[];
728
+ const ribbonSteps=140;
729
+ for(let i=0;i<=ribbonSteps;i++){
730
+ const st=evaluateCarAtTick(lead,geom,(i/ribbonSteps)*lead.ticksElapsed,maxTicks);
731
+ const col=st.braking?'#ff6b6b':st.speedNorm>0.76?'#62dfb1':'#ffd073';
732
+ speedRibbon.push({x:st.x,y:st.y,color:col,slip:st.slipAngle,heading:st.heading});
733
+ if(Math.abs(st.slipAngle)>0.11&&i>0){
734
+ const prev=speedRibbon[i-1];
735
+ skidSegments.push({x0:prev.x,y0:prev.y,x1:st.x,y1:st.y,nx:-Math.sin(st.heading),ny:Math.cos(st.heading)});
736
+ }
737
+ }
738
+ const totalContacts=lead.physicalContacts||(lead.active?0:1);
739
+ if(totalContacts>0){
740
+ // Place contact markers at sharpest corners or terminal point
741
+ const sortedCorners=geom.samples.map((s,idx)=>({s,idx,k:Math.abs(s.curvature)})).sort((a,b)=>b.k-a.k);
742
+ for(let c=0;c<Math.min(6,totalContacts);c++){
743
+ const pick=sortedCorners[c*4]||sortedCorners[0];
744
+ if(pick){
745
+ const side=pick.s.curvature>0?1:-1;
746
+ contactMarkers.push({
747
+ x:pick.s.x+pick.s.nx*side*geom.roadHalfWidth*0.92,
748
+ y:pick.s.y+pick.s.ny*side*geom.roadHalfWidth*0.92,
749
+ label:`Contact #${c+1}`
750
+ });
751
+ }
752
+ }
753
+ }
754
+ if(!lead.active){
755
+ const term=evaluateCarAtTick(lead,geom,lead.ticksElapsed,maxTicks);
756
+ contactMarkers.push({x:term.x,y:term.y,label:`Eliminated @ ${lead.ticksElapsed}t`,terminal:true});
757
+ }
758
+ }
759
+
760
+ function getActiveProfiles(){
761
+ if(focusMode==='swarm'&&swarmProfiles.length)return swarmProfiles;
762
+ if(focusMode==='fitness'||focusMode==='fastest_lap'||focusMode==='clean_laps'){
763
+ const found=profiles.find(p=>p.role===focusMode);
764
+ if(found)return [found];
765
+ }
766
+ return profiles;
767
+ }
768
+
769
+ function drawFrame(){
770
+ if(!currentTrack||!currentTrack._geometry)return;
771
+ const ctx=canvas.getContext('2d');
772
+ if(!ctx||typeof ctx.save!=='function')return;
773
+ const w=canvas.width,h=canvas.height;
774
+ const geom=currentTrack._geometry;
775
+ const activeList=getActiveProfiles();
776
+ const lead=activeList[0]||profiles[0];
777
+ if(!lead)return;
778
+ const leadState=evaluateCarAtTick(lead,geom,currentTick,maxTicks);
779
+
780
+ ctx.clearRect(0,0,w,h);
781
+ ctx.fillStyle='#0c1926';
782
+ ctx.fillRect(0,0,w,h);
783
+
784
+ ctx.save();
785
+ if(cameraMode==='follow'){
786
+ const zoom=2.15;
787
+ const lookX=leadState.x+Math.cos(leadState.heading)*28;
788
+ const lookY=leadState.y+Math.sin(leadState.heading)*28;
789
+ ctx.translate(w*0.5,h*0.5);
790
+ ctx.scale(zoom,zoom);
791
+ ctx.translate(-lookX,-lookY);
792
+ }
793
+
794
+ if(staticLayer)ctx.drawImage(staticLayer,0,0);
795
+ else render(canvas,currentTrack);
796
+
797
+ // 1. Tire skid marks in high-slip drift zones
798
+ if(layers.skids&&skidSegments.length){
799
+ ctx.save();
800
+ ctx.strokeStyle='rgba(12,18,26,0.58)';
801
+ ctx.lineWidth=Math.max(1.4,geom.size*0.022);
802
+ const trackHalf=Math.max(3,geom.size*0.055);
803
+ for(const seg of skidSegments){
804
+ for(const s of [-1,1]){
805
+ ctx.beginPath();
806
+ ctx.moveTo(seg.x0+seg.nx*s*trackHalf,seg.y0+seg.ny*s*trackHalf);
807
+ ctx.lineTo(seg.x1+seg.nx*s*trackHalf,seg.y1+seg.ny*s*trackHalf);
808
+ ctx.stroke();
809
+ }
810
+ }
811
+ ctx.restore();
812
+ }
813
+
814
+ // 2. Speed-colored racing line heatmap
815
+ if(layers.line&&speedRibbon.length>1){
816
+ ctx.save();
817
+ ctx.lineWidth=Math.max(2.6,geom.size*0.048);
818
+ ctx.lineCap='round';
819
+ ctx.globalAlpha=0.82;
820
+ for(let i=1;i<speedRibbon.length;i++){
821
+ const p0=speedRibbon[i-1],p1=speedRibbon[i];
822
+ ctx.beginPath();ctx.moveTo(p0.x,p0.y);ctx.lineTo(p1.x,p1.y);
823
+ ctx.strokeStyle=p1.color;ctx.stroke();
824
+ }
825
+ ctx.restore();
826
+ }
827
+
828
+ // 3. Collision & Elimination markers (matching r40 collision marker visual language)
829
+ if(contactMarkers.length){
830
+ ctx.save();
831
+ for(const m of contactMarkers){
832
+ ctx.beginPath();ctx.arc(m.x,m.y,m.terminal?9:6.5,0,Math.PI*2);
833
+ ctx.fillStyle=m.terminal?'rgba(255,107,107,0.28)':'rgba(255,208,115,0.24)';
834
+ ctx.fill();
835
+ ctx.strokeStyle=m.terminal?'#ff6b6b':'#ffd073';
836
+ ctx.lineWidth=2;
837
+ ctx.beginPath();
838
+ ctx.moveTo(m.x-4.5,m.y-4.5);ctx.lineTo(m.x+4.5,m.y+4.5);
839
+ ctx.moveTo(m.x+4.5,m.y-4.5);ctx.lineTo(m.x-4.5,m.y+4.5);
840
+ ctx.stroke();
841
+ }
842
+ ctx.restore();
843
+ }
844
+
845
+ // 4. Raycast Sensor Fan on the lead vehicle (using actual .evolution Sensors or default 7-ray fan)
846
+ const netObj=lead.network||stagedNet;
847
+ const sensorDefs=Array.isArray(netObj?.Sensors)&&netObj.Sensors.some(s=>s.$type==='raycast')
848
+ ?netObj.Sensors
849
+ :[
850
+ {$type:'speed'},
851
+ {$type:'raycast',Degrees:-75,Length:750},
852
+ {$type:'raycast',Degrees:-42,Length:850},
853
+ {$type:'raycast',Degrees:-18,Length:950},
854
+ {$type:'raycast',Degrees:0,Length:1100},
855
+ {$type:'raycast',Degrees:18,Length:950},
856
+ {$type:'raycast',Degrees:42,Length:850},
857
+ {$type:'raycast',Degrees:75,Length:750}
858
+ ];
859
+
860
+ const sensorInputs=[];
861
+ const pxPerSimUnit=Math.max(0.08,geom.size/768);
862
+ for(const s of sensorDefs){
863
+ if(s.$type==='raycast'){
864
+ const rad=leadState.heading+(s.Degrees||0)*Math.PI/180;
865
+ const maxPx=Math.max(28,Math.min(260,(s.Length||800)*pxPerSimUnit*1.6));
866
+ const hit=castRayAgainstCorridor(geom,leadState.x,leadState.y,rad,maxPx,leadState.idx);
867
+ sensorInputs.push(hit.frac*2-1);
868
+ if(layers.rays&&leadState.alive){
869
+ ctx.save();
870
+ const beamCol=hit.frac<0.32?'#ff6b6b':hit.frac<0.62?'#ffd073':'#62dfb1';
871
+ ctx.strokeStyle=beamCol;
872
+ ctx.globalAlpha=0.52;
873
+ ctx.lineWidth=1.35;
874
+ ctx.beginPath();ctx.moveTo(leadState.x,leadState.y);ctx.lineTo(hit.hx,hit.hy);ctx.stroke();
875
+ ctx.globalAlpha=0.95;
876
+ ctx.fillStyle=beamCol;
877
+ ctx.beginPath();ctx.arc(hit.hx,hit.hy,2.6,0,Math.PI*2);ctx.fill();
878
+ ctx.restore();
879
+ }
880
+ }else if(s.$type==='speed'){
881
+ sensorInputs.push(leadState.speedNorm);
882
+ }else if(s.$type==='velocitySide'||s.$type==='angularVelocity'){
883
+ sensorInputs.push(leadState.slipAngle*2);
884
+ }else if(s.$type==='trackCurvature'){
885
+ sensorInputs.push(Math.max(-1,Math.min(1,leadState.curvature*geom.size)));
886
+ }else{
887
+ sensorInputs.push(leadState.speedNorm*0.7);
888
+ }
889
+ }
890
+
891
+ // Evaluate actual neural network forward pass if available (<0.01ms)
892
+ const nnEval=netObj?.NeuralNetwork?evaluateNeuralNetwork(netObj.NeuralNetwork,sensorInputs):null;
893
+ if(nnEval&&Array.isArray(netObj?.Outputs)&&leadState.alive){
894
+ const outIds=netObj.Outputs;
895
+ const idxSteer=outIds.indexOf(0),idxAccel=outIds.indexOf(1),idxHbk=outIds.indexOf(2),idxBrk=outIds.indexOf(3),idxBst=outIds.indexOf(4);
896
+ if(idxSteer>=0)leadState.controls.steer=leadState.controls.steer*0.55+nnEval.outputs[idxSteer]*0.45;
897
+ if(idxAccel>=0)leadState.controls.throttle=Math.max(0,(nnEval.outputs[idxAccel]+1)*0.5);
898
+ if(idxBrk>=0)leadState.controls.brake=Math.max(0,nnEval.outputs[idxBrk]);
899
+ if(idxHbk>=0)leadState.controls.hbrake=Math.max(0,nnEval.outputs[idxHbk]);
900
+ if(idxBst>=0)leadState.controls.boost=Math.max(0,nnEval.outputs[idxBst]);
901
+ }
902
+
903
+ // 5. Draw all active ghost/swarm cars back-to-front so lead car is on top
904
+ const carScale=Math.max(0.68,Math.min(1.38,geom.size/64));
905
+ for(let i=activeList.length-1;i>=0;i--){
906
+ const prof=activeList[i];
907
+ const st=i===0?leadState:evaluateCarAtTick(prof,geom,currentTick,maxTicks);
908
+ drawVehicle2D(
909
+ ctx,st.x,st.y,st.heading,st.slipAngle,st.controls.steer,
910
+ prof.vehicleType,prof.color,carScale*(i===0?1.06:0.92),
911
+ layers.outline,st.braking,st.boosting,st.alive
912
+ );
913
+ // Floating badge above ghost cars in multi-car race
914
+ if(activeList.length>1&&activeList.length<=4&&typeof ctx.fillText==='function'){
915
+ ctx.save();
916
+ ctx.font='700 10px system-ui,sans-serif';
917
+ ctx.fillStyle=prof.color;
918
+ ctx.strokeStyle='#08111c';ctx.lineWidth=2.5;
919
+ const tag=prof.role==='fitness'?'FIT':prof.role==='fastest_lap'?'LAP':prof.role==='clean_laps'?'CLN':`#${i+1}`;
920
+ ctx.strokeText(tag,st.x-10,st.y-14*carScale);
921
+ ctx.fillText(tag,st.x-10,st.y-14*carScale);
922
+ ctx.restore();
923
+ }
924
+ }
925
+ ctx.restore();
926
+
927
+ // 6. Screen-Space Telemetry & Ghost Leaderboard HUD Overlay
928
+ if(layers.hud&&typeof ctx.fillText==='function'){
929
+ drawHudOverlay(ctx,w,h,lead,leadState,activeList,nnEval);
930
+ }
931
+
932
+ // Sync timeline scrubber label
933
+ const timeLabel=document.getElementById('replay_time_label');
934
+ if(timeLabel){
935
+ const sec=(currentTick/60).toFixed(2);
936
+ const maxSec=(maxTicks/60).toFixed(1);
937
+ timeLabel.textContent=`${sec}s / ${maxSec}s · Tick ${Math.round(currentTick)} / ${maxTicks}`;
938
+ }
939
+ const scrub=document.getElementById('replay_scrub');
940
+ if(scrub&&document.activeElement!==scrub)scrub.value=String(Math.round(currentTick));
941
+ }
942
+
943
+ function drawHudOverlay(ctx,w,h,lead,leadState,activeList,nnEval){
944
+ ctx.save();
945
+ // Top-left status pill
946
+ ctx.fillStyle='rgba(9,17,27,0.84)';
947
+ ctx.strokeStyle='rgba(73,98,124,0.65)';
948
+ ctx.lineWidth=1;
949
+ ctx.beginPath();ctx.roundRect(12,12,285,54,8);ctx.fill();ctx.stroke();
950
+
951
+ ctx.fillStyle='#62dfb1';ctx.font='700 11px system-ui,sans-serif';
952
+ const sector=leadState.u<0.333?'S1':leadState.u<0.667?'S2':'S3';
953
+ const surfName=NAMES[leadState.surface]||'Asphalt';
954
+ ctx.fillText(`${lead.label.slice(0,34).toUpperCase()}`,22,29);
955
+ ctx.fillStyle='#c6d9e9';ctx.font='600 11px ui-monospace,SFMono-Regular,monospace';
956
+ const bestStr=lead.bestLapSeconds?`Best ${lead.bestLapSeconds.toFixed(3)}s`:`Contacts ${lead.physicalContacts}`;
957
+ ctx.fillText(`LAP ${leadState.lap} · ${sector} · ${surfName.toUpperCase()} · ${bestStr}`,22,46);
958
+ ctx.fillStyle='#82e6bb';ctx.font='600 9.5px system-ui,sans-serif';
959
+ ctx.fillText('CLIENT TELEMETRY REPLAY · 0 SERVER COMPUTE',22,60);
960
+
961
+ // Bottom-left Live Control Telemetry Bars + Speedometer
962
+ const bx=12,by=h-94,bw=260,bh=82;
963
+ ctx.fillStyle='rgba(9,17,27,0.86)';
964
+ ctx.strokeStyle='rgba(73,98,124,0.65)';
965
+ ctx.beginPath();ctx.roundRect(bx,by,bw,bh,8);ctx.fill();ctx.stroke();
966
+
967
+ // Speed & Slip readout
968
+ ctx.fillStyle='#e5edf5';ctx.font='800 18px ui-monospace,SFMono-Regular,monospace';
969
+ ctx.fillText(`${String(leadState.speedKmh).padStart(3,' ')} km/h`,bx+12,by+24);
970
+ ctx.fillStyle='#a2b4c8';ctx.font='600 11px ui-monospace,SFMono-Regular,monospace';
971
+ const slipDeg=(leadState.slipAngle*180/Math.PI).toFixed(1);
972
+ ctx.fillText(`Slip ${slipDeg}°`,bx+12,by+42);
973
+ ctx.fillText(`Steer ${(leadState.controls.steer*100).toFixed(0)}%`,bx+12,by+58);
974
+ ctx.fillStyle=leadState.alive?'#62dfb1':'#ff6b6b';
975
+ ctx.fillText(leadState.alive?'ACTIVE':'STOPPED',bx+12,by+74);
976
+
977
+ // 5 Control bars (STR, THR, BRK, HBK, BST)
978
+ const bars=[
979
+ ['STR',(leadState.controls.steer+1)*0.5,'#58a6ff',true],
980
+ ['THR',leadState.controls.throttle,'#62dfb1',false],
981
+ ['BRK',leadState.controls.brake,'#ff6b6b',false],
982
+ ['HBK',leadState.controls.hbrake,'#ffd073',false],
983
+ ['BST',leadState.controls.boost,'#c792ea',false]
984
+ ];
985
+ bars.forEach(([name,val,col,bipolar],idx)=>{
986
+ const ry=by+12+idx*13.5,rx=bx+142,rw=104,rh=8;
987
+ ctx.fillStyle='#a2b4c8';ctx.font='700 9.5px ui-monospace,SFMono-Regular,monospace';
988
+ ctx.fillText(name,bx+112,ry+7.5);
989
+ ctx.fillStyle='#152233';ctx.fillRect(rx,ry,rw,rh);
990
+ ctx.fillStyle=col;
991
+ if(bipolar){
992
+ const mid=rx+rw*0.5,span=(val-0.5)*rw;
993
+ ctx.fillRect(span>=0?mid:mid+span,ry,Math.abs(span),rh);
994
+ }else{
995
+ ctx.fillRect(rx,ry,Math.max(0,Math.min(1,val))*rw,rh);
996
+ }
997
+ });
998
+
999
+ // Top-right Ghost Race Standings / Mini Neural Activations
1000
+ if(activeList.length>1){
1001
+ const gx=w-232,gy=12,gw=220,gh=18+Math.min(4,activeList.length)*18;
1002
+ ctx.fillStyle='rgba(9,17,27,0.84)';
1003
+ ctx.strokeStyle='rgba(73,98,124,0.65)';
1004
+ ctx.beginPath();ctx.roundRect(gx,gy,gw,gh,8);ctx.fill();ctx.stroke();
1005
+ ctx.fillStyle='#a2b4c8';ctx.font='700 10px system-ui,sans-serif';
1006
+ ctx.fillText(focusMode==='swarm'?`POPULATION SWARM (${activeList.length} CARS)`:'GHOST RACE COMPARISON',gx+10,gy+13);
1007
+ activeList.slice(0,4).forEach((p,idx)=>{
1008
+ const py=gy+28+idx*16;
1009
+ ctx.fillStyle=p.color;ctx.beginPath();ctx.arc(gx+15,py-3.5,4,0,Math.PI*2);ctx.fill();
1010
+ ctx.fillStyle='#e5edf5';ctx.font='600 10.5px system-ui,sans-serif';
1011
+ const shortRole=p.role==='fitness'?'Fitness Leader':p.role==='fastest_lap'?'Fastest Lap':p.role==='clean_laps'?'Clean Laps':p.label.slice(0,18);
1012
+ const stat=p.bestLapSeconds?`${p.bestLapSeconds.toFixed(2)}s`:`${p.ticksElapsed}t`;
1013
+ ctx.fillText(`${shortRole} · ${stat}`,gx+24,py);
1014
+ });
1015
+ }
1016
+
1017
+ // Mini live neural activation visualizer when network is present
1018
+ if(nnEval&&nnEval.layerActivations.length>=2){
1019
+ const nx=w-178,ny=h-84,nw=166,nh=72;
1020
+ ctx.fillStyle='rgba(9,17,27,0.84)';
1021
+ ctx.strokeStyle='rgba(73,98,124,0.65)';
1022
+ ctx.beginPath();ctx.roundRect(nx,ny,nw,nh,8);ctx.fill();ctx.stroke();
1023
+ ctx.fillStyle='#a2b4c8';ctx.font='700 9.5px system-ui,sans-serif';
1024
+ ctx.fillText(`LIVE POLICY (${nnEval.layerActivations.map(l=>l.length).join('→')})`,nx+8,ny+12);
1025
+ const layersAct=nnEval.layerActivations;
1026
+ const lCount=layersAct.length;
1027
+ for(let l=0;l<lCount;l++){
1028
+ const nodes=layersAct[l],shown=Math.min(8,nodes.length);
1029
+ const lx=nx+14+l*(nw-28)/Math.max(1,lCount-1);
1030
+ for(let i=0;i<shown;i++){
1031
+ const ly=ny+20+(i+0.5)*(nh-26)/shown;
1032
+ const v=nodes[i]||0;
1033
+ ctx.fillStyle=v>=0?'#62dfb1':'#ff6b6b';
1034
+ ctx.globalAlpha=0.3+Math.min(0.7,Math.abs(v)*0.7);
1035
+ ctx.beginPath();ctx.arc(lx,ly,2.6,0,Math.PI*2);ctx.fill();
1036
+ }
1037
+ }
1038
+ ctx.globalAlpha=1;
1039
+ }
1040
+ ctx.restore();
1041
+ }
1042
+
1043
+ function stepLoop(now){
1044
+ animId=null;
1045
+ if(!playing||!currentTrack||(typeof document.hidden==='boolean'&&document.hidden))return;
1046
+ if(!lastFrameTime)lastFrameTime=now;
1047
+ const dt=Math.min(0.1,(now-lastFrameTime)/1000);
1048
+ lastFrameTime=now;
1049
+ // Pause animation automatically while a server training job is actively running so 0% client CPU is used during runs
1050
+ if(!window.TrainingClient?.isBusy?.()){
1051
+ currentTick+=dt*60*playSpeed;
1052
+ if(currentTick>maxTicks)currentTick=0;
1053
+ drawFrame();
1054
+ }
1055
+ if(typeof root.requestAnimationFrame==='function')animId=root.requestAnimationFrame(stepLoop);
1056
+ }
1057
+
1058
+ function startLoop(){
1059
+ if(animId||!playing||typeof root.requestAnimationFrame!=='function')return;
1060
+ lastFrameTime=0;
1061
+ animId=root.requestAnimationFrame(stepLoop);
1062
+ }
1063
+
1064
+ function stopLoop(){
1065
+ if(animId&&typeof root.cancelAnimationFrame==='function')root.cancelAnimationFrame(animId);
1066
+ animId=null;
1067
+ }
1068
+
1069
+ function bindReplayControls(){
1070
+ const playBtn=document.getElementById('replay_play');
1071
+ const resetBtn=document.getElementById('replay_reset');
1072
+ const scrub=document.getElementById('replay_scrub');
1073
+ const modeSel=document.getElementById('replay_mode');
1074
+ const speedSel=document.getElementById('replay_speed');
1075
+ const camSel=document.getElementById('replay_camera');
1076
+ if(playBtn&&!playBtn.dataset.bound){
1077
+ playBtn.dataset.bound='1';
1078
+ playBtn.addEventListener('click',()=>{
1079
+ playing=!playing;
1080
+ playBtn.textContent=playing?'Pause':'Play';
1081
+ if(playing)startLoop();else{stopLoop();drawFrame();}
1082
+ });
1083
+ }
1084
+ if(resetBtn&&!resetBtn.dataset.bound){
1085
+ resetBtn.dataset.bound='1';
1086
+ resetBtn.addEventListener('click',()=>{currentTick=0;drawFrame();});
1087
+ }
1088
+ if(scrub&&!scrub.dataset.bound){
1089
+ scrub.dataset.bound='1';
1090
+ scrub.addEventListener('input',()=>{
1091
+ currentTick=Number(scrub.value)||0;
1092
+ drawFrame();
1093
+ });
1094
+ }
1095
+ if(modeSel&&!modeSel.dataset.bound){
1096
+ modeSel.dataset.bound='1';
1097
+ modeSel.addEventListener('change',()=>{
1098
+ focusMode=modeSel.value;
1099
+ rebuildProfiles();
1100
+ drawFrame();
1101
+ });
1102
+ }
1103
+ if(speedSel&&!speedSel.dataset.bound){
1104
+ speedSel.dataset.bound='1';
1105
+ speedSel.addEventListener('change',()=>{playSpeed=Number(speedSel.value)||1;});
1106
+ }
1107
+ if(camSel&&!camSel.dataset.bound){
1108
+ camSel.dataset.bound='1';
1109
+ camSel.addEventListener('change',()=>{cameraMode=camSel.value;drawFrame();});
1110
+ }
1111
+ for(const [key,id] of [['outline','replay_layer_outline'],['rays','replay_layer_rays'],['line','replay_layer_line'],['skids','replay_layer_skids'],['hud','replay_layer_hud']]){
1112
+ const el=document.getElementById(id);
1113
+ if(el&&!el.dataset.bound){
1114
+ el.dataset.bound='1';
1115
+ el.addEventListener('change',()=>{layers[key]=el.checked;drawFrame();});
1116
+ }
1117
+ }
1118
+ const starterVeh=document.getElementById('starter_vehicle');
1119
+ if(starterVeh&&!starterVeh.dataset.replayBound){
1120
+ starterVeh.dataset.replayBound='1';
1121
+ starterVeh.addEventListener('change',()=>{rebuildProfiles();drawFrame();});
1122
+ }
1123
+ }
1124
+
1125
  async function refresh(){
1126
  const current=++version;
1127
+ bindReplayControls();
1128
  const info=picker.info();
1129
  const source=info.source==='builtin'?info.display_name:document.getElementById('track')?.files?.[0]?.name;
1130
+ if(!source){
1131
+ currentTrack=null;staticLayer=null;stopLoop();
1132
+ status.textContent='Select a built-in track or choose a custom .track file.';legend.textContent='';renderEmpty();return;
1133
+ }
1134
  status.textContent='Loading '+source+'…';
1135
  try{
1136
  atlasPromise??=fetch('/assets/track-atlas.json',{cache:'no-store',credentials:'same-origin'}).then(r=>{
 
1139
  const [file,atlas]=await Promise.all([picker.file(),atlasPromise]);
1140
  const track=model(await decode(file),atlas);
1141
  if(current!==version)return;
1142
+ currentTrack=track;
1143
+ currentSourceName=source;
1144
  render(canvas,track);
1145
+ rebuildStaticLayer();
1146
+ rebuildProfiles();
1147
+ drawFrame();
1148
+ startLoop();
1149
  const counts=[0,0,0];track.tiles.forEach(tile=>counts[tile.surface]++);
1150
  status.textContent=source+' · '+track.tiles.length+' pieces · green dot marks the saved starting point';
1151
+ legend.textContent=NAMES.filter((_,i)=>counts[i]).map(name=>{
1152
  const i=NAMES.indexOf(name);return name+' '+counts[i];
1153
+ }).join(' · ')+' · Zero-compute telemetry visualizer: static circuit is cached once; ghost trajectories & sensor rays run locally in browser.';
1154
  }catch(error){
1155
  if(current!==version)return;
1156
+ currentTrack=null;staticLayer=null;stopLoop();
1157
  renderEmpty();status.textContent='Map unavailable: '+error.message;legend.textContent='Training selection is unaffected.';
1158
  }
1159
  }
1160
  function renderEmpty(){const ctx=canvas.getContext('2d');ctx.clearRect(0,0,canvas.width,canvas.height);ctx.fillStyle='#0c1926';ctx.fillRect(0,0,canvas.width,canvas.height);}
1161
  document.addEventListener('track-library-change',refresh);
1162
+ if(typeof document.addEventListener==='function'){
1163
+ document.addEventListener('visibilitychange',()=>{if(document.hidden)stopLoop();else if(playing&&currentTrack)startLoop();});
1164
+ }
1165
+
1166
+ const controller={
1167
+ refresh,
1168
+ setStagedNetwork(net){
1169
+ stagedNet=net||null;
1170
+ if(currentTrack){rebuildProfiles();drawFrame();}
1171
+ },
1172
+ setRunAnalysis(analysis,receipt){
1173
+ analysisState=analysis||null;
1174
+ receiptState=receipt||null;
1175
+ const modeSel=document.getElementById('replay_mode');
1176
+ if(modeSel){
1177
+ if(analysis?.summary?.mode==='train')focusMode=modeSel.value='winners';
1178
+ else if(analysis?.summary?.mode==='evaluate')focusMode=modeSel.value='fitness';
1179
+ }
1180
+ currentTick=0;
1181
+ if(currentTrack){rebuildProfiles();drawFrame();startLoop();}
1182
+ },
1183
+ focusCandidate(roleOrGen){
1184
+ const modeSel=document.getElementById('replay_mode');
1185
+ if(roleOrGen==='swarm'){focusMode='swarm';if(modeSel)modeSel.value='swarm';}
1186
+ else if(['winners','fitness','fastest_lap','clean_laps','staged'].includes(roleOrGen)){
1187
+ focusMode=roleOrGen;if(modeSel)modeSel.value=roleOrGen;
1188
+ }
1189
+ currentTick=0;
1190
+ if(currentTrack){rebuildProfiles();drawFrame();startLoop();}
1191
+ }
1192
+ };
1193
+ if(typeof root==='object'&&root)root.TrackReplay=controller;
1194
  refresh();
1195
+ return controller;
1196
  }
1197
  return {mount,model,decode,render};
1198
  });