Spaces:
Running on Zero
Running on Zero
Upgrade track telemetry replay visualizer, KPI strip, generation chart, and 2D vehicle outline preview
Browse files- FRONTEND_SOURCE.json +7 -7
- RELEASE_MANIFEST.json +4 -4
- __pycache__/analysis_result.cpython-313.pyc +0 -0
- __pycache__/app_release.cpython-313.pyc +0 -0
- __pycache__/bucket_storage.cpython-313.pyc +0 -0
- __pycache__/build_info.cpython-313.pyc +0 -0
- __pycache__/builtin_tracks.cpython-313.pyc +0 -0
- __pycache__/delivery.cpython-313.pyc +0 -0
- __pycache__/hf_identity.cpython-313.pyc +0 -0
- __pycache__/portable_bundle.cpython-313.pyc +0 -0
- __pycache__/server.cpython-313.pyc +0 -0
- __pycache__/star_check.cpython-313.pyc +0 -0
- __pycache__/steam_leaderboard.cpython-313.pyc +0 -0
- __pycache__/training_config.cpython-313.pyc +0 -0
- __pycache__/worker.cpython-313.pyc +0 -0
- client.js +60 -3
- index.html +87 -6
- track-map.js +1047 -5
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": "
|
| 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": "
|
| 12 |
"packaged_frontend_files": {
|
| 13 |
"assets/track-atlas.json": "d97c8900e3e5fb17734b5bd61d1f4f4b196d9be30af9848e2e273ae7e4079ae4",
|
| 14 |
-
"client.js": "
|
| 15 |
-
"help.html": "
|
| 16 |
"hf-auth.js": "3f3938bbc0935e4e9230c094029ef46e03beea74be199887c50e053e03dafcfe",
|
| 17 |
-
"index.html": "
|
| 18 |
"track-library.js": "351ec4c026b49420ce963f4310cb000d9e987fd28e6dac5ecd0176509340f6e6",
|
| 19 |
-
"track-map.js": "
|
| 20 |
},
|
| 21 |
-
"packaged_frontend_sha256": "
|
| 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": "
|
| 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": "
|
| 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": "
|
| 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": "
|
| 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 & tutorial ↗</a></div></header>
|
| 36 |
-
<details class="about"><summary>About & 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 & results</button><button type="button" id="tab-training" role="tab" data-tab="training" aria-controls="training" aria-selected="false" tabindex="-1">Training & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & tutorial ↗</a></div></header>
|
| 37 |
+
<details class="about"><summary>About & 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 & results</button><button type="button" id="tab-training" role="tab" data-tab="training" aria-controls="training" aria-selected="false" tabindex="-1">Training & 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 & 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 & 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 & Curbs</label><label><input type="checkbox" id="replay_layer_hud" checked> Live Telemetry & 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 & 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 & 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 & mutation</button><button type="button" data-train-filter="run_settings" aria-pressed="false">Timing & 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 & 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 & 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 & 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 & 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){
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 143 |
const i=NAMES.indexOf(name);return name+' '+counts[i];
|
| 144 |
-
}).join(' · ')+' ·
|
| 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
|
| 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&¤tTrack)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 |
});
|