@inventure71/paddockjs 4.1.0 → 4.3.0

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 (257) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/INSTALL_AND_UPDATE.md +31 -5
  3. package/README.md +34 -3
  4. package/docs/advanced_ai_pace.md +131 -0
  5. package/docs/advanced_physics_rebuild.md +166 -0
  6. package/docs/architecture.md +111 -30
  7. package/docs/cleanup_coverage.md +284 -0
  8. package/docs/component_inventory.md +32 -16
  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/getting-started.md +3 -0
  16. package/docs/grid_walk.md +80 -0
  17. package/docs/index.md +5 -2
  18. package/docs/keyboard_driving_fix.md +118 -0
  19. package/docs/limit_driving_2026-10-14.md +88 -0
  20. package/docs/loading.md +141 -0
  21. package/docs/maintainability_audit_2026-09-08.md +146 -0
  22. package/docs/racing_ai_improvements.md +145 -0
  23. package/docs/rules.md +43 -13
  24. package/docs/sense_contract.md +4 -1
  25. package/docs/system_specs.md +40 -12
  26. package/docs/testing-reliability-guide.md +67 -0
  27. package/docs/v11_improvements.md +108 -0
  28. package/package.json +23 -15
  29. package/src/api/MountedSimulatorComponents.js +101 -0
  30. package/src/api/PaddockSimulatorController.js +56 -81
  31. package/src/app/BrowserExpertAdapter.js +0 -6
  32. package/src/app/BrowserExpertExternalRenderer.js +72 -50
  33. package/src/app/ExternalTrackSnapshot.js +50 -0
  34. package/src/app/F1SimulatorApp.js +242 -297
  35. package/src/app/HostLifecycleEmitter.js +93 -0
  36. package/src/app/banners/raceDataBannerController.js +9 -4
  37. package/src/app/camera/cameraConstants.js +1 -1
  38. package/src/app/camera/cameraController.js +10 -13
  39. package/src/app/camera/cameraGeometry.js +1 -1
  40. package/src/app/controls/SimulatorViewControls.js +45 -0
  41. package/src/app/controls/workbenchBindings.js +64 -0
  42. package/src/app/controls/workbenchDisclosure.js +41 -0
  43. package/src/app/domBindings.js +21 -13
  44. package/src/app/gridWalk/GridWalkController.js +180 -0
  45. package/src/app/gridWalk/gridWalkCamera.js +88 -0
  46. package/src/app/gridWalk/gridWalkControls.js +24 -0
  47. package/src/app/gridWalk/gridWalkCrew.js +246 -0
  48. package/src/app/gridWalk/gridWalkLabels.js +104 -0
  49. package/src/app/gridWalk/gridWalkLayout.js +46 -0
  50. package/src/app/gridWalk/gridWalkNavigation.js +99 -0
  51. package/src/app/gridWalk/gridWalkProjectBanner.js +43 -0
  52. package/src/app/gridWalk/gridWalkSession.js +50 -0
  53. package/src/app/layoutSupport.js +2 -0
  54. package/src/app/raceOverlayClearanceSupport.js +21 -7
  55. package/src/app/readouts/carOverviewRenderer.js +9 -4
  56. package/src/app/readouts/readoutFormatters.js +1 -8
  57. package/src/app/readouts/telemetryRenderer.js +1 -3
  58. package/src/app/readouts/timingTowerRenderer.js +48 -50
  59. package/src/app/rendering/appAssets.js +8 -10
  60. package/src/app/rendering/carRenderer.js +27 -79
  61. package/src/app/rendering/displayUtils.js +0 -6
  62. package/src/app/rendering/expertSensorRenderer.js +1 -1
  63. package/src/app/rendering/replayGhostRenderer.js +4 -1
  64. package/src/app/rendering/trackRenderer.js +2 -2
  65. package/src/app/runtime/frameLoop.js +18 -17
  66. package/src/app/runtime/pixiStartupLifecycle.js +94 -0
  67. package/src/app/runtime/runtimeTiming.js +4 -1
  68. package/src/app/runtime/runtimeVisibility.js +7 -2
  69. package/src/config/cssValues.js +1 -1
  70. package/src/config/defaultOptions.js +12 -173
  71. package/src/config/restartOptions.js +1 -0
  72. package/src/config/runtimeSeed.js +10 -0
  73. package/src/config/simulatorDefaults.js +106 -0
  74. package/src/config/telemetryModules.js +20 -0
  75. package/src/config/theme/cssTokens.js +85 -0
  76. package/src/config/theme/themeCss.js +72 -0
  77. package/src/config/theme/themeInputs.js +155 -0
  78. package/src/config/theme/themeResolver.js +172 -0
  79. package/src/config/theme/themeSelectors.js +69 -0
  80. package/src/config/theme/themeTokens.js +257 -0
  81. package/src/config/theme/themeUtils.js +15 -0
  82. package/src/config/themeOptions.js +14 -830
  83. package/src/config/timingGapMode.js +0 -2
  84. package/src/config/uiOptions.js +53 -0
  85. package/src/data/championship.js +16 -158
  86. package/src/data/championshipDefaults.js +87 -0
  87. package/src/data/driverData.js +10 -20
  88. package/src/data/entryIdentity.js +67 -0
  89. package/src/data/entryPresentation.js +23 -0
  90. package/src/data/entryTypes.d.ts +160 -0
  91. package/src/data/index.d.ts +12 -195
  92. package/src/data/ratingScale.js +14 -0
  93. package/src/data/teamTypes.d.ts +23 -0
  94. package/src/data/trackTypes.d.ts +101 -0
  95. package/src/data/vehicleData.js +12 -23
  96. package/src/environment/actions.js +3 -2
  97. package/src/environment/controllerLoop.js +57 -44
  98. package/src/environment/controllerScheduler.js +30 -0
  99. package/src/environment/driverObservations.js +260 -0
  100. package/src/environment/driverReset.js +51 -0
  101. package/src/environment/episode.js +0 -1
  102. package/src/environment/evaluation.js +6 -90
  103. package/src/environment/evaluationTracker.js +100 -0
  104. package/src/environment/events.js +23 -0
  105. package/src/environment/index.d.ts +35 -91
  106. package/src/environment/metrics.js +3 -17
  107. package/src/environment/observationSchema.js +104 -0
  108. package/src/environment/observationVector.js +16 -115
  109. package/src/environment/observations.js +16 -316
  110. package/src/environment/options.js +5 -24
  111. package/src/environment/renderFrames.js +21 -0
  112. package/src/environment/resetOptions.js +41 -0
  113. package/src/environment/results.js +125 -0
  114. package/src/environment/rewards.js +10 -16
  115. package/src/environment/runtime.js +17 -300
  116. package/src/environment/scenarioOptions.js +90 -0
  117. package/src/environment/scenarioPlacements.js +70 -0
  118. package/src/environment/scenarioPresets.js +78 -0
  119. package/src/environment/scenarios.js +5 -236
  120. package/src/environment/scratch.js +18 -0
  121. package/src/environment/sensorOptions.js +20 -0
  122. package/src/environment/sensors/index.js +2 -556
  123. package/src/environment/sensors/indexedRayBands.js +0 -18
  124. package/src/environment/sensors/nearbyCars.js +18 -15
  125. package/src/environment/sensors/opponentRadar.js +3 -2
  126. package/src/environment/sensors/pitConnectorProximity.js +20 -0
  127. package/src/environment/sensors/rayBandTrace.js +1 -37
  128. package/src/environment/sensors/rayBatch.js +81 -0
  129. package/src/environment/sensors/rayConfig.js +26 -0
  130. package/src/environment/sensors/rayGeometry.js +0 -8
  131. package/src/environment/sensors/rayGuards.js +0 -6
  132. package/src/environment/sensors/rayOutputs.js +75 -0
  133. package/src/environment/sensors/rayQuery.js +110 -0
  134. package/src/environment/sensors/rayScratch.js +146 -0
  135. package/src/environment/sensors/sensorTargets.js +12 -4
  136. package/src/environment/sensors/surfaceRays.js +6 -61
  137. package/src/environment/sensors/trackRays.js +7 -80
  138. package/src/environment/simulationSetup.js +30 -0
  139. package/src/environment/specs.js +15 -109
  140. package/src/index.d.ts +43 -202
  141. package/src/index.js +28 -6
  142. package/src/placeholder/index.js +1 -8
  143. package/src/rendering/displayLifecycle.js +6 -0
  144. package/src/rendering/proceduralTrackAsset.js +1 -1
  145. package/src/rendering/renderSnapshot.js +3 -0
  146. package/src/rendering/track/finishLineRenderer.js +1 -1
  147. package/src/rendering/track/gridRenderer.js +1 -1
  148. package/src/rendering/track/offsetStrokeSafety.js +2 -2
  149. package/src/rendering/track/pitLaneRenderer.js +1 -1
  150. package/src/rendering/track/trackMaterialRenderer.js +4 -4
  151. package/src/rendering/track/trackRenderConstants.js +0 -1
  152. package/src/rendering/track/trackRenderGeometry.js +1 -7
  153. package/src/simulation/collisionGeometry.js +12 -20
  154. package/src/simulation/driver/advancedPathControls.js +53 -0
  155. package/src/simulation/driver/advancedRacePace.js +112 -0
  156. package/src/simulation/driver/advancedRacingControls.js +61 -0
  157. package/src/simulation/driver/advancedRacingEnvelope.js +255 -0
  158. package/src/simulation/driver/advancedRacingSteering.js +76 -0
  159. package/src/simulation/driver/advancedRacingTiming.js +16 -0
  160. package/src/simulation/driver/arcadeRacePace.js +83 -0
  161. package/src/simulation/driver/arcadeRacingControls.js +111 -0
  162. package/src/simulation/driver/arcadeRacingEnvelope.js +63 -0
  163. package/src/simulation/driver/driverControlConstants.js +0 -2
  164. package/src/simulation/driver/driverController.js +16 -5
  165. package/src/simulation/driver/edgeRecovery.js +2 -3
  166. package/src/simulation/driver/racingControls.js +10 -396
  167. package/src/simulation/driver/racingDriverProfile.js +48 -0
  168. package/src/simulation/driver/racingLinePlan.js +54 -20
  169. package/src/simulation/driver/rejoinControls.js +59 -112
  170. package/src/simulation/driver/safetyCarControls.js +12 -0
  171. package/src/simulation/driver/trafficScan.js +136 -27
  172. package/src/simulation/participants/participantInteractions.js +24 -15
  173. package/src/simulation/pit/pitCarPlacement.js +29 -0
  174. package/src/simulation/pit/pitFlow.js +8 -30
  175. package/src/simulation/pit/pitIntent.js +4 -3
  176. package/src/simulation/pit/pitOccupancy.js +2 -2
  177. package/src/simulation/pit/pitQueue.js +2 -18
  178. package/src/simulation/pit/pitRouteMovement.js +1 -36
  179. package/src/simulation/pit/pitRouting.js +5 -81
  180. package/src/simulation/pit/pitService.js +3 -32
  181. package/src/simulation/pit/pitServiceConstants.js +1 -0
  182. package/src/simulation/pit/pitServiceProgress.js +34 -0
  183. package/src/simulation/pit/pitSnapshots.js +8 -4
  184. package/src/simulation/pit/pitState.js +1 -5
  185. package/src/simulation/race/classification.js +1 -8
  186. package/src/simulation/race/raceFinish.js +3 -8
  187. package/src/simulation/race/raceOrder.js +2 -13
  188. package/src/simulation/race/raceProgress.js +6 -20
  189. package/src/simulation/race/raceSetup.js +1 -1
  190. package/src/simulation/race/raceStep.js +20 -11
  191. package/src/simulation/race/redFlag.js +2 -4
  192. package/src/simulation/race/safetyCar.js +5 -9
  193. package/src/simulation/race/stalledDnf.js +2 -4
  194. package/src/simulation/raceSimulation.js +6 -38
  195. package/src/simulation/replay/replayGhosts.js +44 -32
  196. package/src/simulation/rules/collisionSteward.js +0 -14
  197. package/src/simulation/rules/penaltyActions.js +37 -0
  198. package/src/simulation/rules/penaltyLedger.js +8 -1
  199. package/src/simulation/rules/pitLaneSpeedingSteward.js +1 -1
  200. package/src/simulation/rules/rulesReview.js +20 -16
  201. package/src/simulation/rules/trackLimitsSteward.js +2 -2
  202. package/src/simulation/simMath.js +4 -0
  203. package/src/simulation/snapshots/raceSnapshots.js +30 -11
  204. package/src/simulation/timing/drsState.js +8 -0
  205. package/src/simulation/timing/drsTiming.js +2 -4
  206. package/src/simulation/timing/lapTelemetry.js +7 -4
  207. package/src/simulation/timing/sectorPerformance.js +14 -6
  208. package/src/simulation/timing/timingLines.js +12 -6
  209. package/src/simulation/track/pitLaneAccess.js +2 -2
  210. package/src/simulation/track/pitLaneLayout.js +4 -135
  211. package/src/simulation/track/pitLaneModel.js +70 -0
  212. package/src/simulation/track/pitLaneState.js +0 -1
  213. package/src/simulation/track/pitServiceLayout.js +121 -0
  214. package/src/simulation/track/proceduralTrack.js +77 -0
  215. package/src/simulation/track/spatialQueries.js +76 -31
  216. package/src/simulation/track/trackBuilder.js +54 -0
  217. package/src/simulation/track/trackConstants.js +0 -22
  218. package/src/simulation/track/trackMath.js +4 -29
  219. package/src/simulation/track/trackModel.js +12 -240
  220. package/src/simulation/track/trackModelCache.js +105 -0
  221. package/src/simulation/track/trackNearestQueries.js +1218 -0
  222. package/src/simulation/track/trackQueryConstants.js +3 -0
  223. package/src/simulation/track/trackQueryConstruction.js +368 -0
  224. package/src/simulation/track/trackQueryIndex.js +12 -1933
  225. package/src/simulation/track/trackQuerySegments.js +174 -0
  226. package/src/simulation/track/trackQueryStats.js +12 -0
  227. package/src/simulation/track/trackRayQueries.js +165 -0
  228. package/src/simulation/vehicle/advancedContactResponse.js +89 -0
  229. package/src/simulation/vehicle/advancedTireForces.js +92 -0
  230. package/src/simulation/vehicle/advancedVehicleModel.js +65 -0
  231. package/src/simulation/vehicle/advancedVehiclePhysics.js +127 -0
  232. package/src/simulation/vehicle/contactResolution.js +23 -9
  233. package/src/simulation/vehicle/mainTrackWheelSurface.js +2 -13
  234. package/src/simulation/vehicle/pitWheelSurface.js +0 -12
  235. package/src/simulation/vehicle/runoffResponse.js +2 -4
  236. package/src/simulation/vehicle/sampledWheelSurface.js +266 -0
  237. package/src/simulation/vehicle/vehicleGeometry.js +85 -103
  238. package/src/simulation/vehicle/vehicleKinematics.js +10 -0
  239. package/src/simulation/vehicle/vehiclePhysics.js +42 -196
  240. package/src/simulation/vehicle/vehicleSnapshots.js +18 -6
  241. package/src/simulation/vehicle/vehicleState.js +5 -3
  242. package/src/simulation/vehicle/wheelSurface.js +13 -432
  243. package/src/simulation/vehicle/wheelSurfaceCache.js +55 -0
  244. package/src/simulation/vehicle/wheelSurfaceSummary.js +101 -0
  245. package/src/styles.css +173 -19
  246. package/src/ui/bannerTemplates.js +4 -2
  247. package/src/ui/cameraControlsTemplate.js +4 -2
  248. package/src/ui/gridWalkTemplate.js +29 -0
  249. package/src/ui/htmlEscaping.js +8 -0
  250. package/src/ui/raceCanvasTemplate.js +3 -2
  251. package/src/ui/shellTemplate.js +3 -0
  252. package/src/ui/telemetryDrawerTemplate.js +4 -2
  253. package/src/ui/telemetryTemplates.js +3 -22
  254. package/src/ui/templateUtils.js +2 -8
  255. package/src/ui/viewControlsTemplate.js +21 -0
  256. package/examples/train-basic-policy.mjs +0 -276
  257. package/examples/trainingData.mjs +0 -133
