Spaces:
Running on Zero
Running on Zero
Fix original-game export loading and add opt-in Steam comparison
Browse filesKeep existing Space access settings and simulator binaries. Fix required Id, Stats and Snapshots in exported game networks. Add optional read-only Steam comparison, run-wide best lap and bounded request admission. Formula remains the only hosted vehicle.
- DATA_POLICY.md +9 -0
- FRONTEND_SOURCE.json +3 -3
- RELEASE_MANIFEST.json +7 -6
- client.js +44 -0
- index.html +6 -3
- server.py +32 -0
- steam_leaderboard.py +228 -0
- worker.py +11 -2
DATA_POLICY.md
CHANGED
|
@@ -108,3 +108,12 @@ to the server. Exporting saves a new file, not the original source. No training
|
|
| 108 |
history is attributed to freshly created or edited weights. A future bucket
|
| 109 |
adapter is not implemented; it must be explicitly selected and must not silently
|
| 110 |
turn public ephemeral storage into persistent storage.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
history is attributed to freshly created or edited weights. A future bucket
|
| 109 |
adapter is not implemented; it must be explicitly selected and must not silently
|
| 110 |
turn public ephemeral storage into persistent storage.
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
## Optional public Steam leaderboard check
|
| 114 |
+
|
| 115 |
+
The “After a completed lap” checkbox is off by default. With it off, or when a run reports no completed lap, the browser makes no leaderboard request. When enabled, the browser sends only the built-in track ID and hash, Formula vehicle type, and reported best-lap seconds to this app's same-origin server. Custom or unidentified tracks are not sent. HF does not forward the lap, network, Steam ID, account credential, file name or other run data to Steam.
|
| 116 |
+
|
| 117 |
+
The app reads the public Steam Community track index and matching board XML without a Steam login or publisher key. It resolves exact catalog reference labels against that live index; an absent or ambiguous mapping is unavailable. The app retains only a short-lived in-process cache of public board rank/time rows (up to 60 seconds) and the public index mapping (up to five minutes). Player names, Steam IDs and raw XML are discarded after parsing. The comparison request and result are not stored in the job result store or recipe library.
|
| 118 |
+
|
| 119 |
+
The displayed value is an insertion-position estimate against the fetched raw global board, at millisecond precision. The board may contain entries for different vehicle types; the estimate is not vehicle-filtered, an official rank, a submitted score, or proof that replica timing matches the Steam game. The check runs in the background with an eight-second browser deadline. It cannot delay result availability or download; a timeout leaves the completed result intact. Steam's community page may change or become unavailable, in which case the app reports the check as unavailable.
|
FRONTEND_SOURCE.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
{
|
| 2 |
"source": "Runner r14 single-engine browser UI; selected .track tile renderer added from simulator atlas",
|
| 3 |
"source_sha256": "65d1e5f2c4e19b4951b828e82510005ab99a5270cf3dcb9f4eaa0fa1af8c40f8",
|
| 4 |
-
"output_sha256": "
|
| 5 |
"scope": "Frontend only; no engine, private data, track assets, API calls or storage copied",
|
| 6 |
-
"status": "
|
| 7 |
"track_atlas_sha256": "d97c8900e3e5fb17734b5bd61d1f4f4b196d9be30af9848e2e273ae7e4079ae4",
|
| 8 |
-
"client_sha256": "
|
| 9 |
"track_map_sha256": "ab5374aa49c7ccec90a787ee8c56cd71beb56b903bf2bbc322783e6f43c009f0"
|
| 10 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"source": "Runner r14 single-engine browser UI; selected .track tile renderer added from simulator atlas",
|
| 3 |
"source_sha256": "65d1e5f2c4e19b4951b828e82510005ab99a5270cf3dcb9f4eaa0fa1af8c40f8",
|
| 4 |
+
"output_sha256": "713496757a5fa796cf35eb9fba7a4f29b0eb2f8abdd6c8e3a6fdfd8036a3a30f",
|
| 5 |
"scope": "Frontend only; no engine, private data, track assets, API calls or storage copied",
|
| 6 |
+
"status": "Original-game export metadata corrected; optional Steam comparison; vehicle/track audit separate",
|
| 7 |
"track_atlas_sha256": "d97c8900e3e5fb17734b5bd61d1f4f4b196d9be30af9848e2e273ae7e4079ae4",
|
| 8 |
+
"client_sha256": "93af3e91c0c5fe56ea0d95c40bb26b3e81aff3d36c38b035b4666a4d0955563a",
|
| 9 |
"track_map_sha256": "ab5374aa49c7ccec90a787ee8c56cd71beb56b903bf2bbc322783e6f43c009f0"
|
| 10 |
}
|
RELEASE_MANIFEST.json
CHANGED
|
@@ -5,9 +5,9 @@
|
|
| 5 |
"BETA_TESTING.md": "840192b5dec11ab903b05b358d14bd266efd3aaceedcf6d2794f7dc874b9caeb",
|
| 6 |
"BINARY_PROVENANCE.json": "1116e1757afc6c8629cb2d957e3d77380fe4f7e3186c3832d16a5a08abce6aa8",
|
| 7 |
"CUDA_REFERENCE.md": "ecee71c9a509702011026ee62bb60ba4e41dac868d6710bcc8c49c8995a2ffeb",
|
| 8 |
-
"DATA_POLICY.md": "
|
| 9 |
"FEEDBACK.md": "f95b8f6afbf4a360976c0edca5d52f5b3a0b668c9a944d4947b840a7c53a122a",
|
| 10 |
-
"FRONTEND_SOURCE.json": "
|
| 11 |
"GODOT_LICENSE": "fb2b3bb82c60f1e76841ba2122eb902f8e39c151476fe21ce10c9a05d1045304",
|
| 12 |
"IMPORTER_BUILD_RECEIPT.json": "4ba5c533cc617105478e4d85c83f456a7dfa68b1388049624619871ba9e42e9d",
|
| 13 |
"LICENSE": "53d5851b6440feeae36f72a5f57fcab37858c48b7084765ece4b0c54b9a28ed8",
|
|
@@ -53,14 +53,15 @@
|
|
| 53 |
"builtin_tracks/tracks/Replica R08 Demolition Derby.track": "6f11a9ffbcb477c9d19964566b1b612d2a94675605246e93e21f27bbbdb357ef",
|
| 54 |
"builtin_tracks/tracks/Replica R09 Eau Noir.track": "3ec0a78478cca1a4f0d2d98ac81f40783928adf271b70f0c32d295a6da2cdaa9",
|
| 55 |
"builtin_tracks/tracks/Replica The Champions Trial.track": "a3bfb76422affc774b44c739b0636d08acb901fa7615b9adcb52c069f79834e1",
|
| 56 |
-
"client.js": "
|
| 57 |
"delivery.py": "07453999c3831a16e8b1fcc2b4ae5fb28beb7d3952a3fae8de65d16eb6f11e5b",
|
| 58 |
"gpu_tickets.py": "b81aadb5ed42cec9212d44da7e7bbb7b413c5bd517383cac0b49ec9b4d659c92",
|
| 59 |
"hf-auth.js": "d5cf17b961d1f2efa9d4648d33ba86a148eff0d542c74a121710c4df35c92936",
|
| 60 |
"hf_identity.py": "6993d0238ada835606dbdc3275c17a3fe5a5cac5ad2156ab78230acf2d051eba",
|
| 61 |
-
"index.html": "
|
| 62 |
"requirements.txt": "9b532747e51bd052e82090ece860d6dafe027407f4512dc74272c148ea104c22",
|
| 63 |
-
"server.py": "
|
|
|
|
| 64 |
"track-library.js": "351ec4c026b49420ce963f4310cb000d9e987fd28e6dac5ecd0176509340f6e6",
|
| 65 |
"track-map.js": "ab5374aa49c7ccec90a787ee8c56cd71beb56b903bf2bbc322783e6f43c009f0",
|
| 66 |
"training_config.py": "a0dd095e0de8c5a964ace66954f346e5d7b861db3d34cd2feaceedc5ecb80bad",
|
|
@@ -70,7 +71,7 @@
|
|
| 70 |
"vendor/hf-oauth/LICENSE": "d6e7b19451c1b3d1d66353bb4b1274138953b3bfd9ae60c56e77b1f548796982",
|
| 71 |
"vendor/hf-oauth/index.js": "53282844000a171011e547f6472a3846bb4cecb5071adafea079fcac3a2aee7e",
|
| 72 |
"vendor/hf-oauth/package.json": "fafd4d8891023a1961546a7d3bc25cd24f64923a2ebfc449fe6d516a2949bda8",
|
| 73 |
-
"worker.py": "
|
| 74 |
},
|
| 75 |
"interface": "single-engine-runner-with-map",
|
| 76 |
"upstream_revision": "d8ab74c25df70b6a86ed101080e33d5fb785ea6f"
|
|
|
|
| 5 |
"BETA_TESTING.md": "840192b5dec11ab903b05b358d14bd266efd3aaceedcf6d2794f7dc874b9caeb",
|
| 6 |
"BINARY_PROVENANCE.json": "1116e1757afc6c8629cb2d957e3d77380fe4f7e3186c3832d16a5a08abce6aa8",
|
| 7 |
"CUDA_REFERENCE.md": "ecee71c9a509702011026ee62bb60ba4e41dac868d6710bcc8c49c8995a2ffeb",
|
| 8 |
+
"DATA_POLICY.md": "4d7d0da26a5366ea9c7d33a47dacb8d0d5545174febae170226cc2b924b0a479",
|
| 9 |
"FEEDBACK.md": "f95b8f6afbf4a360976c0edca5d52f5b3a0b668c9a944d4947b840a7c53a122a",
|
| 10 |
+
"FRONTEND_SOURCE.json": "53a819f3ce95a9393713e8db5e7b112cd0eed15bb57b84a415499436fad6224f",
|
| 11 |
"GODOT_LICENSE": "fb2b3bb82c60f1e76841ba2122eb902f8e39c151476fe21ce10c9a05d1045304",
|
| 12 |
"IMPORTER_BUILD_RECEIPT.json": "4ba5c533cc617105478e4d85c83f456a7dfa68b1388049624619871ba9e42e9d",
|
| 13 |
"LICENSE": "53d5851b6440feeae36f72a5f57fcab37858c48b7084765ece4b0c54b9a28ed8",
|
|
|
|
| 53 |
"builtin_tracks/tracks/Replica R08 Demolition Derby.track": "6f11a9ffbcb477c9d19964566b1b612d2a94675605246e93e21f27bbbdb357ef",
|
| 54 |
"builtin_tracks/tracks/Replica R09 Eau Noir.track": "3ec0a78478cca1a4f0d2d98ac81f40783928adf271b70f0c32d295a6da2cdaa9",
|
| 55 |
"builtin_tracks/tracks/Replica The Champions Trial.track": "a3bfb76422affc774b44c739b0636d08acb901fa7615b9adcb52c069f79834e1",
|
| 56 |
+
"client.js": "93af3e91c0c5fe56ea0d95c40bb26b3e81aff3d36c38b035b4666a4d0955563a",
|
| 57 |
"delivery.py": "07453999c3831a16e8b1fcc2b4ae5fb28beb7d3952a3fae8de65d16eb6f11e5b",
|
| 58 |
"gpu_tickets.py": "b81aadb5ed42cec9212d44da7e7bbb7b413c5bd517383cac0b49ec9b4d659c92",
|
| 59 |
"hf-auth.js": "d5cf17b961d1f2efa9d4648d33ba86a148eff0d542c74a121710c4df35c92936",
|
| 60 |
"hf_identity.py": "6993d0238ada835606dbdc3275c17a3fe5a5cac5ad2156ab78230acf2d051eba",
|
| 61 |
+
"index.html": "713496757a5fa796cf35eb9fba7a4f29b0eb2f8abdd6c8e3a6fdfd8036a3a30f",
|
| 62 |
"requirements.txt": "9b532747e51bd052e82090ece860d6dafe027407f4512dc74272c148ea104c22",
|
| 63 |
+
"server.py": "af567a30347bbcb75dda324408c47bc2fa8b519693d95c450403fd3012eb8fc5",
|
| 64 |
+
"steam_leaderboard.py": "0f2dbe9615f1bcda2e8f4399a2fa4257ef86a751887dd0b41e0f65034ed3138b",
|
| 65 |
"track-library.js": "351ec4c026b49420ce963f4310cb000d9e987fd28e6dac5ecd0176509340f6e6",
|
| 66 |
"track-map.js": "ab5374aa49c7ccec90a787ee8c56cd71beb56b903bf2bbc322783e6f43c009f0",
|
| 67 |
"training_config.py": "a0dd095e0de8c5a964ace66954f346e5d7b861db3d34cd2feaceedc5ecb80bad",
|
|
|
|
| 71 |
"vendor/hf-oauth/LICENSE": "d6e7b19451c1b3d1d66353bb4b1274138953b3bfd9ae60c56e77b1f548796982",
|
| 72 |
"vendor/hf-oauth/index.js": "53282844000a171011e547f6472a3846bb4cecb5071adafea079fcac3a2aee7e",
|
| 73 |
"vendor/hf-oauth/package.json": "fafd4d8891023a1961546a7d3bc25cd24f64923a2ebfc449fe6d516a2949bda8",
|
| 74 |
+
"worker.py": "2ddf02d3424bba451aa0031dfd9f06d1ec9e210c245ed36dcbe5c79f3732a9ba"
|
| 75 |
},
|
| 76 |
"interface": "single-engine-runner-with-map",
|
| 77 |
"upstream_revision": "d8ab74c25df70b6a86ed101080e33d5fb785ea6f"
|
client.js
CHANGED
|
@@ -13,6 +13,8 @@ let batchStopRequested = false;
|
|
| 13 |
let batchReceipt = null;
|
| 14 |
let runReceipt = null;
|
| 15 |
let appBuild = null;
|
|
|
|
|
|
|
| 16 |
// Display context affects quota guidance, never whether GPU can be selected.
|
| 17 |
const hostedStandalone = location.hostname.endsWith('.hf.space') && window.self === window.top;
|
| 18 |
const say = text => { $('status').textContent = text; };
|
|
@@ -45,6 +47,12 @@ function buttons() {
|
|
| 45 |
if($('batch_export'))$('batch_export').disabled=busy || !batchReceipt;
|
| 46 |
}
|
| 47 |
$('backend').addEventListener('change',buttons);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
const feedbackLink=$('feedbackLink');
|
| 49 |
feedbackLink?.addEventListener('click',()=>{
|
| 50 |
const url=new URL('https://github.com/seeker-cyber-maker/driving_sim_rs/issues/new');
|
|
@@ -87,6 +95,38 @@ window.TrainingClient={
|
|
| 87 |
buttons();
|
| 88 |
}
|
| 89 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
async function submit(body){
|
| 91 |
if(body.backend==='gpu' && gpuTransport==='gradio-ticket'){
|
| 92 |
await verifyGpuIdentity();
|
|
@@ -145,6 +185,8 @@ function showDetails(r){
|
|
| 145 |
'Generation '+h.generation+' · mutation '+metric(h.mutation)+' · best '+metric(h.best_score)+' · mean '+metric(h.mean_score)+' · lap '+(h.best_lap_seconds??'none')).join('\n');
|
| 146 |
}
|
| 147 |
$('run').onclick = async () => {
|
|
|
|
|
|
|
| 148 |
const started=performance.now();
|
| 149 |
$('runState').textContent='RUNNING — waiting for server confirmation';
|
| 150 |
$('runState').style.color='#ffd073';
|
|
@@ -178,6 +220,8 @@ $('run').onclick = async () => {
|
|
| 178 |
'A completed test does not establish improved driving.'
|
| 179 |
].join('\n'):'Server returned a candidate; detailed metrics unavailable.';
|
| 180 |
showDetails(r);
|
|
|
|
|
|
|
| 181 |
$('track').value=''; $('network').value='';
|
| 182 |
stagedNetwork=null;if($('networkChoice'))$('networkChoice').textContent='Network input released after the run.';
|
| 183 |
say('Result ready ('+job.backend.toUpperCase()+'). Inputs and intermediate worker files have been removed.');
|
|
|
|
| 13 |
let batchReceipt = null;
|
| 14 |
let runReceipt = null;
|
| 15 |
let appBuild = null;
|
| 16 |
+
let leaderboardRequest = null;
|
| 17 |
+
let leaderboardGeneration = 0;
|
| 18 |
// Display context affects quota guidance, never whether GPU can be selected.
|
| 19 |
const hostedStandalone = location.hostname.endsWith('.hf.space') && window.self === window.top;
|
| 20 |
const say = text => { $('status').textContent = text; };
|
|
|
|
| 47 |
if($('batch_export'))$('batch_export').disabled=busy || !batchReceipt;
|
| 48 |
}
|
| 49 |
$('backend').addEventListener('change',buttons);
|
| 50 |
+
$('leaderboard_opt_in')?.addEventListener('change',()=>{
|
| 51 |
+
if(!$('leaderboard_opt_in').checked&&leaderboardRequest){
|
| 52 |
+
cancelSteamLeaderboard();
|
| 53 |
+
$('leaderboard_status').textContent='Steam check stopped. The completed run is unaffected.';
|
| 54 |
+
}
|
| 55 |
+
});
|
| 56 |
const feedbackLink=$('feedbackLink');
|
| 57 |
feedbackLink?.addEventListener('click',()=>{
|
| 58 |
const url=new URL('https://github.com/seeker-cyber-maker/driving_sim_rs/issues/new');
|
|
|
|
| 95 |
buttons();
|
| 96 |
}
|
| 97 |
};
|
| 98 |
+
async function checkSteamLeaderboard(trackInfo,report){
|
| 99 |
+
const status=$('leaderboard_status'),optIn=$('leaderboard_opt_in');
|
| 100 |
+
if(!optIn?.checked)return;
|
| 101 |
+
const lap=report?.best_lap_seconds;
|
| 102 |
+
if(typeof lap!=='number'||!Number.isFinite(lap)||lap<=0){status.textContent='Steam check not run: no completed best lap was reported.';return;}
|
| 103 |
+
if(trackInfo?.source!=='builtin'||typeof trackInfo.id!=='string'||typeof trackInfo.sha256!=='string'){
|
| 104 |
+
status.textContent='Steam check unavailable for custom or unidentified tracks. Your run result is unaffected.';return;
|
| 105 |
+
}
|
| 106 |
+
cancelSteamLeaderboard();
|
| 107 |
+
const generation=leaderboardGeneration,controller=new AbortController();
|
| 108 |
+
leaderboardRequest=controller;
|
| 109 |
+
status.textContent='Checking the public Steam board in the background…';
|
| 110 |
+
const timer=setTimeout(()=>controller.abort(),8000);
|
| 111 |
+
try{
|
| 112 |
+
const response=await fetch('/api/leaderboard/compare',{method:'POST',cache:'no-store',credentials:'same-origin',signal:controller.signal,headers:{'Content-Type':'application/json'},body:JSON.stringify({track_id:trackInfo.id,track_sha256:trackInfo.sha256,vehicle_type:0,lap_seconds:lap})});
|
| 113 |
+
const result=await response.json();
|
| 114 |
+
if(generation!==leaderboardGeneration)return;
|
| 115 |
+
if(!response.ok)throw Error('Steam check unavailable');
|
| 116 |
+
if(result.status==='ok'){
|
| 117 |
+
const rank=result.estimated_position?.label||'position unavailable';
|
| 118 |
+
status.textContent='Estimated '+rank+' of '+result.entries+' entries on '+result.board.track_label+' (snapshot '+result.fetched_at_utc+'). '+result.message;
|
| 119 |
+
}else status.textContent=result.message||'Steam check unavailable. Your run result is unaffected.';
|
| 120 |
+
}catch(error){
|
| 121 |
+
if(generation!==leaderboardGeneration)return;
|
| 122 |
+
status.textContent=error?.name==='AbortError'?'Steam check timed out; the completed result is ready to download.':'Steam check unavailable; the completed result is ready to download.';
|
| 123 |
+
}finally{clearTimeout(timer);if(generation===leaderboardGeneration)leaderboardRequest=null;}
|
| 124 |
+
}
|
| 125 |
+
function cancelSteamLeaderboard(){
|
| 126 |
+
leaderboardGeneration++;
|
| 127 |
+
const active=leaderboardRequest;leaderboardRequest=null;
|
| 128 |
+
active?.abort();
|
| 129 |
+
}
|
| 130 |
async function submit(body){
|
| 131 |
if(body.backend==='gpu' && gpuTransport==='gradio-ticket'){
|
| 132 |
await verifyGpuIdentity();
|
|
|
|
| 185 |
'Generation '+h.generation+' · mutation '+metric(h.mutation)+' · best '+metric(h.best_score)+' · mean '+metric(h.mean_score)+' · lap '+(h.best_lap_seconds??'none')).join('\n');
|
| 186 |
}
|
| 187 |
$('run').onclick = async () => {
|
| 188 |
+
cancelSteamLeaderboard();
|
| 189 |
+
if($('leaderboard_status'))$('leaderboard_status').textContent='';
|
| 190 |
const started=performance.now();
|
| 191 |
$('runState').textContent='RUNNING — waiting for server confirmation';
|
| 192 |
$('runState').style.color='#ffd073';
|
|
|
|
| 220 |
'A completed test does not establish improved driving.'
|
| 221 |
].join('\n'):'Server returned a candidate; detailed metrics unavailable.';
|
| 222 |
showDetails(r);
|
| 223 |
+
if($('leaderboard_opt_in')?.checked)void checkSteamLeaderboard(trackInfo,r);
|
| 224 |
+
else if($('leaderboard_status'))$('leaderboard_status').textContent='';
|
| 225 |
$('track').value=''; $('network').value='';
|
| 226 |
stagedNetwork=null;if($('networkChoice'))$('networkChoice').textContent='Network input released after the run.';
|
| 227 |
say('Result ready ('+job.backend.toUpperCase()+'). Inputs and intermediate worker files have been removed.');
|
index.html
CHANGED
|
@@ -44,7 +44,7 @@
|
|
| 44 |
<p><button id="saveLocal" disabled>Save received result again (browser only)</button> <button id="forgetLocal" disabled>Discard browser result</button></p>
|
| 45 |
<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>
|
| 46 |
<h3 id="runState" role="status" aria-live="polite">READY — no run started</h3>
|
| 47 |
-
<pre id="runSummary" style="white-space:pre-wrap;line-height:1.7" aria-live="polite"></pre>
|
| 48 |
<p><button id="run_export" disabled>Export run receipt</button></p><p class="hint">Receipts include build identity, settings, metrics and file hashes. Review them before sharing; they contain no network weights or job access keys.</p>
|
| 49 |
<p id="deadline"></p><p id="status" role="status">Ready. Inputs remain on this device until you run the test.</p>
|
| 50 |
<details><summary>Effective training settings</summary><pre id="effectiveSettings"></pre></details><details><summary>Generation results</summary><pre id="generationHistory"></pre></details><details><summary>Star challenges — not evaluated</summary><p>The current runner has star-challenge reporting. Its evaluator and required statistics have not been ported to HF; no star result is inferred from a completed test.</p></details></section></div>
|
|
@@ -224,7 +224,10 @@
|
|
| 224 |
const network=neural(n),shape=network.Shape;
|
| 225 |
if(!Array.isArray(source.Sensors)||source.Sensors.length!==shape[0]||!outputsValid(source.Outputs)||source.Outputs.length!==shape.at(-1))throw Error('Editing must preserve the sensor/output contract');
|
| 226 |
if(typeof name!=='string'||!name.trim()||name.length>80)throw Error('Network name required (80 characters maximum)');
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
| 228 |
}
|
| 229 |
function fresh({shape,outputs=[0,1,2,3,4],seed,xavier=true,bias=false,name='Fresh network'}){
|
| 230 |
if(!Array.isArray(shape)||shape.length<1||shape.length>11||shape.some(n=>!Number.isInteger(n)||n<1)||shape[0]>20||shape.slice(1).some(n=>n>16))throw Error('Fresh shape: 1–20 inputs and 0–10 hidden layers of 1–16 nodes');
|
|
@@ -529,4 +532,4 @@ document.querySelectorAll('[data-tab]').forEach(button=>button.addEventListener(
|
|
| 529 |
event.preventDefault();try{const {file}=await artifact();if(exportURL)URL.revokeObjectURL(exportURL);exportURL=URL.createObjectURL(file);const a=document.createElement('a');a.href=exportURL;a.download=file.name;a.click();status('Export requested. This is a new edited artifact with no inherited training scores.');}catch(error){status('Not exported: '+error.message);}
|
| 530 |
};
|
| 531 |
})();
|
| 532 |
-
</script></html>
|
|
|
|
| 44 |
<p><button id="saveLocal" disabled>Save received result again (browser only)</button> <button id="forgetLocal" disabled>Discard browser result</button></p>
|
| 45 |
<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>
|
| 46 |
<h3 id="runState" role="status" aria-live="polite">READY — no run started</h3>
|
| 47 |
+
<pre id="runSummary" style="white-space:pre-wrap;line-height:1.7" aria-live="polite"></pre><label class="keep-result"><input id="leaderboard_opt_in" type="checkbox"> After a completed lap, check its position against the public Steam leaderboard</label><p class="privacy-note">Optional, read-only check for supported built-in tracks. It sends the track ID and best lap to this app server; Steam receives only a public leaderboard read. No score is submitted. The result is an estimate, not an official rank.</p><p id="leaderboard_status" role="status" aria-live="polite"></p>
|
| 48 |
<p><button id="run_export" disabled>Export run receipt</button></p><p class="hint">Receipts include build identity, settings, metrics and file hashes. Review them before sharing; they contain no network weights or job access keys.</p>
|
| 49 |
<p id="deadline"></p><p id="status" role="status">Ready. Inputs remain on this device until you run the test.</p>
|
| 50 |
<details><summary>Effective training settings</summary><pre id="effectiveSettings"></pre></details><details><summary>Generation results</summary><pre id="generationHistory"></pre></details><details><summary>Star challenges — not evaluated</summary><p>The current runner has star-challenge reporting. Its evaluator and required statistics have not been ported to HF; no star result is inferred from a completed test.</p></details></section></div>
|
|
|
|
| 224 |
const network=neural(n),shape=network.Shape;
|
| 225 |
if(!Array.isArray(source.Sensors)||source.Sensors.length!==shape[0]||!outputsValid(source.Outputs)||source.Outputs.length!==shape.at(-1))throw Error('Editing must preserve the sensor/output contract');
|
| 226 |
if(typeof name!=='string'||!name.trim()||name.length>80)throw Error('Network name required (80 characters maximum)');
|
| 227 |
+
const idPattern=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
| 228 |
+
const id=typeof source.Id==='string'&&idPattern.test(source.Id)?source.Id:(root.crypto&&typeof root.crypto.randomUUID==='function'?root.crypto.randomUUID():null);
|
| 229 |
+
if(!id)throw Error('Secure GUID generation is unavailable');
|
| 230 |
+
return {Id:id,Name:name.trim(),Type:source.Type,Stats:{Generations:[]},Snapshots:[],Outputs:[...source.Outputs],Sensors:JSON.parse(JSON.stringify(source.Sensors)),NeuralNetwork:network};
|
| 231 |
}
|
| 232 |
function fresh({shape,outputs=[0,1,2,3,4],seed,xavier=true,bias=false,name='Fresh network'}){
|
| 233 |
if(!Array.isArray(shape)||shape.length<1||shape.length>11||shape.some(n=>!Number.isInteger(n)||n<1)||shape[0]>20||shape.slice(1).some(n=>n>16))throw Error('Fresh shape: 1–20 inputs and 0–10 hidden layers of 1–16 nodes');
|
|
|
|
| 532 |
event.preventDefault();try{const {file}=await artifact();if(exportURL)URL.revokeObjectURL(exportURL);exportURL=URL.createObjectURL(file);const a=document.createElement('a');a.href=exportURL;a.download=file.name;a.click();status('Export requested. This is a new edited artifact with no inherited training scores.');}catch(error){status('Not exported: '+error.message);}
|
| 533 |
};
|
| 534 |
})();
|
| 535 |
+
</script></html>
|
server.py
CHANGED
|
@@ -12,16 +12,48 @@ from build_info import build_identity
|
|
| 12 |
from training_config import validate_config
|
| 13 |
from hf_identity import oauth_config
|
| 14 |
from builtin_tracks import TrackLibrary
|
|
|
|
| 15 |
|
| 16 |
ROOT=Path(__file__).resolve().parent
|
| 17 |
|
| 18 |
def create_app(worker=run_reported,store=None,gpu_worker=None,track_library=None):
|
| 19 |
app=create_api(store)
|
| 20 |
admission=threading.BoundedSemaphore(1)
|
|
|
|
| 21 |
library=track_library if track_library is not None else TrackLibrary(ROOT/'builtin_tracks')
|
| 22 |
@app.get('/api/tracks')
|
| 23 |
def track_catalog():
|
| 24 |
return JSONResponse(library.catalog(),headers={'Cache-Control':'no-store'})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
@app.get('/api/tracks/{identifier}/file')
|
| 26 |
def reference_track(identifier:str):
|
| 27 |
try:payload=library.read(identifier)
|
|
|
|
| 12 |
from training_config import validate_config
|
| 13 |
from hf_identity import oauth_config
|
| 14 |
from builtin_tracks import TrackLibrary
|
| 15 |
+
from steam_leaderboard import compare_public_best_lap, milliseconds, SteamUnavailable
|
| 16 |
|
| 17 |
ROOT=Path(__file__).resolve().parent
|
| 18 |
|
| 19 |
def create_app(worker=run_reported,store=None,gpu_worker=None,track_library=None):
|
| 20 |
app=create_api(store)
|
| 21 |
admission=threading.BoundedSemaphore(1)
|
| 22 |
+
leaderboard_admission=threading.BoundedSemaphore(1)
|
| 23 |
library=track_library if track_library is not None else TrackLibrary(ROOT/'builtin_tracks')
|
| 24 |
@app.get('/api/tracks')
|
| 25 |
def track_catalog():
|
| 26 |
return JSONResponse(library.catalog(),headers={'Cache-Control':'no-store'})
|
| 27 |
+
@app.post('/api/leaderboard/compare')
|
| 28 |
+
async def leaderboard_compare(request:Request):
|
| 29 |
+
data=bytearray()
|
| 30 |
+
async for chunk in request.stream():
|
| 31 |
+
if len(data)+len(chunk)>4096: raise HTTPException(413,'Request too large')
|
| 32 |
+
data.extend(chunk)
|
| 33 |
+
try:
|
| 34 |
+
body=json.loads(data)
|
| 35 |
+
if not isinstance(body,dict) or set(body)!={'track_id','track_sha256','vehicle_type','lap_seconds'}:
|
| 36 |
+
raise ValueError()
|
| 37 |
+
if not isinstance(body['track_id'],str) or not isinstance(body['track_sha256'],str): raise ValueError()
|
| 38 |
+
if type(body['vehicle_type']) is not int: raise ValueError()
|
| 39 |
+
milliseconds(body['lap_seconds'])
|
| 40 |
+
except Exception: raise HTTPException(400,'Invalid leaderboard comparison request') from None
|
| 41 |
+
finally: data.clear()
|
| 42 |
+
if body['vehicle_type']!=0:
|
| 43 |
+
return {'status':'unsupported_vehicle','message':'Steam comparison is available only for Formula runs.'}
|
| 44 |
+
entry=next((track for track in library.catalog()['tracks'] if track['id']==body['track_id']),None)
|
| 45 |
+
if entry is None:
|
| 46 |
+
return {'status':'unsupported_track','message':'Steam comparison is available only for supported built-in tracks.'}
|
| 47 |
+
if body['track_sha256']!=entry['sha256']:
|
| 48 |
+
raise HTTPException(400,'Track identity did not match the built-in catalog')
|
| 49 |
+
if not leaderboard_admission.acquire(blocking=False):
|
| 50 |
+
return {'status':'busy','message':'Another Steam check is in progress. The completed result is unaffected.'}
|
| 51 |
+
try:
|
| 52 |
+
return await run_in_threadpool(compare_public_best_lap,entry,body['lap_seconds'])
|
| 53 |
+
except SteamUnavailable:
|
| 54 |
+
return {'status':'unavailable','message':'Steam could not provide a complete public leaderboard. The completed result is unaffected.'}
|
| 55 |
+
finally:
|
| 56 |
+
leaderboard_admission.release()
|
| 57 |
@app.get('/api/tracks/{identifier}/file')
|
| 58 |
def reference_track(identifier:str):
|
| 59 |
try:payload=library.read(identifier)
|
steam_leaderboard.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Anonymous read-only Steam Community leaderboard comparison; never uploads scores."""
|
| 2 |
+
from datetime import datetime, timezone
|
| 3 |
+
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
| 4 |
+
from html.parser import HTMLParser
|
| 5 |
+
import hashlib
|
| 6 |
+
import re
|
| 7 |
+
import threading
|
| 8 |
+
import time
|
| 9 |
+
from urllib.parse import urlparse
|
| 10 |
+
from urllib.request import Request, urlopen
|
| 11 |
+
import xml.etree.ElementTree as ET
|
| 12 |
+
|
| 13 |
+
APP_ID = 3312030
|
| 14 |
+
INDEX_URL = f'https://steamcommunity.com/stats/{APP_ID}/leaderboards/'
|
| 15 |
+
MAX_BYTES = 8_000_000
|
| 16 |
+
TIMEOUT_SECONDS = 4
|
| 17 |
+
TOTAL_TIMEOUT_SECONDS = 10
|
| 18 |
+
INDEX_TTL_SECONDS = 300
|
| 19 |
+
BOARD_TTL_SECONDS = 60
|
| 20 |
+
|
| 21 |
+
_cache_lock = threading.Lock()
|
| 22 |
+
_index_cache = None
|
| 23 |
+
_board_cache = {}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class SteamUnavailable(Exception):
|
| 27 |
+
"""The public Community endpoint could not provide a validated board."""
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class BoardOptions(HTMLParser):
|
| 31 |
+
def __init__(self):
|
| 32 |
+
super().__init__(convert_charrefs=True)
|
| 33 |
+
self.options = {}
|
| 34 |
+
self.value = None
|
| 35 |
+
self.parts = []
|
| 36 |
+
self.duplicate = False
|
| 37 |
+
|
| 38 |
+
def handle_starttag(self, tag, attrs):
|
| 39 |
+
if tag == 'option':
|
| 40 |
+
self.value = dict(attrs).get('value')
|
| 41 |
+
self.parts = []
|
| 42 |
+
|
| 43 |
+
def handle_data(self, data):
|
| 44 |
+
if self.value is not None:
|
| 45 |
+
self.parts.append(data)
|
| 46 |
+
|
| 47 |
+
def handle_endtag(self, tag):
|
| 48 |
+
if tag == 'option' and self.value is not None:
|
| 49 |
+
if self.value in self.options:
|
| 50 |
+
self.duplicate = True
|
| 51 |
+
self.options[self.value] = ''.join(self.parts).strip()
|
| 52 |
+
self.value = None
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _read(url):
|
| 56 |
+
# Callers pass only INDEX_URL or a URL assembled from a numeric ID in that
|
| 57 |
+
# verified public index. No request data is placed in a Steam URL.
|
| 58 |
+
request = Request(url, headers={'User-Agent': 'ALD-hosted-public-board-check/1.0'})
|
| 59 |
+
started = time.monotonic()
|
| 60 |
+
try:
|
| 61 |
+
with urlopen(request, timeout=TIMEOUT_SECONDS) as response:
|
| 62 |
+
final = urlparse(response.geturl())
|
| 63 |
+
if final.scheme != 'https' or final.hostname != 'steamcommunity.com':
|
| 64 |
+
raise SteamUnavailable('Steam public leaderboard redirected outside its fixed host')
|
| 65 |
+
chunks, size = [], 0
|
| 66 |
+
while True:
|
| 67 |
+
if time.monotonic() - started >= TOTAL_TIMEOUT_SECONDS:
|
| 68 |
+
raise SteamUnavailable('Steam public leaderboard request exceeded its time limit')
|
| 69 |
+
chunk = response.read(min(65536, MAX_BYTES + 1 - size))
|
| 70 |
+
if not chunk:
|
| 71 |
+
break
|
| 72 |
+
chunks.append(chunk)
|
| 73 |
+
size += len(chunk)
|
| 74 |
+
if size > MAX_BYTES:
|
| 75 |
+
raise SteamUnavailable('Steam public leaderboard response exceeded the size limit')
|
| 76 |
+
return b''.join(chunks)
|
| 77 |
+
except SteamUnavailable:
|
| 78 |
+
raise
|
| 79 |
+
except Exception as error:
|
| 80 |
+
raise SteamUnavailable('Steam public leaderboard is temporarily unavailable') from error
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _index():
|
| 84 |
+
global _index_cache
|
| 85 |
+
now = time.monotonic()
|
| 86 |
+
with _cache_lock:
|
| 87 |
+
if _index_cache and now - _index_cache[0] < INDEX_TTL_SECONDS:
|
| 88 |
+
return _index_cache[1]
|
| 89 |
+
raw = _read(INDEX_URL)
|
| 90 |
+
parser = BoardOptions()
|
| 91 |
+
try:
|
| 92 |
+
parser.feed(raw.decode('utf-8'))
|
| 93 |
+
except (UnicodeDecodeError, ValueError) as error:
|
| 94 |
+
raise SteamUnavailable('Steam public leaderboard index was invalid') from error
|
| 95 |
+
if parser.duplicate:
|
| 96 |
+
raise SteamUnavailable('Steam public leaderboard index had duplicate IDs')
|
| 97 |
+
boards = {}
|
| 98 |
+
for identifier, label in parser.options.items():
|
| 99 |
+
if identifier.isdecimal() and label:
|
| 100 |
+
if label in boards:
|
| 101 |
+
raise SteamUnavailable('Steam public leaderboard index had ambiguous track labels')
|
| 102 |
+
boards[label] = (int(identifier), label)
|
| 103 |
+
fetched = datetime.now(timezone.utc).isoformat()
|
| 104 |
+
_index_cache = (now, {'boards': boards, 'sha256': hashlib.sha256(raw).hexdigest(), 'fetched_at_utc': fetched})
|
| 105 |
+
return _index_cache[1]
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def _parse_board(raw, board_id):
|
| 109 |
+
try:
|
| 110 |
+
root = ET.fromstring(raw)
|
| 111 |
+
if root.tag != 'response' or root.findtext('appID') != str(APP_ID):
|
| 112 |
+
raise ValueError()
|
| 113 |
+
if root.findtext('leaderboardID') != str(board_id):
|
| 114 |
+
raise ValueError()
|
| 115 |
+
count = int(root.findtext('totalLeaderboardEntries'))
|
| 116 |
+
received = int(root.findtext('resultCount'))
|
| 117 |
+
entries = []
|
| 118 |
+
prior_score = 0
|
| 119 |
+
for row in root.findall('./entries/entry'):
|
| 120 |
+
rank = int(row.findtext('rank'))
|
| 121 |
+
score = int(row.findtext('score'))
|
| 122 |
+
if rank != len(entries) + 1 or score <= 0 or score < prior_score:
|
| 123 |
+
raise ValueError()
|
| 124 |
+
entries.append({'rank': rank, 'score_ms': score})
|
| 125 |
+
prior_score = score
|
| 126 |
+
if count < 1 or received != count or len(entries) != count:
|
| 127 |
+
raise ValueError()
|
| 128 |
+
except (ET.ParseError, TypeError, ValueError, OverflowError) as error:
|
| 129 |
+
raise SteamUnavailable('Steam returned an incomplete or mismatched leaderboard') from error
|
| 130 |
+
return entries
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def _board(label, board_id):
|
| 134 |
+
key = (label, board_id)
|
| 135 |
+
now = time.monotonic()
|
| 136 |
+
with _cache_lock:
|
| 137 |
+
cached = _board_cache.get(key)
|
| 138 |
+
if cached and now - cached[0] < BOARD_TTL_SECONDS:
|
| 139 |
+
return cached[1]
|
| 140 |
+
url = f'{INDEX_URL}{board_id}/?xml=1'
|
| 141 |
+
raw = _read(url)
|
| 142 |
+
entries = _parse_board(raw, board_id)
|
| 143 |
+
value = {
|
| 144 |
+
'entries': entries,
|
| 145 |
+
'fetched_at_utc': datetime.now(timezone.utc).isoformat(),
|
| 146 |
+
'source_sha256': hashlib.sha256(raw).hexdigest(),
|
| 147 |
+
}
|
| 148 |
+
_board_cache[key] = (now, value)
|
| 149 |
+
return value
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def milliseconds(seconds):
|
| 153 |
+
if isinstance(seconds, bool):
|
| 154 |
+
raise ValueError('Lap time must be a positive finite number')
|
| 155 |
+
try:
|
| 156 |
+
value = Decimal(str(seconds))
|
| 157 |
+
except (InvalidOperation, ValueError) as error:
|
| 158 |
+
raise ValueError('Lap time must be a positive finite number') from error
|
| 159 |
+
if not value.is_finite() or value <= 0 or value > 3600:
|
| 160 |
+
raise ValueError('Lap time must be positive and no more than one hour')
|
| 161 |
+
score = int((value * 1000).quantize(Decimal('1'), rounding=ROUND_HALF_UP))
|
| 162 |
+
if score < 1:
|
| 163 |
+
raise ValueError('Lap time must be at least one millisecond')
|
| 164 |
+
return score
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def compare_public_best_lap(track, lap_seconds):
|
| 168 |
+
"""Compare to a public full board; caller has already verified car + track."""
|
| 169 |
+
label = track['reference_label']
|
| 170 |
+
# The original game only assigns campaign leaderboard IDs to A/B tracks.
|
| 171 |
+
# Some R-series labels happen to appear in the public HTML index, but the
|
| 172 |
+
# game source has no verified mapping from those tracks to Steam boards.
|
| 173 |
+
if not isinstance(label, str) or not re.match(r'^[AB]\d{2} ', label):
|
| 174 |
+
return {
|
| 175 |
+
'status': 'unsupported_track',
|
| 176 |
+
'track': {'reference_label': label if isinstance(label, str) else None},
|
| 177 |
+
'source': 'Original game leaderboard mapping',
|
| 178 |
+
'message': 'The original game has no verified Steam leaderboard mapping for this track.',
|
| 179 |
+
'official_rank': False,
|
| 180 |
+
}
|
| 181 |
+
mapping = _index()
|
| 182 |
+
item = mapping['boards'].get(label)
|
| 183 |
+
if not item:
|
| 184 |
+
return {
|
| 185 |
+
'status': 'unsupported_track',
|
| 186 |
+
'track': {'reference_label': label},
|
| 187 |
+
'source': 'Steam Community public index',
|
| 188 |
+
'fetched_at_utc': mapping['fetched_at_utc'],
|
| 189 |
+
'message': 'Steam has no public leaderboard for this built-in track.',
|
| 190 |
+
'official_rank': False,
|
| 191 |
+
}
|
| 192 |
+
board_id, label = item
|
| 193 |
+
data = _board(label, board_id)
|
| 194 |
+
score = milliseconds(lap_seconds)
|
| 195 |
+
entries = data['entries']
|
| 196 |
+
ties = [row['rank'] for row in entries if row['score_ms'] == score]
|
| 197 |
+
if ties:
|
| 198 |
+
lower, upper = min(ties), max(ties)
|
| 199 |
+
else:
|
| 200 |
+
lower = 1 + sum(row['score_ms'] < score for row in entries)
|
| 201 |
+
upper = lower
|
| 202 |
+
exact = lower == upper
|
| 203 |
+
position = f'#{lower}' if exact else f'#{lower}–#{upper}'
|
| 204 |
+
if ties:
|
| 205 |
+
position += ' (displayed-time tie)'
|
| 206 |
+
return {
|
| 207 |
+
'status': 'ok',
|
| 208 |
+
'board': {'app_id': APP_ID, 'leaderboard_id': board_id, 'track_label': label, 'scope': 'global'},
|
| 209 |
+
'source': 'Steam Community public XML',
|
| 210 |
+
'source_url': f'{INDEX_URL}{board_id}/?xml=1',
|
| 211 |
+
'fetched_at_utc': data['fetched_at_utc'],
|
| 212 |
+
'index_sha256': mapping['sha256'],
|
| 213 |
+
'board_sha256': data['source_sha256'],
|
| 214 |
+
'entries': len(entries),
|
| 215 |
+
'lap_milliseconds': score,
|
| 216 |
+
'estimated_position': {'lower': lower, 'upper': upper, 'exact_in_snapshot': exact, 'label': position},
|
| 217 |
+
'ties': ties,
|
| 218 |
+
'rank_basis': 'Insertion position in the complete raw global Steam board at fetch time; not filtered by vehicle metadata.',
|
| 219 |
+
'official_rank': False,
|
| 220 |
+
'message': 'Estimate only: the run was not submitted. This is a snapshot insertion position, not an official Steam rank or verification of game timing parity.',
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def _reset_cache_for_tests():
|
| 225 |
+
global _index_cache
|
| 226 |
+
with _cache_lock:
|
| 227 |
+
_index_cache = None
|
| 228 |
+
_board_cache.clear()
|
worker.py
CHANGED
|
@@ -13,6 +13,7 @@ import subprocess
|
|
| 13 |
import struct
|
| 14 |
import tempfile
|
| 15 |
import time
|
|
|
|
| 16 |
from training_config import validate_config
|
| 17 |
from build_info import build_identity
|
| 18 |
|
|
@@ -95,7 +96,8 @@ def network_input(data):
|
|
| 95 |
not isinstance(biases[i],list) or len(biases[i])!=b): raise ValueError('Dimensions')
|
| 96 |
for n in [x for row in weights[i] for x in row]+biases[i]:
|
| 97 |
if type(n) not in (int,float) or not math.isfinite(n) or abs(n)>1e6: raise ValueError('Weight value')
|
| 98 |
-
return {'
|
|
|
|
| 99 |
'Name':'HF candidate','NeuralNetwork':{'Shape':shape,'InputShape':shape[0],'OutputShape':shape[-1],
|
| 100 |
'NeuronsCount':sum(shape),'TotalParams':sum((a+1)*b for a,b in zip(shape,shape[1:])),
|
| 101 |
'Weights':weights,'Biases':biases}}
|
|
@@ -123,6 +125,13 @@ def native_environment(gpu_library=None):
|
|
| 123 |
env['ALTD_GPU_LIB']=str(library)
|
| 124 |
return env
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
def run_native(track_bytes,network_bytes,*,binary=None,importer=None,scene_template=None,with_report=False,gpu_library=None,config=None):
|
| 127 |
started=time.perf_counter()
|
| 128 |
config=validate_config(config)
|
|
@@ -177,7 +186,7 @@ def run_native(track_bytes,network_bytes,*,binary=None,importer=None,scene_templ
|
|
| 177 |
report={'build':build_identity(),'state':'completed','elapsed_seconds':time.perf_counter()-started,
|
| 178 |
'generations_completed':len(history),'generations_requested':config['generations'],'population':config['population'],
|
| 179 |
'best_score':last.get('best_score'),'mean_score':last.get('mean_score'),
|
| 180 |
-
'best_lap_seconds':
|
| 181 |
'simulated_ticks':last.get('simulated_ticks'),'requested_ticks':config['ticks'],
|
| 182 |
'candidate_validated':True,'execution_backend':'cuda' if gpu_library is not None else 'cpu',
|
| 183 |
'inputs':{'track_sha256':hashlib.sha256(track_bytes).hexdigest(),'network_sha256':hashlib.sha256(network_bytes).hexdigest()},
|
|
|
|
| 13 |
import struct
|
| 14 |
import tempfile
|
| 15 |
import time
|
| 16 |
+
import uuid
|
| 17 |
from training_config import validate_config
|
| 18 |
from build_info import build_identity
|
| 19 |
|
|
|
|
| 96 |
not isinstance(biases[i],list) or len(biases[i])!=b): raise ValueError('Dimensions')
|
| 97 |
for n in [x for row in weights[i] for x in row]+biases[i]:
|
| 98 |
if type(n) not in (int,float) or not math.isfinite(n) or abs(n)>1e6: raise ValueError('Weight value')
|
| 99 |
+
return {'Id':str(uuid.uuid4()),'Stats':{'Generations':[]},'Snapshots':[],
|
| 100 |
+
'Type':0,'Outputs':outputs[:],'Sensors':clean_sensors,
|
| 101 |
'Name':'HF candidate','NeuralNetwork':{'Shape':shape,'InputShape':shape[0],'OutputShape':shape[-1],
|
| 102 |
'NeuronsCount':sum(shape),'TotalParams':sum((a+1)*b for a,b in zip(shape,shape[1:])),
|
| 103 |
'Weights':weights,'Biases':biases}}
|
|
|
|
| 125 |
env['ALTD_GPU_LIB']=str(library)
|
| 126 |
return env
|
| 127 |
|
| 128 |
+
def best_run_lap(history):
|
| 129 |
+
# Native best_lap_s is generation-local. Include the independently logged
|
| 130 |
+
# all-time value and prior generations, never a non-finite sentinel.
|
| 131 |
+
values = [row.get(key) for row in history for key in ('best_lap_s', 'all_time_best_lap_s')]
|
| 132 |
+
laps = [v for v in values if type(v) in (int, float) and math.isfinite(v) and v > 0]
|
| 133 |
+
return min(laps) if laps else None
|
| 134 |
+
|
| 135 |
def run_native(track_bytes,network_bytes,*,binary=None,importer=None,scene_template=None,with_report=False,gpu_library=None,config=None):
|
| 136 |
started=time.perf_counter()
|
| 137 |
config=validate_config(config)
|
|
|
|
| 186 |
report={'build':build_identity(),'state':'completed','elapsed_seconds':time.perf_counter()-started,
|
| 187 |
'generations_completed':len(history),'generations_requested':config['generations'],'population':config['population'],
|
| 188 |
'best_score':last.get('best_score'),'mean_score':last.get('mean_score'),
|
| 189 |
+
'best_lap_seconds':best_run_lap(history),'best_lap_scope':'Best completed lap observed during this run; may belong to a different car than the exported candidate','lapped_cars':last.get('lapped_cars'),
|
| 190 |
'simulated_ticks':last.get('simulated_ticks'),'requested_ticks':config['ticks'],
|
| 191 |
'candidate_validated':True,'execution_backend':'cuda' if gpu_library is not None else 'cpu',
|
| 192 |
'inputs':{'track_sha256':hashlib.sha256(track_bytes).hexdigest(),'network_sha256':hashlib.sha256(network_bytes).hexdigest()},
|