@inventure71/paddockjs 4.2.0 → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/INSTALL_AND_UPDATE.md +24 -4
  3. package/README.md +31 -2
  4. package/docs/advanced_ai_pace.md +131 -0
  5. package/docs/advanced_physics_rebuild.md +166 -0
  6. package/docs/architecture.md +109 -30
  7. package/docs/cleanup_coverage.md +284 -0
  8. package/docs/component_inventory.md +31 -15
  9. package/docs/composable-layouts.md +2 -0
  10. package/docs/concepts.md +12 -1
  11. package/docs/custom_model_controller.md +1 -1
  12. package/docs/data_contract.md +33 -10
  13. package/docs/data_model.md +4 -2
  14. package/docs/efficiency_cleanup_2026-10-13.md +80 -0
  15. package/docs/grid_walk.md +84 -0
  16. package/docs/index.md +1 -0
  17. package/docs/keyboard_driving_fix.md +118 -0
  18. package/docs/limit_driving_2026-10-14.md +88 -0
  19. package/docs/maintainability_audit_2026-09-08.md +146 -0
  20. package/docs/racing_ai_improvements.md +145 -0
  21. package/docs/rules.md +43 -13
  22. package/docs/sense_contract.md +4 -1
  23. package/docs/system_specs.md +38 -11
  24. package/docs/testing-reliability-guide.md +5 -0
  25. package/docs/v11_improvements.md +108 -0
  26. package/package.json +24 -13
  27. package/src/api/MountedSimulatorComponents.js +101 -0
  28. package/src/api/PaddockSimulatorController.js +56 -81
  29. package/src/app/BrowserExpertAdapter.js +0 -6
  30. package/src/app/BrowserExpertExternalRenderer.js +72 -50
  31. package/src/app/ExternalTrackSnapshot.js +50 -0
  32. package/src/app/F1SimulatorApp.js +242 -297
  33. package/src/app/HostLifecycleEmitter.js +93 -0
  34. package/src/app/banners/raceDataBannerController.js +9 -4
  35. package/src/app/camera/cameraConstants.js +1 -1
  36. package/src/app/camera/cameraController.js +10 -13
  37. package/src/app/camera/cameraGeometry.js +1 -1
  38. package/src/app/controls/SimulatorViewControls.js +45 -0
  39. package/src/app/controls/workbenchBindings.js +64 -0
  40. package/src/app/controls/workbenchDisclosure.js +41 -0
  41. package/src/app/domBindings.js +21 -13
  42. package/src/app/gridWalk/GridWalkController.js +180 -0
  43. package/src/app/gridWalk/gridWalkCamera.js +88 -0
  44. package/src/app/gridWalk/gridWalkControls.js +24 -0
  45. package/src/app/gridWalk/gridWalkCrew.js +246 -0
  46. package/src/app/gridWalk/gridWalkLabels.js +104 -0
  47. package/src/app/gridWalk/gridWalkLayout.js +46 -0
  48. package/src/app/gridWalk/gridWalkNavigation.js +106 -0
  49. package/src/app/gridWalk/gridWalkProjectBanner.js +43 -0
  50. package/src/app/gridWalk/gridWalkSession.js +50 -0
  51. package/src/app/layoutSupport.js +2 -0
  52. package/src/app/raceOverlayClearanceSupport.js +21 -7
  53. package/src/app/readouts/carOverviewRenderer.js +9 -4
  54. package/src/app/readouts/readoutFormatters.js +1 -8
  55. package/src/app/readouts/telemetryRenderer.js +1 -3
  56. package/src/app/readouts/timingTowerRenderer.js +48 -50
  57. package/src/app/rendering/appAssets.js +8 -10
  58. package/src/app/rendering/carRenderer.js +27 -79
  59. package/src/app/rendering/displayUtils.js +0 -6
  60. package/src/app/rendering/expertSensorRenderer.js +1 -1
  61. package/src/app/rendering/replayGhostRenderer.js +4 -1
  62. package/src/app/rendering/trackRenderer.js +2 -2
  63. package/src/app/runtime/frameLoop.js +18 -17
  64. package/src/app/runtime/pixiStartupLifecycle.js +94 -0
  65. package/src/app/runtime/runtimeTiming.js +4 -1
  66. package/src/app/runtime/runtimeVisibility.js +7 -2
  67. package/src/config/cssValues.js +1 -1
  68. package/src/config/defaultOptions.js +12 -173
  69. package/src/config/restartOptions.js +1 -0
  70. package/src/config/runtimeSeed.js +10 -0
  71. package/src/config/simulatorDefaults.js +106 -0
  72. package/src/config/telemetryModules.js +20 -0
  73. package/src/config/theme/cssTokens.js +85 -0
  74. package/src/config/theme/themeCss.js +72 -0
  75. package/src/config/theme/themeInputs.js +155 -0
  76. package/src/config/theme/themeResolver.js +172 -0
  77. package/src/config/theme/themeSelectors.js +69 -0
  78. package/src/config/theme/themeTokens.js +257 -0
  79. package/src/config/theme/themeUtils.js +15 -0
  80. package/src/config/themeOptions.js +14 -830
  81. package/src/config/timingGapMode.js +0 -2
  82. package/src/config/uiOptions.js +53 -0
  83. package/src/data/championship.js +16 -158
  84. package/src/data/championshipDefaults.js +87 -0
  85. package/src/data/driverData.js +10 -20
  86. package/src/data/entryIdentity.js +67 -0
  87. package/src/data/entryPresentation.js +23 -0
  88. package/src/data/entryTypes.d.ts +160 -0
  89. package/src/data/index.d.ts +13 -259
  90. package/src/data/ratingScale.js +14 -0
  91. package/src/data/teamTypes.d.ts +23 -0
  92. package/src/data/trackTypes.d.ts +101 -0
  93. package/src/data/vehicleData.js +12 -23
  94. package/src/environment/actions.js +3 -2
  95. package/src/environment/controllerLoop.js +57 -44
  96. package/src/environment/controllerScheduler.js +30 -0
  97. package/src/environment/driverObservations.js +260 -0
  98. package/src/environment/driverReset.js +51 -0
  99. package/src/environment/episode.js +0 -1
  100. package/src/environment/evaluation.js +6 -90
  101. package/src/environment/evaluationTracker.js +100 -0
  102. package/src/environment/events.js +23 -0
  103. package/src/environment/index.d.ts +21 -140
  104. package/src/environment/metrics.js +3 -17
  105. package/src/environment/observationSchema.js +104 -0
  106. package/src/environment/observationVector.js +16 -115
  107. package/src/environment/observations.js +16 -316
  108. package/src/environment/options.js +5 -24
  109. package/src/environment/renderFrames.js +21 -0
  110. package/src/environment/resetOptions.js +41 -0
  111. package/src/environment/results.js +125 -0
  112. package/src/environment/rewards.js +10 -16
  113. package/src/environment/runtime.js +17 -300
  114. package/src/environment/scenarioOptions.js +90 -0
  115. package/src/environment/scenarioPlacements.js +70 -0
  116. package/src/environment/scenarioPresets.js +78 -0
  117. package/src/environment/scenarios.js +5 -236
  118. package/src/environment/scratch.js +18 -0
  119. package/src/environment/sensorOptions.js +20 -0
  120. package/src/environment/sensors/index.js +2 -556
  121. package/src/environment/sensors/indexedRayBands.js +0 -18
  122. package/src/environment/sensors/nearbyCars.js +18 -15
  123. package/src/environment/sensors/opponentRadar.js +3 -2
  124. package/src/environment/sensors/pitConnectorProximity.js +20 -0
  125. package/src/environment/sensors/rayBandTrace.js +1 -37
  126. package/src/environment/sensors/rayBatch.js +81 -0
  127. package/src/environment/sensors/rayConfig.js +26 -0
  128. package/src/environment/sensors/rayGeometry.js +0 -8
  129. package/src/environment/sensors/rayGuards.js +0 -6
  130. package/src/environment/sensors/rayOutputs.js +75 -0
  131. package/src/environment/sensors/rayQuery.js +110 -0
  132. package/src/environment/sensors/rayScratch.js +146 -0
  133. package/src/environment/sensors/sensorTargets.js +12 -4
  134. package/src/environment/sensors/surfaceRays.js +6 -61
  135. package/src/environment/sensors/trackRays.js +7 -80
  136. package/src/environment/simulationSetup.js +30 -0
  137. package/src/environment/specs.js +15 -109
  138. package/src/index.d.ts +34 -259
  139. package/src/index.js +28 -6
  140. package/src/placeholder/index.js +1 -8
  141. package/src/rendering/displayLifecycle.js +6 -0
  142. package/src/rendering/proceduralTrackAsset.js +1 -1
  143. package/src/rendering/renderSnapshot.js +3 -0
  144. package/src/rendering/track/finishLineRenderer.js +1 -1
  145. package/src/rendering/track/gridRenderer.js +1 -1
  146. package/src/rendering/track/offsetStrokeSafety.js +2 -2
  147. package/src/rendering/track/pitLaneRenderer.js +1 -1
  148. package/src/rendering/track/trackMaterialRenderer.js +4 -4
  149. package/src/rendering/track/trackRenderConstants.js +0 -1
  150. package/src/rendering/track/trackRenderGeometry.js +1 -7
  151. package/src/simulation/collisionGeometry.js +5 -11
  152. package/src/simulation/driver/advancedPathControls.js +53 -0
  153. package/src/simulation/driver/advancedRacePace.js +112 -0
  154. package/src/simulation/driver/advancedRacingControls.js +61 -0
  155. package/src/simulation/driver/advancedRacingEnvelope.js +255 -0
  156. package/src/simulation/driver/advancedRacingSteering.js +76 -0
  157. package/src/simulation/driver/advancedRacingTiming.js +16 -0
  158. package/src/simulation/driver/arcadeRacePace.js +83 -0
  159. package/src/simulation/driver/arcadeRacingControls.js +111 -0
  160. package/src/simulation/driver/arcadeRacingEnvelope.js +63 -0
  161. package/src/simulation/driver/driverControlConstants.js +0 -2
  162. package/src/simulation/driver/driverController.js +14 -3
  163. package/src/simulation/driver/edgeRecovery.js +0 -1
  164. package/src/simulation/driver/racingControls.js +10 -406
  165. package/src/simulation/driver/racingDriverProfile.js +48 -0
  166. package/src/simulation/driver/racingLinePlan.js +48 -15
  167. package/src/simulation/driver/rejoinControls.js +58 -111
  168. package/src/simulation/driver/safetyCarControls.js +12 -0
  169. package/src/simulation/driver/trafficScan.js +89 -6
  170. package/src/simulation/pit/pitCarPlacement.js +29 -0
  171. package/src/simulation/pit/pitFlow.js +8 -30
  172. package/src/simulation/pit/pitIntent.js +4 -3
  173. package/src/simulation/pit/pitOccupancy.js +2 -2
  174. package/src/simulation/pit/pitQueue.js +2 -18
  175. package/src/simulation/pit/pitRouteMovement.js +1 -36
  176. package/src/simulation/pit/pitRouting.js +5 -81
  177. package/src/simulation/pit/pitService.js +3 -32
  178. package/src/simulation/pit/pitServiceConstants.js +1 -0
  179. package/src/simulation/pit/pitServiceProgress.js +34 -0
  180. package/src/simulation/pit/pitSnapshots.js +8 -4
  181. package/src/simulation/pit/pitState.js +1 -5
  182. package/src/simulation/race/classification.js +1 -8
  183. package/src/simulation/race/raceFinish.js +3 -8
  184. package/src/simulation/race/raceOrder.js +2 -13
  185. package/src/simulation/race/raceProgress.js +6 -20
  186. package/src/simulation/race/raceSetup.js +1 -1
  187. package/src/simulation/race/raceStep.js +1 -1
  188. package/src/simulation/race/redFlag.js +2 -4
  189. package/src/simulation/race/safetyCar.js +3 -8
  190. package/src/simulation/race/stalledDnf.js +2 -4
  191. package/src/simulation/raceSimulation.js +6 -38
  192. package/src/simulation/replay/replayGhosts.js +44 -32
  193. package/src/simulation/rules/collisionSteward.js +0 -14
  194. package/src/simulation/rules/penaltyActions.js +37 -0
  195. package/src/simulation/rules/penaltyLedger.js +8 -1
  196. package/src/simulation/rules/rulesReview.js +1 -7
  197. package/src/simulation/simMath.js +4 -0
  198. package/src/simulation/snapshots/raceSnapshots.js +30 -11
  199. package/src/simulation/timing/drsState.js +8 -0
  200. package/src/simulation/timing/drsTiming.js +2 -4
  201. package/src/simulation/timing/lapTelemetry.js +7 -4
  202. package/src/simulation/timing/sectorPerformance.js +3 -3
  203. package/src/simulation/timing/timingLines.js +12 -6
  204. package/src/simulation/track/pitLaneAccess.js +2 -2
  205. package/src/simulation/track/pitLaneLayout.js +4 -135
  206. package/src/simulation/track/pitLaneModel.js +70 -0
  207. package/src/simulation/track/pitLaneState.js +0 -1
  208. package/src/simulation/track/pitServiceLayout.js +121 -0
  209. package/src/simulation/track/proceduralTrack.js +77 -0
  210. package/src/simulation/track/spatialQueries.js +1 -25
  211. package/src/simulation/track/trackBuilder.js +54 -0
  212. package/src/simulation/track/trackConstants.js +0 -22
  213. package/src/simulation/track/trackMath.js +4 -29
  214. package/src/simulation/track/trackModel.js +4 -241
  215. package/src/simulation/track/trackModelCache.js +105 -0
  216. package/src/simulation/track/trackNearestQueries.js +1218 -0
  217. package/src/simulation/track/trackQueryConstants.js +3 -0
  218. package/src/simulation/track/trackQueryConstruction.js +368 -0
  219. package/src/simulation/track/trackQueryIndex.js +12 -1933
  220. package/src/simulation/track/trackQuerySegments.js +174 -0
  221. package/src/simulation/track/trackQueryStats.js +12 -0
  222. package/src/simulation/track/trackRayQueries.js +165 -0
  223. package/src/simulation/vehicle/advancedContactResponse.js +89 -0
  224. package/src/simulation/vehicle/advancedTireForces.js +92 -0
  225. package/src/simulation/vehicle/advancedVehicleModel.js +65 -0
  226. package/src/simulation/vehicle/advancedVehiclePhysics.js +127 -0
  227. package/src/simulation/vehicle/contactResolution.js +20 -6
  228. package/src/simulation/vehicle/mainTrackWheelSurface.js +2 -13
  229. package/src/simulation/vehicle/pitWheelSurface.js +0 -12
  230. package/src/simulation/vehicle/runoffResponse.js +2 -4
  231. package/src/simulation/vehicle/sampledWheelSurface.js +266 -0
  232. package/src/simulation/vehicle/vehicleGeometry.js +0 -56
  233. package/src/simulation/vehicle/vehicleKinematics.js +10 -0
  234. package/src/simulation/vehicle/vehiclePhysics.js +35 -194
  235. package/src/simulation/vehicle/vehicleSnapshots.js +13 -4
  236. package/src/simulation/vehicle/vehicleState.js +2 -1
  237. package/src/simulation/vehicle/wheelSurface.js +13 -432
  238. package/src/simulation/vehicle/wheelSurfaceCache.js +55 -0
  239. package/src/simulation/vehicle/wheelSurfaceSummary.js +101 -0
  240. package/src/styles.css +173 -19
  241. package/src/ui/bannerTemplates.js +4 -2
  242. package/src/ui/cameraControlsTemplate.js +4 -2
  243. package/src/ui/gridWalkTemplate.js +29 -0
  244. package/src/ui/htmlEscaping.js +8 -0
  245. package/src/ui/shellTemplate.js +3 -0
  246. package/src/ui/telemetryDrawerTemplate.js +4 -2
  247. package/src/ui/telemetryTemplates.js +3 -22
  248. package/src/ui/templateUtils.js +2 -8
  249. package/src/ui/viewControlsTemplate.js +21 -0