@@ -1,21 +1,16 @@
1
- import { createRaceSimulation, FIXED_STEP } from '../simulation/raceSimulation.js';
2
- import { disableWarmupOptions, runWarmupWithGuard } from '../simulation/warmup/runtimeWarmup.js';
1
+ import { FIXED_STEP } from '../simulation/raceSimulation.js';
3
2
  import { handleActionError, resolveActionMap } from './actions.js';
4
3
  import { collectStepEvents } from './events.js';
5
- import {
6
- advanceDriverEpisodes,
7
- buildDriverEpisodeInfo,
8
- createEpisodeState,
9
- evaluateEpisode,
10
- initializeDriverEpisodes,
11
- markDestroyedDriverEpisodes,
12
- resetDriverEpisodes,
13
- } from './episode.js';
14
- import { buildDriverMetrics } from './metrics.js';
15
- import { buildEnvironmentObservation } from './observations.js';
4
+ import { advanceDriverEpisodes, createEpisodeState, initializeDriverEpisodes, resetDriverEpisodes } from './episode.js';
16
5
  import { resolveEnvironmentOptions } from './options.js';
17
- import { applyEnvironmentPlacements, applyEnvironmentScenario, normalizeEnvironmentPlacements } from './scenarios.js';
6
+ import { applyEnvironmentPlacements, normalizeEnvironmentPlacements } from './scenarios.js';
18
7
  import { buildActionSpec, buildObservationSpec } from './specs.js';
