TiGa-RCE commited on
Commit
a6e1920
·
verified ·
1 Parent(s): 928b9a7

Fix original-game export loading and add opt-in Steam comparison

Browse files

Keep 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.

Files changed (8) hide show
  1. DATA_POLICY.md +9 -0
  2. FRONTEND_SOURCE.json +3 -3
  3. RELEASE_MANIFEST.json +7 -6
  4. client.js +44 -0
  5. index.html +6 -3
  6. server.py +32 -0
  7. steam_leaderboard.py +228 -0
  8. 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": "182f8d8b5b5393a28d259aec2dbe9003fc356e4ff817b0e6c15a9f1be004dbf1",
5
  "scope": "Frontend only; no engine, private data, track assets, API calls or storage copied",
6
- "status": "Five-output and full vanilla-network candidate; hosted qualification recorded separately",
7
  "track_atlas_sha256": "d97c8900e3e5fb17734b5bd61d1f4f4b196d9be30af9848e2e273ae7e4079ae4",
8
- "client_sha256": "b6c2e0439bc1e4cf55c5e751e497e842e1e911b229f7480d7b1e04dea246536f",
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": "38da17f8528ede1ef1850e98832f75ba35ebdc86a0cde87e27674eb0ff9c7a7f",
9
  "FEEDBACK.md": "f95b8f6afbf4a360976c0edca5d52f5b3a0b668c9a944d4947b840a7c53a122a",
10
- "FRONTEND_SOURCE.json": "0c478aac6c58f387b468d0c758bfdd16b461e033064d8b637b64fb3fcbe87f1b",
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": "b6c2e0439bc1e4cf55c5e751e497e842e1e911b229f7480d7b1e04dea246536f",
57
  "delivery.py": "07453999c3831a16e8b1fcc2b4ae5fb28beb7d3952a3fae8de65d16eb6f11e5b",
58
  "gpu_tickets.py": "b81aadb5ed42cec9212d44da7e7bbb7b413c5bd517383cac0b49ec9b4d659c92",
59
  "hf-auth.js": "d5cf17b961d1f2efa9d4648d33ba86a148eff0d542c74a121710c4df35c92936",
60
  "hf_identity.py": "6993d0238ada835606dbdc3275c17a3fe5a5cac5ad2156ab78230acf2d051eba",
61
- "index.html": "182f8d8b5b5393a28d259aec2dbe9003fc356e4ff817b0e6c15a9f1be004dbf1",
62
  "requirements.txt": "9b532747e51bd052e82090ece860d6dafe027407f4512dc74272c148ea104c22",
63
- "server.py": "de98db424b96a4fbab7ce5b98ee42a94e846e2b3f317593e2346c5f87489ede8",
 
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": "58bc6ba0a96e9d0b02af04c4d87df33cf973a44fdd7e67d8d4efe0b6d67cf0fe"
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
- return {Name:name.trim(),Type:source.Type,Outputs:[...source.Outputs],Sensors:JSON.parse(JSON.stringify(source.Sensors)),NeuralNetwork:network};
 
 
 
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 {'Type':0,'Outputs':outputs[:],'Sensors':clean_sensors,
 
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':last.get('best_lap_s'),'lapped_cars':last.get('lapped_cars'),
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()},