@@ -1,3 +1,5 @@
1
+ import { cancelScheduled, defaultScheduler, now } from './controllerScheduler.js';
2
+
1
3
  export function createPaddockDriverControllerLoop({
2
4
  runtime,
3
5
  controller,
@@ -28,6 +30,8 @@ export function createPaddockDriverControllerLoop({
28
30
  let previousActions = {};
29
31
  let lastDecisionMs = 0;
30
32
  let lastError = null;
33
+ let generation = 0;
34
+ let generationController = new AbortController();
31
35
 
32
36
  async function ensureSpecs() {
33
37
  if (!actionSpec) actionSpec = runtime.getActionSpec();
@@ -39,14 +43,24 @@ export function createPaddockDriverControllerLoop({
39
43
  observationSpec = runtime.getObservationSpec();
40
44
  }
41
45
 
46
+ async function runControllerHook(name, context) {
47
+ try {
48
+ return await controller[name]?.(context);
49
+ } catch (error) {
50
+ if (context.signal.aborted) return;
51
+ throw error;
52
+ }
53
+ }
54
+
42
55
  async function ensureInitialized(context) {
43
56
  if (initialized) return;
44
- await controller.init?.(context);
45
- initialized = true;
57
+ await runControllerHook('init', context);
58
+ if (!context.signal.aborted) initialized = true;
46
59
  }
47
60
 
48
61
  async function resetRuntime(options = {}, { stopPlayback = true } = {}) {
49
62
  if (stopPlayback) stop();
63
+ const resetGeneration = generation;
50
64
  result = runtime.reset(options);
51
65
  previousResult = null;
52
66
  heldActions = null;
@@ -57,9 +71,11 @@ export function createPaddockDriverControllerLoop({
57
71
  previousActions = {};
58
72
  lastError = null;
59
73
  await refreshSpecs();
74
+ if (generation !== resetGeneration) return result;
60
75
  const context = buildContext({ resetDriverIds: controlledDrivers() });
61
76
  await ensureInitialized(context);
62
- await controller.reset?.(context);
77
+ if (generation !== resetGeneration) return result;
78
+ await runControllerHook('reset', context);
63
79
  return result;
64
80
  }
65
81
 
@@ -75,6 +91,9 @@ export function createPaddockDriverControllerLoop({
75
91
  if (typeof runtime.resetDrivers !== 'function') {
76
92
  throw new Error('The supplied Paddock runtime does not support resetDrivers().');
77
93
  }
94
+ const resumePlayback = running;
95
+ stop();
96
+ const resetGeneration = generation;
78
97
  const resetDriverIds = Object.keys(placements);
79
98
  previousResult = result;
80
99
  result = runtime.resetDrivers(placements, resultOptions);
@@ -89,29 +108,39 @@ export function createPaddockDriverControllerLoop({
89
108
  delete previousActions[driverId];
90
109
  });
91
110
  await ensureSpecs();
111
+ if (generation !== resetGeneration) return result;
92
112
  const context = buildContext({ resetDriverIds });
93
113
  await ensureInitialized(context);
94
- await controller.reset?.(context);
114
+ if (generation !== resetGeneration) return result;
115
+ await runControllerHook('reset', context);
116
+ if (resumePlayback && generation === resetGeneration) start();
95
117
  return result;
96
118
  }
97
119
 
98
- async function beginDecision() {
120
+ async function beginDecision(expectedGeneration) {
99
121
  await ensureResult();
122
+ if (generation !== expectedGeneration) return false;
100
123
  const context = buildContext();
101
124
  await ensureInitialized(context);
125
+ if (generation !== expectedGeneration) return false;
102
126
  const startedAt = now();
103
- const actions = await controller.decideBatch(context);
127
+ const actions = await runControllerHook('decideBatch', context);
128
+ if (generation !== expectedGeneration) return false;
104
129
  lastDecisionMs = now() - startedAt;
105
130
  heldActions = actions && typeof actions === 'object' ? actions : {};
106
131
  heldFramesRemaining = repeat;
107
132
  policyStep += 1;
133
+ return true;
108
134
  }
109
135
 
110
- async function stepFrame() {
136
+ async function stepFrame(expectedGeneration = generation) {
111
137
  await ensureResult();
138
+ if (generation !== expectedGeneration) return result;
112
139
  if (!heldActions || heldFramesRemaining <= 0) {
113
- await beginDecision();
140
+ const decisionReady = await beginDecision(expectedGeneration);
141
+ if (!decisionReady) return result;
114
142
  }
143
+ if (generation !== expectedGeneration) return result;
115
144
  const actionIndex = repeat - heldFramesRemaining;
116
145
  const actions = heldActions;
117
146
  const previousActionsForStep = previousActions;
@@ -121,7 +150,8 @@ export function createPaddockDriverControllerLoop({
121
150
  heldFramesRemaining -= 1;
122
151
  runtimeStep = Number(result?.info?.step ?? runtimeStep + 1);
123
152
  previousActions = actions;
124
- await controller.onStep?.(buildContext({
153
+ if (generation !== expectedGeneration) return result;
154
+ await runControllerHook('onStep', buildContext({
125
155
  actionIndex,
126
156
  actions,
127
157
  previousActions: previousActionsForStep,
@@ -133,8 +163,10 @@ export function createPaddockDriverControllerLoop({
133
163
  async function step() {
134
164
  let latest = result;
135
165
  const targetPolicyStep = policyStep + 1;
166
+ const expectedGeneration = generation;
136
167
  while (policyStep < targetPolicyStep || heldFramesRemaining > 0) {
137
- latest = await stepFrame();
168
+ latest = await stepFrame(expectedGeneration);
169
+ if (generation !== expectedGeneration) break;
138
170
  if (latest?.done) break;
139
171
  if (policyStep >= targetPolicyStep && heldFramesRemaining <= 0) break;
140
172
  }
@@ -145,18 +177,21 @@ export function createPaddockDriverControllerLoop({
145
177
  if (running) return;
146
178
  lastError = null;
147
179
  running = true;
180
+ const expectedGeneration = generation;
148
181
  const schedule = scheduler ?? defaultScheduler();
149
182
  const tick = async () => {
150
- if (!running) return;
183
+ if (!running || generation !== expectedGeneration) return;
151
184
  scheduled = null;
152
185
  try {
153
- await stepFrame();
186
+ await stepFrame(expectedGeneration);
154
187
  } catch (error) {
188
+ if (generation !== expectedGeneration) return;
155
189
  lastError = error;
156
190
  running = false;
157
191
  scheduled = null;
158
192
  return;
159
193
  }
194
+ if (generation !== expectedGeneration) return;
160
195
  if (!running || result?.done) {
161
196
  running = false;
162
197
  return;
@@ -167,9 +202,17 @@ export function createPaddockDriverControllerLoop({
167
202
  }
168
203
 
169
204
  function stop() {
205
+ const previousController = generationController;
206
+ const previousSchedule = scheduled;
170
207
  running = false;
171
- cancelScheduled(scheduled);
208
+ generation += 1;
209
+ generationController = new AbortController();
172
210
  scheduled = null;
211
+ try {
212
+ cancelScheduled(previousSchedule);
213
+ } finally {
214
+ previousController.abort();
215
+ }
173
216
  }
174
217
 
175
218
  function controlledDrivers() {
@@ -184,6 +227,7 @@ export function createPaddockDriverControllerLoop({
184
227
  };
185
228
  return {
186
229
  runtime,
230
+ signal: generationController.signal,
187
231
  mode,
188
232
  actionRepeat: repeat,
189
233
  controlledDrivers: drivers,
@@ -242,34 +286,3 @@ export function createPaddockDriverControllerLoop({
242
286
  },
243
287
  };
244
288
  }
245
-
246
- function now() {
247
- return typeof performance !== 'undefined' && typeof performance.now === 'function'
248
- ? performance.now()
249
- : Date.now();
250
- }
251
-
252
- function cancelScheduled(handle) {
253
- if (handle == null) return;
254
- if (typeof handle.cancel === 'function') {
255
- handle.cancel();
256
- return;
257
- }
258
- if (typeof handle === 'number') {
259
- if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(handle);
260
- if (typeof clearTimeout === 'function') clearTimeout(handle);
261
- }
262
- }
263
-
264
- function defaultScheduler() {
265
- if (typeof requestAnimationFrame === 'function') {
266
- return (callback) => {
267
- const id = requestAnimationFrame(callback);
268
- return { cancel: () => cancelAnimationFrame(id) };
269
- };
270
- }
271
- return (callback) => {
272
- const id = setTimeout(callback, 0);
273
- return { cancel: () => clearTimeout(id) };
274
- };
275
- }
@@ -0,0 +1,30 @@
1
+ export function now() {
2
+ return typeof performance !== 'undefined' && typeof performance.now === 'function'
3
+ ? performance.now()
4
+ : Date.now();
5
+ }
6
+
7
+ export function cancelScheduled(handle) {
8
+ if (handle == null) return;
9
+ if (typeof handle.cancel === 'function') {
10
+ handle.cancel();
11
+ return;
12
+ }
13
+ if (typeof handle === 'number') {
14
+ if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(handle);
15
+ if (typeof clearTimeout === 'function') clearTimeout(handle);
16
+ }
17
+ }
18
+
19
+ export function defaultScheduler() {
20
+ if (typeof requestAnimationFrame === 'function') {
21
+ return (callback) => {
22
+ const id = requestAnimationFrame(callback);
23
+ return { cancel: () => cancelAnimationFrame(id) };
24
+ };
25
+ }
26
+ return (callback) => {
27
+ const id = setTimeout(callback, 0);
28
+ return { cancel: () => clearTimeout(id) };
29
+ };
30
+ }
@@ -0,0 +1,260 @@
1
+ import { metersToSimUnits, simUnitsToMeters } from '../simulation/units.js';
2
+ import { normalizeAngle } from '../simulation/simMath.js';
3
+ import { pointAt } from '../simulation/track/trackModel.js';
4
+ import { normalizeLookaheadMeters } from './observationOptions.js';
5
+ import { buildBodySenses } from './sensors/bodySenses.js';
6
+ import { buildBoundarySenses } from './sensors/boundarySenses.js';
7
+ import { buildContactPatchSenses } from './sensors/contactSenses.js';
8
+ import { enrichOpponentRadar } from './sensors/opponentRadar.js';
9
+ import { buildNearbyCars, appendRaySensorVectorValues, buildRaySensors } from './sensors.js';
10
+ import { buildObservationVector } from './observationVector.js';
11
+
12
+ export function buildDriverObservation(car, snapshot, options, events, sensors, sensorContexts) {
13
+ const output = options.observation?.output ?? 'full';
14
+ const includeSchema = options.observation?.includeSchema !== false;
15
+ const wantsObject = output === 'full' || output === 'object';
16
+ const wantsVector = output === 'full' || output === 'vector';
17
+ const object = wantsObject
18
+ ? buildDriverObservationObject(car, snapshot, options, events, sensors, sensorContexts)
19
+ : null;
20
+ let vector;
21
+ let schema = [];
22
+ if (wantsVector) {
23
+ const encoded = !object && !includeSchema
24
+ ? buildDriverVectorDirect(car, snapshot, options, sensors, sensorContexts)
25
+ : buildObservationVector(
26
+ object ?? buildDriverObservationObject(car, snapshot, options, events, sensors, sensorContexts),
27
+ sensors,
28
+ { includeSchema, vectorType: options.observation?.vectorType },
29
+ );
30
+ vector = encoded.vector;
31
+ schema = encoded.schema;
32
+ }
33
+ return { object, vector, schema, events };
34
+ }
35
+
36
+ function buildDriverNearbyCars(car, snapshot, sensors, sensorContexts) {
37
+ if (!sensors.nearbyCars.enabled) return [];
38
+ const context = sensorContexts?.getNearbyBatchContext();
39
+ return enrichOpponentRadar(car, buildNearbyCars(car, snapshot, sensors.nearbyCars, context), snapshot, context);
40
+ }
41
+
42
+ function buildDriverObservationObject(car, snapshot, options, events, sensors, sensorContexts = null) {
43
+ const sensorCar = withEnvironmentControlFlag(car, options);
44
+ const onTrack = isCarLegallyOnTrack(car);
45
+ const body = buildBodySenses(car);
46
+ const trackHeadingError = car.trackHeadingError ?? estimateTrackHeadingError(car, snapshot);
47
+ const trackRelation = {
48
+ ...buildBoundarySenses(car, snapshot, onTrack),
49
+ headingErrorRadians: trackHeadingError,
50
+ };
51
+ const nearbyCars = buildDriverNearbyCars(car, snapshot, sensors, sensorContexts);
52
+ return {
53
+ profile: options.observation?.profile ?? 'default',
54
+ self: {
55
+ ...body,
56
+ lap: car.lap,
57
+ completedLaps: car.lapTelemetry?.completedLaps ?? 0,
58
+ lapProgressMeters: simUnitsToMeters(car.progress ?? 0),
59
+ trackOffsetMeters: trackRelation.lateralOffsetMeters,
60
+ trackHeadingErrorRadians: trackHeadingError,
61
+ onTrack,
62
+ surface: car.surface ?? 'track',
63
+ inPitLane: Boolean(car.inPitLane),
64
+ pitLanePart: car.pitLanePart ?? null,
65
+ pitBoxId: car.pitBoxId ?? null,
66
+ tireEnergy: car.tireEnergy ?? null,
67
+ pitIntent: car.pitIntent ?? car.pitStop?.intent ?? 0,
68
+ pitTargetCompound: car.pitStop?.targetTire ?? null,
69
+ pitStopStatus: car.pitStop?.status ?? null,
70
+ pitStopPhase: car.pitStop?.phase ?? null,
71
+ pitStopServiceRemainingSeconds: car.pitStop?.serviceRemainingSeconds ?? null,
72
+ pitStopPenaltyServiceRemainingSeconds: car.pitStop?.penaltyServiceRemainingSeconds ?? null,
73
+ pitStopsCompleted: car.pitStop?.stopsCompleted ?? 0,
74
+ },
75
+ trackRelation,
76
+ contactPatches: buildContactPatchSenses(car),
77
+ race: {
78
+ position: car.rank,
79
+ totalCars: snapshot.cars.length,
80
+ raceMode: snapshot.raceControl.mode,
81
+ pitLaneOpen: Boolean(snapshot.raceControl.pitLaneOpen),
82
+ redFlag: Boolean(snapshot.raceControl.redFlag),
83
+ totalLaps: snapshot.totalLaps,
84
+ },
85
+ track: {
86
+ lengthMeters: simUnitsToMeters(snapshot.track.length ?? 0),
87
+ widthMeters: simUnitsToMeters(snapshot.track.width ?? 0),
88
+ curvature: car.trackState?.curvature ?? pointAt(snapshot.track, car.progress ?? 0).curvature ?? 0,
89
+ lookahead: buildTrackLookahead(car, snapshot, options),
90
+ },
91
+ rays: sensors.rays.enabled ? buildRaySensors(sensorCar, snapshot, sensors.rays, rayBatchContextForSensors(sensors, sensorContexts?.getRayBatchContext)) : [],
92
+ nearbyCars,
93
+ events,
94
+ };
95
+ }
96
+
97
+ function estimateTrackHeadingError(car, snapshot) {
98
+ const base = pointAt(snapshot.track, car.progress ?? 0);
99
+ return normalizeAngle((car.heading ?? base.heading) - base.heading);
100
+ }
101
+
102
+ function buildTrackLookahead(car, snapshot, options) {
103
+ const distances = Array.isArray(options.observation?.lookaheadMeters)
104
+ ? options.observation.lookaheadMeters
105
+ : normalizeLookaheadMeters(options.observation?.lookaheadMeters);
106
+ if (!distances.length) return [];
107
+ const base = pointAt(snapshot.track, car.progress ?? 0);
108
+ return distances.map((distanceMeters) => {
109
+ const sample = pointAt(snapshot.track, (car.progress ?? 0) + metersToSimUnits(distanceMeters));
110
+ return {
111
+ distanceMeters,
112
+ curvature: sample.curvature ?? 0,
113
+ headingDeltaRadians: normalizeAngle(sample.heading - base.heading),
114
+ };
115
+ });
116
+ }
117
+
118
+ function isCarLegallyOnTrack(car) {
119
+ if (Array.isArray(car.wheels) && car.wheels.length > 0) {
120
+ return car.wheels.every((wheel) => wheel.onTrack || wheel.inPitLane);
121
+ }
122
+ if (car.inPitLane) return true;
123
+ return ['track', 'kerb', 'pit-entry', 'pit-lane', 'pit-exit', 'pit-box'].includes(car.surface ?? 'track');
124
+ }
125
+
126
+ function buildDriverVectorDirect(car, snapshot, options, sensors, sensorContexts = null) {
127
+ const profile = options.observation?.profile ?? 'default';
128
+ const includePhysicalDriverSenses = profile === 'physical-driver';
129
+ const onTrack = isCarLegallyOnTrack(car);
130
+ const body = buildBodySenses(car);
131
+ const trackHeadingError = car.trackHeadingError ?? estimateTrackHeadingError(car, snapshot);
132
+ const sensorCar = withEnvironmentControlFlag(car, options);
133
+ const trackRelation = {
134
+ ...buildBoundarySenses(car, snapshot, onTrack),
135
+ headingErrorRadians: trackHeadingError,
136
+ };
137
+ const contactPatches = includePhysicalDriverSenses ? buildContactPatchSenses(car) : [];
138
+ const appendRayVectorValues = sensors.rays.enabled
139
+ ? (vector) => appendRaySensorVectorValues(
140
+ vector,
141
+ sensorCar,
142
+ snapshot,
143
+ sensors.rays,
144
+ rayBatchContextForSensors(sensors, sensorContexts?.getRayBatchContext, { scratch: true }),
145
+ )
146
+ : null;
147
+ const nearbyCars = buildDriverNearbyCars(car, snapshot, sensors, sensorContexts);
148
+ const trackLengthMeters = simUnitsToMeters(snapshot.track.length ?? 0);
149
+ const trackCurvature = car.trackState?.curvature ?? pointAt(snapshot.track, car.progress ?? 0).curvature ?? 0;
150
+ const source = {
151
+ profile,
152
+ self: {
153
+ ...body,
154
+ lapProgressMeters: simUnitsToMeters(car.progress ?? 0),
155
+ trackOffsetMeters: trackRelation.lateralOffsetMeters,
156
+ trackHeadingErrorRadians: trackHeadingError,
157
+ onTrack,
158
+ inPitLane: Boolean(car.inPitLane),
159
+ tireEnergy: car.tireEnergy ?? null,
160
+ pitIntent: car.pitIntent ?? car.pitStop?.intent ?? 0,
161
+ pitStopStatus: car.pitStop?.status ?? null,
162
+ },
163
+ trackRelation,
164
+ contactPatches,
165
+ race: {
166
+ position: car.rank,
167
+ totalCars: snapshot.cars.length,
168
+ raceMode: snapshot.raceControl.mode,
169
+ pitLaneOpen: Boolean(snapshot.raceControl.pitLaneOpen),
170
+ redFlag: Boolean(snapshot.raceControl.redFlag),
171
+ },
172
+ track: {
173
+ lengthMeters: trackLengthMeters,
174
+ curvature: trackCurvature,
175
+ lookahead: buildTrackLookahead(car, snapshot, options),
176
+ },
177
+ rays: [],
178
+ appendRayVectorValues,
179
+ nearbyCars,
180
+ };
181
+ return buildObservationVector(source, sensors, {
182
+ includeSchema: false,
183
+ vectorType: options.observation?.vectorType,
184
+ });
185
+ }
186
+
187
+ function rayBatchContextForSensors(sensors, getRayBatchContext, { scratch = false } = {}) {
188
+ if (!getRayBatchContext || !sensors.rays?.channels?.includes('car')) return null;
189
+ return getRayBatchContext({ scratch });
190
+ }
191
+
192
+ export function emptyObservation(driverId) {
193
+ return {
194
+ object: {
195
+ self: {
196
+ id: driverId,
197
+ speedKph: 0,
198
+ speedMetersPerSecond: 0,
199
+ headingRadians: 0,
200
+ yawRateRadiansPerSecond: 0,
201
+ steeringAngleRadians: 0,
202
+ throttle: 0,
203
+ brake: 0,
204
+ lateralG: 0,
205
+ longitudinalG: 0,
206
+ gripUsage: 0,
207
+ slipAngleRadians: 0,
208
+ tractionLimited: false,
209
+ stabilityState: 'stable',
210
+ destroyed: false,
211
+ destroyReason: null,
212
+ lap: 0,
213
+ completedLaps: 0,
214
+ lapProgressMeters: 0,
215
+ trackOffsetMeters: 0,
216
+ trackHeadingErrorRadians: 0,
217
+ onTrack: false,
218
+ surface: 'missing',
219
+ inPitLane: false,
220
+ pitLanePart: null,
221
+ pitBoxId: null,
222
+ tireEnergy: null,
223
+ pitIntent: 0,
224
+ pitTargetCompound: null,
225
+ pitStopStatus: null,
226
+ pitStopPhase: null,
227
+ pitStopServiceRemainingSeconds: null,
228
+ pitStopPenaltyServiceRemainingSeconds: null,
229
+ pitStopsCompleted: 0,
230
+ },
231
+ trackRelation: {
232
+ lateralOffsetMeters: 0,
233
+ headingErrorRadians: 0,
234
+ legalWidthMeters: 0,
235
+ leftBoundaryMeters: 0,
236
+ rightBoundaryMeters: 0,
237
+ onLegalSurface: false,
238
+ surface: 'missing',
239
+ },
240
+ contactPatches: [],
241
+ race: { position: 0, totalCars: 0, raceMode: 'missing', pitLaneOpen: false, redFlag: false, totalLaps: 0 },
242
+ track: { lengthMeters: 0, widthMeters: 0, curvature: 0, lookahead: [] },
243
+ rays: [],
244
+ nearbyCars: [],
245
+ events: [],
246
+ },
247
+ vector: [],
248
+ schema: [],
249
+ events: [],
250
+ };
251
+ }
252
+
253
+ function withEnvironmentControlFlag(car, options) {
254
+ if (!car || car.environmentControlled) return car;
255
+ if (!Array.isArray(options?.controlledDrivers) || !options.controlledDrivers.includes(car.id)) return car;
256
+ return {
257
+ ...car,
258
+ environmentControlled: true,
259
+ };
260
+ }
@@ -0,0 +1,51 @@
1
+ export function initializeControlledPitIntent(host) {
2
+ const sim = host.getSimulation?.();
3
+ const options = host.getOptions?.();
4
+ initializeControlledPitIntentForSimulation(sim, options);
5
+ }
6
+
7
+ export function initializeControlledPitIntentForSimulation(sim, options) {
8
+ options?.controlledDrivers?.forEach?.((driverId) => {
9
+ sim?.setAutomaticPitIntentEnabled?.(driverId, false);
10
+ sim?.setPitIntent?.(driverId, 0);
11
+ const car = sim?.cars?.find?.((item) => item.id === driverId);
12
+ if (car) car.environmentControlled = true;
13
+ });
14
+ }
15
+
16
+ function resetEnvironmentPitStop(car) {
17
+ const stop = car?.pitStop;
18
+ if (!stop) return;
19
+ stop.status = 'pending';
20
+ stop.phase = null;
21
+ stop.intent = 0;
22
+ stop.queueingForService = false;
23
+ stop.route = null;
24
+ stop.routeProgress = 0;
25
+ stop.routeStartRaceDistance = null;
26
+ stop.routeEndRaceDistance = null;
27
+ stop.serviceRemaining = 0;
28
+ stop.penaltyServiceRemaining = 0;
29
+ stop.penaltyServiceTotal = 0;
30
+ stop.servingPenaltyIds = [];
31
+ stop.serviceProfile = null;
32
+ }
33
+
34
+ export function applyControlledRunoffResponse(sim, options) {
35
+ options?.controlledDrivers?.forEach?.((driverId) => {
36
+ const car = sim?.cars?.find?.((item) => item.id === driverId);
37
+ if (car) sim?.applyRunoffResponse?.(car);
38
+ });
39
+ }
40
+
41
+
42
+ export function resetControlledDriverState(sim, driverIds) {
43
+ driverIds.forEach((driverId) => {
44
+ sim.clearCarControls?.(driverId);
45
+ sim.setAutomaticPitIntentEnabled?.(driverId, false);
46
+ const car = sim.cars?.find?.((item) => item.id === driverId);
47
+ resetEnvironmentPitStop(car);
48
+ sim.setPitIntent?.(driverId, 0);
49
+ if (car) sim.applyRunoffResponse?.(car);
50
+ });
51
+ }
@@ -92,7 +92,6 @@ export function evaluateEpisode(snapshot, options, episodeState, controlledDrive
92
92
  if (!state.truncated && state.episodeStep < options.episode.maxSteps) allTruncated = false;
93
93
  if (state.truncated || state.episodeStep >= options.episode.maxSteps) anyTruncated = true;
94
94
  if (!state.terminated && !state.truncated && state.episodeStep < options.episode.maxSteps) allEnded = false;
95
- if (!allTerminated && !allTruncated && !allEnded) break;
96
95
  }
97
96
  if (allTerminated) {
98
97
  return { terminated: true, truncated: false, endReason: controlledDrivers.length === 1 ? firstTerminatedReason : 'all-drivers-terminated' };
@@ -1,5 +1,5 @@
1
- import { simUnitsToMeters } from '../simulation/units.js';
2
- import { isEnvironmentCarOffTrack, isEnvironmentContactEvent } from './metrics.js';
1
+ import { createEvaluationTracker } from './evaluationTracker.js';
2
+ export { createEvaluationTracker } from './evaluationTracker.js';
3
3
  import { createPaddockEnvironment } from './runtime.js';
4
4
 
5
5
  export const DEFAULT_EVALUATION_CASES = Object.freeze([
@@ -37,6 +37,7 @@ export function runEnvironmentEvaluation({
37
37
  baseOptions,
38
38
  policy,
39
39
  cases = DEFAULT_EVALUATION_CASES,
40
+ lapScope = 'since-start',
40
41
  createEnvironment = createPaddockEnvironment,
41
42
  } = {}) {
42
43
  if (!baseOptions || typeof baseOptions !== 'object') {
@@ -52,6 +53,7 @@ export function runEnvironmentEvaluation({
52
53
  policy,
53
54
  evaluationCase,
54
55
  createEnvironment,
56
+ lapScope,
55
57
  })),
56
58
  };
57
59
  }
@@ -61,89 +63,7 @@ function isEvaluationPolicy(policy) {
61
63
  (policy != null && typeof policy.predict === 'function');
62
64
  }
63
65
 
64
- export function createEvaluationTracker(initialResult) {
65
- if (!initialResult?.state?.snapshot) {
66
- throw new Error('PaddockJS environment evaluation tracker requires result.state.snapshot; use result.stateOutput "minimal" or "full".');
67
- }
68
- const initialCars = carsById(initialResult.state.snapshot);
69
- const metrics = Object.fromEntries(initialResult.info.controlledDrivers.map((driverId) => {
70
- const car = initialCars.get(driverId);
71
- const offTrack = isEnvironmentCarOffTrack(car);
72
- return [driverId, {
73
- distanceMeters: 0,
74
- lapProgressMeters: lapProgressMetersFromSnapshotCar(car),
75
- offTrackSteps: offTrack ? 1 : 0,
76
- contactCount: 0,
77
- recoverySuccess: false,
78
- passCount: 0,
79
- lapTimeSeconds: null,
80
- startedOffTrack: offTrack,
81
- startPosition: car?.rank ?? null,
82
- bestPosition: car?.rank ?? null,
83
- startDistanceMeters: cumulativeDistanceMetersFromSnapshotCar(car),
84
- }];
85
- }));
86
-
87
- return {
88
- update(result) {
89
- const currentCars = carsById(result.state.snapshot);
90
- result.info.controlledDrivers.forEach((driverId) => {
91
- const car = currentCars.get(driverId);
92
- const entry = metrics[driverId];
93
- if (!car || !entry) return;
94
- entry.distanceMeters = Math.max(0, cumulativeDistanceMetersFromSnapshotCar(car) - entry.startDistanceMeters);
95
- entry.lapProgressMeters = lapProgressMetersFromSnapshotCar(car);
96
- const offTrack = isEnvironmentCarOffTrack(car);
97
- if (offTrack) entry.offTrackSteps += 1;
98
- if (entry.startedOffTrack && !offTrack) entry.recoverySuccess = true;
99
- if (Number.isFinite(car.rank)) {
100
- entry.bestPosition = entry.bestPosition == null ? car.rank : Math.min(entry.bestPosition, car.rank);
101
- entry.passCount = entry.startPosition == null ? 0 : Math.max(0, entry.startPosition - entry.bestPosition);
102
- }
103
- const completedLaps = car.lapTelemetry?.completedLaps ?? 0;
104
- if (completedLaps > 0 && entry.lapTimeSeconds == null) {
105
- entry.lapTimeSeconds = result.info.elapsedSeconds;
106
- }
107
- });
108
- result.events.forEach((event) => {
109
- if (!isEnvironmentContactEvent(event)) return;
110
- Object.keys(metrics).forEach((driverId) => {
111
- if (event.driverId === driverId || event.carId === driverId || event.otherCarId === driverId || event.driverIds?.includes?.(driverId)) {
112
- metrics[driverId].contactCount += 1;
113
- }
114
- });
115
- });
116
- },
117
- finish() {
118
- return Object.fromEntries(Object.entries(metrics).map(([driverId, entry]) => [
119
- driverId,
120
- {
121
- distanceMeters: entry.distanceMeters,
122
- lapProgressMeters: entry.lapProgressMeters,
123
- offTrackSteps: entry.offTrackSteps,
124
- contactCount: entry.contactCount,
125
- recoverySuccess: entry.recoverySuccess,
126
- passCount: entry.passCount,
127
- lapTimeSeconds: entry.lapTimeSeconds,
128
- },
129
- ]));
130
- },
131
- };
132
- }
133
-
134
- function cumulativeDistanceMetersFromSnapshotCar(car) {
135
- const distance = Number(car?.distanceMeters ?? car?.raceDistanceMeters);
136
- if (Number.isFinite(distance)) return distance;
137
- return simUnitsToMeters(car?.raceDistance ?? 0);
138
- }
139
-
140
- function lapProgressMetersFromSnapshotCar(car) {
141
- const progressMeters = Number(car?.lapProgressMeters ?? car?.progressMeters);
142
- if (Number.isFinite(progressMeters)) return progressMeters;
143
- return simUnitsToMeters(car?.progress ?? 0);
144
- }
145
-
146
- function runEvaluationCase({ baseOptions, policy, evaluationCase, createEnvironment }) {
66
+ function runEvaluationCase({ baseOptions, policy, evaluationCase, createEnvironment, lapScope }) {
147
67
  const env = createEnvironment({
148
68
  ...baseOptions,
149
69
  result: {
@@ -159,7 +79,7 @@ function runEvaluationCase({ baseOptions, policy, evaluationCase, createEnvironm
159
79
  });
160
80
  try {
161
81
  let result = env.reset();
162
- const tracker = createEvaluationTracker(result);
82
+ const tracker = createEvaluationTracker(result, { lapScope });
163
83
  const maxSteps = Math.max(1, Math.floor(evaluationCase.maxSteps ?? baseOptions.episode?.maxSteps ?? 1000));
164
84
  let steps = 0;
165
85
  while (!result.done && steps < maxSteps) {
@@ -200,7 +120,3 @@ function predictAction(policy, observation, context) {
200
120
  : policy.predict(observation, context);
201
121
  return action ?? { steering: 0, throttle: 0, brake: 1 };
202
122
  }
203
-
204
- function carsById(snapshot) {
205
- return new Map(snapshot.cars.map((car) => [car.id, car]));
206
- }