8
+ import { initializeControlledPitIntent, resetControlledDriverState } from './driverReset.js';
9
+ import { createSimulationWithEnvironmentScenario } from './simulationSetup.js';
10
+ import { mergeEnvironmentResetOptions } from './resetOptions.js';
11
+ import { buildEnvironmentResult, snapshotForResult } from './results.js';
12
+ import { emitExternalRenderFrame } from './renderFrames.js';
13
+ import { clearMetricScratch, clearObservationScratch } from './scratch.js';
19
14
 
20
15
  export function createPaddockEnvironment(options = {}) {
21
16
  let resolvedOptions = resolveEnvironmentOptions(options);
@@ -37,31 +32,6 @@ export function createPaddockEnvironment(options = {}) {
37
32
  });
38
33
  }
39
34
 
40
- function createSimulationWithEnvironmentScenario(options) {
41
- runWarmupWithGuard({
42
- options,
43
- surface: 'environment',
44
- execute: ({ warmup }) => {
45
- const warmupSimulation = createRaceSimulation(disableWarmupOptions(options, warmup.surface));
46
- initializeControlledPitIntentForSimulation(warmupSimulation, options);
47
- applyEnvironmentScenario(warmupSimulation, options);
48
- applyControlledRunoffResponse(warmupSimulation, options);
49
- for (let index = 0; index < warmup.steps; index += 1) warmupSimulation.step(FIXED_STEP);
50
- const warmupSnapshot = warmupSimulation.snapshotObservation?.() ?? warmupSimulation.snapshot();
51
- buildEnvironmentObservation({
52
- snapshot: warmupSnapshot,
53
- options,
54
- events: [],
55
- });
56
- },
57
- });
58
- const sim = createRaceSimulation(disableWarmupOptions(options, 'environment'));
59
- initializeControlledPitIntentForSimulation(sim, options);
60
- applyEnvironmentScenario(sim, options);
61
- applyControlledRunoffResponse(sim, options);
62
- return sim;
63
- }
64
-
65
35
  export function createEnvironmentRuntime(host) {
66
36
  const episodeState = createEpisodeState();
67
37
 
@@ -76,9 +46,9 @@ export function createEnvironmentRuntime(host) {
76
46
  episodeState.step = 0;
77
47
  episodeState.previousSnapshot = null;
78
48
  initializeDriverEpisodes(episodeState, options.controlledDrivers);
79
- const result = buildResult({ host, episodeState, events: [], actionErrors: [] });
49
+ const result = buildEnvironmentResult({ host, episodeState, events: [], actionErrors: [] });
80
50
  episodeState.lastResult = result;
81
- emitExternalRenderFrame(host, result, { source: 'reset' });
51
+ emitExternalRenderFrame(host, result, { source: 'reset' }, episodeState.lastObservationSnapshot);
82
52
  host.afterReset(result);
83
53
  return result;
84
54
  }
@@ -116,7 +86,7 @@ export function createEnvironmentRuntime(host) {
116
86
  }
117
87
  episodeState.step += 1;
118
88
  advanceDriverEpisodes(episodeState, options.controlledDrivers);
119
- const result = buildResult({
89
+ const result = buildEnvironmentResult({
120
90
  host,
121
91
  episodeState,
122
92
  events: stepEvents,
@@ -125,14 +95,14 @@ export function createEnvironmentRuntime(host) {
125
95
  rewardEnabled: true,
126
96
  });
127
97
  episodeState.lastResult = result;
128
- emitExternalRenderFrame(host, result, { source: 'step', actions });
98
+ emitExternalRenderFrame(host, result, { source: 'step', actions }, episodeState.lastObservationSnapshot);
129
99
  host.afterStep(result);
130
100
  return result;
131
101
  }
132
102
 
133
103
  function getObservation() {
134
104
  return episodeState.lastResult?.observation ??
135
- buildResult({ host, episodeState, events: [], actionErrors: [] }).observation;
105
+ buildEnvironmentResult({ host, episodeState, events: [], actionErrors: [] }).observation;
136
106
  }
137
107
 
138
108
  function getState({ output = 'full' } = {}) {
@@ -150,21 +120,14 @@ export function createEnvironmentRuntime(host) {
150
120
  const snapshot = sim.snapshotObservation?.() ?? sim.snapshot();
151
121
  const carsById = new Map(snapshot.cars.map((car) => [car.id, car]));
152
122
  applyEnvironmentPlacements(sim, snapshot.track, carsById, normalizedPlacements);
153
- Object.keys(normalizedPlacements).forEach((driverId) => {
154
- sim.clearCarControls?.(driverId);
155
- sim.setAutomaticPitIntentEnabled?.(driverId, false);
156
- const car = sim.cars?.find?.((item) => item.id === driverId);
157
- resetEnvironmentPitStop(car);
158
- sim.setPitIntent?.(driverId, 0);
159
- if (car) sim.applyRunoffResponse?.(car);
160
- });
123
+ resetControlledDriverState(sim, Object.keys(normalizedPlacements));
161
124
  resetDriverEpisodes(episodeState, Object.keys(normalizedPlacements));
162
125
  episodeState.previousSnapshot = null;
163
126
  const resetDriverIds = Object.keys(normalizedPlacements);
164
127
  const observationScope = resultOptions.observationScope ??
165
128
  resultOptions.resetDriversObservationScope ??
166
129
  options.result.resetDriversObservationScope;
167
- const result = buildResult({
130
+ const result = buildEnvironmentResult({
168
131
  host,
169
132
  episodeState,
170
133
  events: [],
@@ -177,7 +140,7 @@ export function createEnvironmentRuntime(host) {
177
140
  source: 'resetDrivers',
178
141
  resetDriverIds,
179
142
  observationScope,
180
- });
143
+ }, episodeState.lastObservationSnapshot);
181
144
  return result;
182
145
  }
183
146
 
@@ -203,255 +166,9 @@ export function createEnvironmentRuntime(host) {
203
166
  return { reset, step, resetDrivers, getObservation, getState, getActionSpec, getObservationSpec, destroy };
204
167
  }
205
168
 
206
- function mergeEnvironmentResetOptions(currentOptions, nextOptions = {}) {
207
- return mergePlainObjects(currentOptions, nextOptions);
208
- }
209
-
210
- const REPLACE_OBJECT_PATHS = new Set([
211
- 'scenario.placements',
212
- ]);
213
-
214
- function mergePlainObjects(base, overrides, path = []) {
215
- if (!isPlainObject(base) || !isPlainObject(overrides)) return overrides;
216
- const merged = { ...base };
217
- Object.entries(overrides).forEach(([key, value]) => {
218
- if (value === undefined) return;
219
- const nextPath = [...path, key];
220
- merged[key] = isPlainObject(value) &&
221
- isPlainObject(base[key]) &&
222
- !REPLACE_OBJECT_PATHS.has(nextPath.join('.'))
223
- ? mergePlainObjects(base[key], value, nextPath)
224
- : value;
225
- });
226
- return merged;
227
- }
228
-
229
- function isPlainObject(value) {
230
- return value != null &&
231
- typeof value === 'object' &&
232
- !Array.isArray(value) &&
233
- (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
234
- }
235
-
236
169
  function isNoopPitIntent(pitIntent) {
237
170
  if (pitIntent && typeof pitIntent === 'object') {
238
171
  return Number(pitIntent.intent ?? pitIntent.pitIntent) === 0;
239
172
  }
240
173
  return Number(pitIntent) === 0;
241
174
  }
242
-
243
- function initializeControlledPitIntent(host) {
244
- const sim = host.getSimulation?.();
245
- const options = host.getOptions?.();
246
- initializeControlledPitIntentForSimulation(sim, options);
247
- }
248
-
249
- function initializeControlledPitIntentForSimulation(sim, options) {
250
- options?.controlledDrivers?.forEach?.((driverId) => {
251
- sim?.setAutomaticPitIntentEnabled?.(driverId, false);
252
- sim?.setPitIntent?.(driverId, 0);
253
- const car = sim?.cars?.find?.((item) => item.id === driverId);
254
- if (car) car.environmentControlled = true;
255
- });
256
- }
257
-
258
- function resetEnvironmentPitStop(car) {
259
- const stop = car?.pitStop;
260
- if (!stop) return;
261
- stop.status = 'pending';
262
- stop.phase = null;
263
- stop.intent = 0;
264
- stop.queueingForService = false;
265
- stop.route = null;
266
- stop.routeProgress = 0;
267
- stop.routeStartRaceDistance = null;
268
- stop.routeEndRaceDistance = null;
269
- stop.serviceRemaining = 0;
270
- stop.penaltyServiceRemaining = 0;
271
- stop.penaltyServiceTotal = 0;
272
- stop.servingPenaltyIds = [];
273
- stop.serviceProfile = null;
274
- }
275
-
276
- function buildResult({
277
- host,
278
- episodeState,
279
- events,
280
- actionErrors,
281
- actions = {},
282
- controlledDrivers = null,
283
- stateOutput = null,
284
- rewardEnabled = false,
285
- }) {
286
- const options = host.getOptions();
287
- const sim = host.getSimulation();
288
- const resultDrivers = controlledDrivers ?? options.controlledDrivers;
289
- const resolvedStateOutput = stateOutput ?? options.result.stateOutput;
290
- const observationSnapshot = snapshotForResult(sim, options, resolvedStateOutput);
291
- markDestroyedDriverEpisodes(episodeState, options.controlledDrivers, observationSnapshot);
292
- const observation = buildEnvironmentObservation({
293
- snapshot: observationSnapshot,
294
- previousSnapshot: episodeState.previousSnapshot,
295
- options,
296
- events,
297
- controlledDrivers: resultDrivers,
298
- scratch: episodeState.observationScratch,
299
- });
300
- const episode = evaluateEpisode(observationSnapshot, options, episodeState, resultDrivers);
301
- const metrics = buildDriverMetrics({
302
- snapshot: observationSnapshot,
303
- previousSnapshot: episodeState.previousSnapshot,
304
- options: { ...options, controlledDrivers: resultDrivers },
305
- events,
306
- scratch: episodeState.metricScratch,
307
- });
308
- const rewardEpisodeInfo = buildDriverEpisodeInfo(episodeState, {
309
- ...options,
310
- controlledDrivers: resultDrivers,
311
- }, episode);
312
- const driverEpisode = resultDrivers === options.controlledDrivers
313
- ? episode
314
- : evaluateEpisode(observationSnapshot, options, episodeState, options.controlledDrivers);
315
- const driverEpisodeInfo = buildDriverEpisodeInfo(episodeState, options, driverEpisode);
316
- const { state, rewardSnapshot } = buildResultState(sim, observationSnapshot, resolvedStateOutput);
317
- const reward = rewardEnabled
318
- ? computeReward({
319
- options: { ...options, controlledDrivers: resultDrivers },
320
- observation,
321
- events,
322
- snapshot: rewardSnapshot,
323
- actions,
324
- previousSnapshot: episodeState.previousSnapshot,
325
- metrics,
326
- driverEpisodeInfo: rewardEpisodeInfo,
327
- })
328
- : null;
329
- episodeState.lastObservationSnapshot = observationSnapshot;
330
- episodeState.lastRewardSnapshot = rewardSnapshot;
331
- return {
332
- observation,
333
- reward,
334
- metrics,
335
- terminated: episode.terminated,
336
- truncated: episode.truncated,
337
- done: episode.terminated || episode.truncated,
338
- events,
339
- state,
340
- info: {
341
- step: episodeState.step,
342
- elapsedSeconds: observationSnapshot.time,
343
- seed: options.seed,
344
- trackSeed: options.trackSeed,
345
- controlledDrivers: [...resultDrivers],
346
- actionErrors,
347
- endReason: episode.endReason,
348
- drivers: driverEpisodeInfo,
349
- },
350
- };
351
- }
352
-
353
- function clearObservationScratch(scratch) {
354
- if (!scratch) return;
355
- scratch.carsById?.clear?.();
356
- scratch.eventsByDriver?.clear?.();
357
- scratch.controlledEventDrivers?.clear?.();
358
- clearRayBatchContextScratch(scratch.rayBatchContextScratch);
359
- if (Array.isArray(scratch.eventDriverIds)) scratch.eventDriverIds.length = 0;
360
- }
361
-
362
- function clearRayBatchContextScratch(scratch) {
363
- if (!scratch) return;
364
- const arrayKeys = [
365
- 'rayTargets',
366
- 'rayTargetPool',
367
- 'carTargets',
368
- 'rayVectors',
369
- 'sharedRayQueries',
370
- 'rays',
371
- 'rayPool',
372
- 'rayVectorValues',
373
- 'rayVectorValuePool',
374
- ];
375
- for (let index = 0; index < arrayKeys.length; index += 1) {
376
- const value = scratch[arrayKeys[index]];
377
- if (Array.isArray(value)) value.length = 0;
378
- }
379
- }
380
-
381
- function clearMetricScratch(scratch) {
382
- if (!scratch) return;
383
- scratch.previousCarsById?.clear?.();
384
- scratch.currentCarsById?.clear?.();
385
- scratch.contactCounts?.clear?.();
386
- if (Array.isArray(scratch.eventDriverIds)) scratch.eventDriverIds.length = 0;
387
- }
388
-
389
- function applyControlledRunoffResponse(sim, options) {
390
- options?.controlledDrivers?.forEach?.((driverId) => {
391
- const car = sim?.cars?.find?.((item) => item.id === driverId);
392
- if (car) sim?.applyRunoffResponse?.(car);
393
- });
394
- }
395
-
396
- function buildResultState(sim, observationSnapshot, stateOutput) {
397
- if (stateOutput === 'none') return { state: null, rewardSnapshot: observationSnapshot };
398
- if (stateOutput === 'minimal') return { state: { snapshot: observationSnapshot }, rewardSnapshot: observationSnapshot };
399
- const snapshot = sim.snapshot();
400
- return { state: { snapshot }, rewardSnapshot: snapshot };
401
- }
402
-
403
- function snapshotForResult(sim, options, stateOutput) {
404
- if (canUseTrainingSnapshot(options, stateOutput)) {
405
- return sim.snapshotTraining?.() ?? sim.snapshotObservation?.() ?? sim.snapshot();
406
- }
407
- return sim.snapshotObservation?.() ?? sim.snapshot();
408
- }
409
-
410
- function canUseTrainingSnapshot(options, stateOutput) {
411
- return stateOutput === 'none' &&
412
- options.observation?.output === 'vector' &&
413
- options.observation?.includeSchema === false;
414
- }
415
-
416
- function computeReward({ options, observation, events, snapshot, actions, previousSnapshot, metrics, driverEpisodeInfo }) {
417
- if (!options.reward) return null;
418
- return Object.fromEntries(options.controlledDrivers.map((driverId) => [
419
- driverId,
420
- normalizeRewardValue(options.reward({
421
- driverId,
422
- previous: previousSnapshot,
423
- current: observation[driverId],
424
- action: actions?.[driverId],
425
- events: observation[driverId]?.events ?? events,
426
- state: { snapshot },
427
- metrics: metrics?.[driverId] ?? null,
428
- episode: driverEpisodeInfo?.[driverId] ?? null,
429
- })),
430
- ]));
431
- }
432
-
433
- function emitExternalRenderFrame(host, result, meta = {}) {
434
- const options = host.getOptions?.();
435
- const onFrame = options?.externalRenderer?.onFrame;
436
- if (typeof onFrame !== 'function') return;
437
- try {
438
- onFrame({
439
- snapshot: result?.state?.snapshot ??
440
- host.getSimulation?.()?.snapshotObservation?.() ??
441
- host.getSimulation?.()?.snapshot?.(),
442
- observation: result?.observation ?? {},
443
- meta: {
444
- ...meta,
445
- step: result?.info?.step ?? null,
446
- elapsedSeconds: result?.info?.elapsedSeconds ?? null,
447
- },
448
- });
449
- } catch {
450
- // External rendering hooks are observer-only and must not break stepping.
451
- }
452
- }
453
-
454
- function normalizeRewardValue(value) {
455
- const reward = Number(value ?? 0);
456
- return Number.isFinite(reward) ? reward : 0;
457
- }
@@ -0,0 +1,90 @@
1
+ import { ENVIRONMENT_SCENARIO_PRESETS } from './scenarioPresets.js';
2
+
3
+ export function resolveScenarioPlacementConfig(scenario = {}, driverIds = new Set()) {
4
+ const preset = scenario.preset == null ? null : String(scenario.preset);
5
+ if (preset && !ENVIRONMENT_SCENARIO_PRESETS.includes(preset)) {
6
+ throw new Error(`PaddockJS environment scenario.preset must be one of: ${ENVIRONMENT_SCENARIO_PRESETS.join(', ')}.`);
7
+ }
8
+
9
+ const placements = normalizePlacementMap(scenario.placements, driverIds);
10
+ const traffic = normalizeTrafficLayout(scenario.traffic, driverIds);
11
+
12
+ return {
13
+ preset,
14
+ placements,
15
+ traffic,
16
+ };
17
+ }
18
+
19
+ export function normalizeEnvironmentPlacements(placements = {}, driverIds = new Set()) {
20
+ return normalizePlacementMap(placements, driverIds);
21
+ }
22
+
23
+ function normalizePlacementMap(value, driverIds) {
24
+ if (value == null) return {};
25
+ if (typeof value !== 'object' || Array.isArray(value)) {
26
+ throw new Error('PaddockJS environment scenario.placements must be an object keyed by driver id.');
27
+ }
28
+ return Object.fromEntries(Object.entries(value).map(([driverId, placement]) => {
29
+ assertKnownDriver(driverId, driverIds, 'scenario placement');
30
+ return [driverId, normalizePlacement(placement, `scenario.placements.${driverId}`)];
31
+ }));
32
+ }
33
+
34
+ function normalizeTrafficLayout(value, driverIds) {
35
+ if (value == null) return [];
36
+ if (!Array.isArray(value)) {
37
+ throw new Error('PaddockJS environment scenario.traffic must be an array.');
38
+ }
39
+ return value.map((entry, index) => {
40
+ if (!entry || typeof entry !== 'object') {
41
+ throw new Error(`PaddockJS environment scenario.traffic[${index}] must be an object.`);
42
+ }
43
+ assertKnownDriver(entry.driverId, driverIds, `scenario.traffic[${index}].driverId`);
44
+ assertKnownDriver(entry.relativeTo, driverIds, `scenario.traffic[${index}].relativeTo`);
45
+ return {
46
+ driverId: entry.driverId,
47
+ relativeTo: entry.relativeTo,
48
+ deltaDistanceMeters: finiteNumber(entry.deltaDistanceMeters, 0, `scenario.traffic[${index}].deltaDistanceMeters`),
49
+ offsetMeters: finiteNumber(entry.offsetMeters, 0, `scenario.traffic[${index}].offsetMeters`),
50
+ speedKph: optionalFiniteNumber(entry.speedKph, `scenario.traffic[${index}].speedKph`),
51
+ headingErrorRadians: finiteNumber(entry.headingErrorRadians, 0, `scenario.traffic[${index}].headingErrorRadians`),
52
+ };
53
+ });
54
+ }
55
+
56
+ function normalizePlacement(placement, label) {
57
+ if (!placement || typeof placement !== 'object') {
58
+ throw new Error(`PaddockJS environment ${label} must be an object.`);
59
+ }
60
+ const distanceMeters = placement.distanceMeters ?? placement.startDistanceMeters;
61
+ return {
62
+ distanceMeters: optionalFiniteNumber(distanceMeters, `${label}.distanceMeters`),
63
+ offsetMeters: finiteNumber(placement.offsetMeters, 0, `${label}.offsetMeters`),
64
+ speedKph: optionalFiniteNumber(placement.speedKph, `${label}.speedKph`),
65
+ headingErrorRadians: finiteNumber(placement.headingErrorRadians, 0, `${label}.headingErrorRadians`),
66
+ };
67
+ }
68
+
69
+ function assertKnownDriver(driverId, driverIds, label) {
70
+ if (typeof driverId !== 'string' || driverId.length === 0) {
71
+ throw new Error(`PaddockJS environment ${label} must be a driver id.`);
72
+ }
73
+ if (!driverIds.has(driverId)) {
74
+ throw new Error(`PaddockJS environment ${label} does not exist: ${driverId}`);
75
+ }
76
+ }
77
+
78
+ function finiteNumber(value, fallback, label) {
79
+ if (value == null) return fallback;
80
+ const number = Number(value);
81
+ if (!Number.isFinite(number)) {
82
+ throw new Error(`PaddockJS environment ${label} must be a finite number.`);
83
+ }
84
+ return number;
85
+ }
86
+
87
+ function optionalFiniteNumber(value, label) {
88
+ if (value == null) return undefined;
89
+ return finiteNumber(value, undefined, label);
90
+ }
@@ -0,0 +1,70 @@
1
+ import { normalizeAngle } from '../simulation/simMath.js';
2
+ import { offsetTrackPoint, pointAt } from '../simulation/track/trackModel.js';
3
+ import { kphToSimSpeed, metersToSimUnits, simUnitsToMeters } from '../simulation/units.js';
4
+
5
+ export function applyEnvironmentPlacements(sim, track, carsById, placements = {}) {
6
+ const states = {};
7
+ Object.entries(placements).forEach(([driverId, placement]) => {
8
+ const currentCar = carsById.get(driverId);
9
+ if (!currentCar) return;
10
+ states[driverId] = placementToCarState(track, currentCar, placement, {
11
+ physicsMode: sim.physicsMode,
12
+ });
13
+ });
14
+ if (typeof sim.setCarStates === 'function') {
15
+ sim.setCarStates(states);
16
+ return;
17
+ }
18
+ Object.entries(states).forEach(([driverId, partial]) => {
19
+ sim.setCarState(driverId, partial);
20
+ });
21
+ }
22
+
23
+ function placementToCarState(track, currentCar, placement, { physicsMode = 'arcade' } = {}) {
24
+ const currentDistanceMeters = currentCar.distanceMeters ?? simUnitsToMeters(currentCar.raceDistance ?? currentCar.progress ?? 0);
25
+ const raceDistance = metersToSimUnits(Math.max(0, placement.distanceMeters ?? currentDistanceMeters));
26
+ const offset = metersToSimUnits(placement.offsetMeters ?? 0);
27
+ const base = pointAt(track, raceDistance);
28
+ const position = offsetTrackPoint(base, offset);
29
+ const heading = normalizeAngle(base.heading + (placement.headingErrorRadians ?? 0));
30
+ const speed = placement.speedKph == null ? null : kphToSimSpeed(placement.speedKph);
31
+ const partial = {
32
+ x: position.x,
33
+ y: position.y,
34
+ previousX: position.x,
35
+ previousY: position.y,
36
+ progress: base.distance,
37
+ raceDistance,
38
+ desiredOffset: offset,
39
+ heading,
40
+ previousHeading: heading,
41
+ destroyed: false,
42
+ destroyReason: null,
43
+ destroyedAt: null,
44
+ outOfRace: false,
45
+ canAttack: true,
46
+ throttle: 0,
47
+ brake: 0,
48
+ longitudinalAcceleration: 0,
49
+ lateralAcceleration: 0,
50
+ lateralG: 0,
51
+ longitudinalG: 0,
52
+ gripUsage: 0,
53
+ slipAngleRadians: 0,
54
+ tractionLimited: false,
55
+ steeringAngle: 0,
56
+ yawRate: 0,
57
+ stabilityState: 'stable',
58
+ };
59
+ if (speed != null) {
60
+ partial.speed = speed;
61
+ if (physicsMode === 'advanced') {
62
+ partial.velocityX = Math.cos(heading) * speed;
63
+ partial.velocityY = Math.sin(heading) * speed;
64
+ } else {
65
+ partial.velocityX = null;
66
+ partial.velocityY = null;
67
+ }
68
+ }
69
+ return partial;
70
+ }
@@ -0,0 +1,78 @@
1
+ import { metersToSimUnits, simUnitsToMeters } from '../simulation/units.js';
2
+
3
+ export const ENVIRONMENT_SCENARIO_PRESETS = Object.freeze([
4
+ 'cornering',
5
+ 'off-track-recovery',
6
+ 'overtaking-pack',
7
+ 'pit-entry',
8
+ ]);
9
+
10
+ export function buildPresetPlacements(snapshot, options) {
11
+ const preset = options.scenario?.preset;
12
+ if (!preset) return {};
13
+ const controlledDriverId = options.controlledDrivers[0];
14
+ const cars = snapshot.cars;
15
+ const track = snapshot.track;
16
+ const primaryDistance = presetBaseDistance(track, preset);
17
+ const placements = {};
18
+
19
+ if (preset === 'cornering') {
20
+ placements[controlledDriverId] = {
21
+ distanceMeters: simUnitsToMeters(primaryDistance),
22
+ offsetMeters: 0,
23
+ speedKph: 135,
24
+ headingErrorRadians: 0,
25
+ };
26
+ } else if (preset === 'off-track-recovery') {
27
+ placements[controlledDriverId] = {
28
+ distanceMeters: simUnitsToMeters(primaryDistance),
29
+ offsetMeters: simUnitsToMeters(track.width / 2) + 7,
30
+ speedKph: 52,
31
+ headingErrorRadians: -0.45,
32
+ };
33
+ } else if (preset === 'pit-entry') {
34
+ placements[controlledDriverId] = {
35
+ distanceMeters: simUnitsToMeters(primaryDistance),
36
+ offsetMeters: 0,
37
+ speedKph: 120,
38
+ headingErrorRadians: 0,
39
+ };
40
+ } else if (preset === 'overtaking-pack') {
41
+ const baseMeters = simUnitsToMeters(primaryDistance);
42
+ cars.slice(0, 4).forEach((car, index) => {
43
+ placements[car.id] = {
44
+ distanceMeters: baseMeters + (index - 1) * 18,
45
+ offsetMeters: index % 2 === 0 ? -2.2 : 2.2,
46
+ speedKph: 135 - index * 3,
47
+ headingErrorRadians: 0,
48
+ };
49
+ });
50
+ }
51
+
52
+ return placements;
53
+ }
54
+
55
+ function presetBaseDistance(track, preset) {
56
+ if (preset === 'cornering') return findHighCurvatureDistance(track) - metersToSimUnits(70);
57
+ if (preset === 'off-track-recovery') return track.length * 0.42;
58
+ if (preset === 'overtaking-pack') return track.length * 0.35;
59
+ if (preset === 'pit-entry') {
60
+ const pitDistance = track.pitLane?.entry?.trackDistance ??
61
+ track.pitLane?.entry?.distanceFromStart ??
62
+ track.length * 0.92;
63
+ return pitDistance - metersToSimUnits(170);
64
+ }
65
+ return 0;
66
+ }
67
+
68
+ function findHighCurvatureDistance(track) {
69
+ const samples = Array.isArray(track.samples) ? track.samples : [];
70
+ const fallback = track.length * 0.25;
71
+ const best = samples
72
+ .filter((sample) => sample.distance > metersToSimUnits(350))
73
+ .reduce((selected, sample) => {
74
+ if (!selected || (sample.curvature ?? 0) > (selected.curvature ?? 0)) return sample;
75
+ return selected;
76
+ }, null);
77
+ return best?.distance ?? fallback;
78
+ }