@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,19 +1,9 @@
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 {
10
- buildNearbyCars,
11
- appendRaySensorVectorValues,
12
- buildRaySensors,
13
- createRayBatchContext,
14
- normalizeRayOptions,
15
- } from './sensors.js';
16
- import { buildObservationVector } from './observationVector.js';
1
+ import { writeEventDriverIds } from './events.js';
2
+ import { defaultSensorOptions, effectiveSensorOptions } from './sensorOptions.js';
3
+ import { buildDriverObservation, emptyObservation } from './driverObservations.js';
4
+ import { createNearbyBatchContext } from './sensors/sensorTargets.js';
5
+ import { createRayBatchContext } from './sensors.js';
6
+ export { defaultSensorOptions, effectiveSensorOptions } from './sensorOptions.js';
17
7
 
18
8
  const EMPTY_EVENTS = Object.freeze([]);
19
9
 
@@ -40,6 +30,11 @@ export function buildEnvironmentObservation({
40
30
  rayBatchContext ??= createRayBatchContext(snapshot);
41
31
  return rayBatchContext;
42
32
  };
33
+ let nearbyBatchContext = null;
34
+ const sensorContexts = {
35
+ getRayBatchContext,
36
+ getNearbyBatchContext: () => nearbyBatchContext ??= createNearbyBatchContext(snapshot),
37
+ };
43
38
  const observations = {};
44
39
  controlledDrivers.forEach((driverId) => {
45
40
  const car = carsById.get(driverId);
@@ -50,27 +45,10 @@ export function buildEnvironmentObservation({
50
45
 
51
46
  const driverEvents = eventsByDriver?.get(driverId) ?? EMPTY_EVENTS;
52
47
  const sensors = effectiveSensorOptions(options, car.id, defaultSensors, hasSensorOverrides);
53
- const output = options.observation?.output ?? 'full';
54
- const includeSchema = options.observation?.includeSchema !== false;
55
- const wantsObject = output === 'full' || output === 'object';
56
- const wantsVector = output === 'full' || output === 'vector';
57
- const object = wantsObject
58
- ? buildDriverObservationObject(car, snapshot, options, driverEvents, sensors, getRayBatchContext)
59
- : null;
60
- const { vector, schema } = wantsVector
61
- ? object
62
- ? buildDriverVector(object, sensors, {
63
- includeSchema,
64
- vectorType: options.observation?.vectorType,
65
- })
66
- : includeSchema
67
- ? buildDriverVector(buildDriverObservationObject(car, snapshot, options, driverEvents, sensors, getRayBatchContext), sensors, {
68
- includeSchema,
69
- vectorType: options.observation?.vectorType,
70
- })
71
- : buildDriverVectorDirect(car, snapshot, options, driverEvents, sensors, getRayBatchContext)
72
- : { vector: undefined, schema: [] };
73
- observations[driverId] = formatObservation({ object, vector, schema, events: driverEvents }, options.observation);
48
+ observations[driverId] = formatObservation(
49
+ buildDriverObservation(car, snapshot, options, driverEvents, sensors, sensorContexts),
50
+ options.observation,
51
+ );
74
52
  });
75
53
  return observations;
76
54
  }
@@ -115,14 +93,7 @@ function groupEventsByDriver(events, controlledDrivers, scratch = null) {
115
93
  controlledSet.add(driverId);
116
94
  }
117
95
  events.forEach((event) => {
118
- eventDriverIds.length = 0;
119
- pushEventDriverId(eventDriverIds, event.driverId);
120
- pushEventDriverId(eventDriverIds, event.carId);
121
- pushEventDriverId(eventDriverIds, event.otherCarId);
122
- const extraDriverIds = event.driverIds ?? [];
123
- for (let index = 0; index < extraDriverIds.length; index += 1) {
124
- pushEventDriverId(eventDriverIds, extraDriverIds[index]);
125
- }
96
+ writeEventDriverIds(eventDriverIds, event);
126
97
  for (let index = 0; index < eventDriverIds.length; index += 1) {
127
98
  const driverId = eventDriverIds[index];
128
99
  if (controlledSet.has(driverId)) byDriver.get(driverId)?.push(event);
@@ -132,280 +103,9 @@ function groupEventsByDriver(events, controlledDrivers, scratch = null) {
132
103
  return byDriver;
133
104
  }
134
105
 
135
- function pushEventDriverId(target, driverId) {
136
- if (!driverId || target.includes(driverId)) return;
137
- target.push(driverId);
138
- }
139
-
140
106
  function rayBatchContextScratchForObservation(scratch) {
141
107
  if (!scratch) return true;
142
108
  const rayScratch = scratch.rayBatchContextScratch ?? {};
143
109
  scratch.rayBatchContextScratch = rayScratch;
144
110
  return rayScratch;
145
111
  }
146
-
147
- function buildDriverObservationObject(car, snapshot, options, events, sensors, getRayBatchContext = null) {
148
- const sensorCar = withEnvironmentControlFlag(car, options);
149
- const onTrack = isCarLegallyOnTrack(car);
150
- const body = buildBodySenses(car);
151
- const trackHeadingError = car.trackHeadingError ?? estimateTrackHeadingError(car, snapshot);
152
- const trackRelation = {
153
- ...buildBoundarySenses(car, snapshot, onTrack),
154
- headingErrorRadians: trackHeadingError,
155
- };
156
- const nearbyCars = sensors.nearbyCars.enabled
157
- ? enrichOpponentRadar(car, buildNearbyCars(car, snapshot, sensors.nearbyCars), snapshot)
158
- : [];
159
- return {
160
- profile: options.observation?.profile ?? 'default',
161
- self: {
162
- ...body,
163
- lap: car.lap,
164
- completedLaps: car.lapTelemetry?.completedLaps ?? 0,
165
- lapProgressMeters: simUnitsToMeters(car.progress ?? 0),
166
- trackOffsetMeters: trackRelation.lateralOffsetMeters,
167
- trackHeadingErrorRadians: trackHeadingError,
168
- onTrack,
169
- surface: car.surface ?? 'track',
170
- inPitLane: Boolean(car.inPitLane),
171
- pitLanePart: car.pitLanePart ?? null,
172
- pitBoxId: car.pitBoxId ?? null,
173
- tireEnergy: car.tireEnergy ?? null,
174
- pitIntent: car.pitIntent ?? car.pitStop?.intent ?? 0,
175
- pitTargetCompound: car.pitStop?.targetTire ?? null,
176
- pitStopStatus: car.pitStop?.status ?? null,
177
- pitStopPhase: car.pitStop?.phase ?? null,
178
- pitStopServiceRemainingSeconds: car.pitStop?.serviceRemainingSeconds ?? null,
179
- pitStopPenaltyServiceRemainingSeconds: car.pitStop?.penaltyServiceRemainingSeconds ?? null,
180
- pitStopsCompleted: car.pitStop?.stopsCompleted ?? 0,
181
- },
182
- trackRelation,
183
- contactPatches: buildContactPatchSenses(car),
184
- race: {
185
- position: car.rank,
186
- totalCars: snapshot.cars.length,
187
- raceMode: snapshot.raceControl.mode,
188
- pitLaneOpen: Boolean(snapshot.raceControl.pitLaneOpen),
189
- redFlag: Boolean(snapshot.raceControl.redFlag),
190
- totalLaps: snapshot.totalLaps,
191
- },
192
- track: {
193
- lengthMeters: simUnitsToMeters(snapshot.track.length ?? 0),
194
- widthMeters: simUnitsToMeters(snapshot.track.width ?? 0),
195
- curvature: car.trackState?.curvature ?? pointAt(snapshot.track, car.progress ?? 0).curvature ?? 0,
196
- lookahead: buildTrackLookahead(car, snapshot, options),
197
- },
198
- rays: sensors.rays.enabled ? buildRaySensors(sensorCar, snapshot, sensors.rays, rayBatchContextForSensors(sensors, getRayBatchContext)) : [],
199
- nearbyCars,
200
- events,
201
- };
202
- }
203
-
204
- function estimateTrackHeadingError(car, snapshot) {
205
- const base = pointAt(snapshot.track, car.progress ?? 0);
206
- return normalizeAngle((car.heading ?? base.heading) - base.heading);
207
- }
208
-
209
- function buildTrackLookahead(car, snapshot, options) {
210
- const distances = Array.isArray(options.observation?.lookaheadMeters)
211
- ? options.observation.lookaheadMeters
212
- : normalizeLookaheadMeters(options.observation?.lookaheadMeters);
213
- if (!distances.length) return [];
214
- const base = pointAt(snapshot.track, car.progress ?? 0);
215
- return distances.map((distanceMeters) => {
216
- const sample = pointAt(snapshot.track, (car.progress ?? 0) + metersToSimUnits(distanceMeters));
217
- return {
218
- distanceMeters,
219
- curvature: sample.curvature ?? 0,
220
- headingDeltaRadians: normalizeAngle(sample.heading - base.heading),
221
- };
222
- });
223
- }
224
-
225
- function isCarLegallyOnTrack(car) {
226
- if (Array.isArray(car.wheels) && car.wheels.length > 0) {
227
- return car.wheels.every((wheel) => wheel.onTrack || wheel.inPitLane);
228
- }
229
- if (car.inPitLane) return true;
230
- return ['track', 'kerb', 'pit-entry', 'pit-lane', 'pit-exit', 'pit-box'].includes(car.surface ?? 'track');
231
- }
232
-
233
- function buildDriverVector(object, sensors, { includeSchema = true, vectorType = 'array' } = {}) {
234
- return buildObservationVector(object, sensors, { includeSchema, vectorType });
235
- }
236
-
237
- function buildDriverVectorDirect(car, snapshot, options, events, sensors, getRayBatchContext = null) {
238
- const profile = options.observation?.profile ?? 'default';
239
- const includePhysicalDriverSenses = profile === 'physical-driver';
240
- const onTrack = isCarLegallyOnTrack(car);
241
- const body = buildBodySenses(car);
242
- const trackHeadingError = car.trackHeadingError ?? estimateTrackHeadingError(car, snapshot);
243
- const sensorCar = withEnvironmentControlFlag(car, options);
244
- const trackRelation = {
245
- ...buildBoundarySenses(car, snapshot, onTrack),
246
- headingErrorRadians: trackHeadingError,
247
- };
248
- const contactPatches = includePhysicalDriverSenses ? buildContactPatchSenses(car) : [];
249
- const appendRayVectorValues = sensors.rays.enabled
250
- ? (vector) => appendRaySensorVectorValues(
251
- vector,
252
- sensorCar,
253
- snapshot,
254
- sensors.rays,
255
- rayBatchContextForSensors(sensors, getRayBatchContext, { scratch: true }),
256
- )
257
- : null;
258
- const nearbyCars = sensors.nearbyCars.enabled
259
- ? enrichOpponentRadar(car, buildNearbyCars(car, snapshot, sensors.nearbyCars), snapshot)
260
- : [];
261
- const trackLengthMeters = simUnitsToMeters(snapshot.track.length ?? 0);
262
- const trackCurvature = car.trackState?.curvature ?? pointAt(snapshot.track, car.progress ?? 0).curvature ?? 0;
263
- const source = {
264
- profile,
265
- self: {
266
- ...body,
267
- lapProgressMeters: simUnitsToMeters(car.progress ?? 0),
268
- trackOffsetMeters: trackRelation.lateralOffsetMeters,
269
- trackHeadingErrorRadians: trackHeadingError,
270
- onTrack,
271
- inPitLane: Boolean(car.inPitLane),
272
- tireEnergy: car.tireEnergy ?? null,
273
- pitIntent: car.pitIntent ?? car.pitStop?.intent ?? 0,
274
- pitStopStatus: car.pitStop?.status ?? null,
275
- },
276
- trackRelation,
277
- contactPatches,
278
- race: {
279
- position: car.rank,
280
- totalCars: snapshot.cars.length,
281
- raceMode: snapshot.raceControl.mode,
282
- pitLaneOpen: Boolean(snapshot.raceControl.pitLaneOpen),
283
- redFlag: Boolean(snapshot.raceControl.redFlag),
284
- },
285
- track: {
286
- lengthMeters: trackLengthMeters,
287
- curvature: trackCurvature,
288
- lookahead: buildTrackLookahead(car, snapshot, options),
289
- },
290
- rays: [],
291
- appendRayVectorValues,
292
- nearbyCars,
293
- };
294
- return buildObservationVector(source, sensors, {
295
- includeSchema: false,
296
- vectorType: options.observation?.vectorType,
297
- });
298
- }
299
-
300
- function rayBatchContextForSensors(sensors, getRayBatchContext, { scratch = false } = {}) {
301
- if (!getRayBatchContext || !sensors.rays?.channels?.includes('car')) return null;
302
- return getRayBatchContext({ scratch });
303
- }
304
-
305
- export function defaultSensorOptions(options) {
306
- return {
307
- rays: normalizeRayOptions(options.sensors.rays),
308
- nearbyCars: options.sensors.nearbyCars,
309
- };
310
- }
311
-
312
- export function effectiveSensorOptions(options, driverId, defaultSensors = defaultSensorOptions(options), hasSensorOverrides = true) {
313
- if (!hasSensorOverrides || !options.sensorsByDriver?.[driverId]) return defaultSensors;
314
- const rayOverrides = options.sensorsByDriver?.[driverId]?.rays ?? {};
315
- return {
316
- rays: normalizeRayOptions(mergeDriverRayOptions(options.sensors.rays, rayOverrides)),
317
- nearbyCars: {
318
- ...options.sensors.nearbyCars,
319
- ...(options.sensorsByDriver?.[driverId]?.nearbyCars ?? {}),
320
- },
321
- };
322
- }
323
-
324
- function mergeDriverRayOptions(baseRays, rayOverrides = {}) {
325
- const merged = {
326
- ...baseRays,
327
- ...rayOverrides,
328
- };
329
- if (Object.hasOwn(rayOverrides, 'rays')) return merged;
330
- if (Object.hasOwn(rayOverrides, 'layout') || Object.hasOwn(rayOverrides, 'anglesDegrees')) {
331
- delete merged.rays;
332
- return merged;
333
- }
334
- if (Object.hasOwn(rayOverrides, 'lengthMeters') || Object.hasOwn(rayOverrides, 'defaultLengthMeters')) {
335
- merged.rays = (baseRays.rays ?? []).map((ray) => ({
336
- id: ray.id,
337
- angleDegrees: ray.angleDegrees,
338
- }));
339
- }
340
- return merged;
341
- }
342
-
343
- function emptyObservation(driverId) {
344
- return {
345
- object: {
346
- self: {
347
- id: driverId,
348
- speedKph: 0,
349
- speedMetersPerSecond: 0,
350
- headingRadians: 0,
351
- yawRateRadiansPerSecond: 0,
352
- steeringAngleRadians: 0,
353
- throttle: 0,
354
- brake: 0,
355
- lateralG: 0,
356
- longitudinalG: 0,
357
- gripUsage: 0,
358
- slipAngleRadians: 0,
359
- tractionLimited: false,
360
- stabilityState: 'stable',
361
- destroyed: false,
362
- destroyReason: null,
363
- lap: 0,
364
- completedLaps: 0,
365
- lapProgressMeters: 0,
366
- trackOffsetMeters: 0,
367
- trackHeadingErrorRadians: 0,
368
- onTrack: false,
369
- surface: 'missing',
370
- inPitLane: false,
371
- pitLanePart: null,
372
- pitBoxId: null,
373
- tireEnergy: null,
374
- pitIntent: 0,
375
- pitTargetCompound: null,
376
- pitStopStatus: null,
377
- pitStopPhase: null,
378
- pitStopServiceRemainingSeconds: null,
379
- pitStopPenaltyServiceRemainingSeconds: null,
380
- pitStopsCompleted: 0,
381
- },
382
- trackRelation: {
383
- lateralOffsetMeters: 0,
384
- headingErrorRadians: 0,
385
- legalWidthMeters: 0,
386
- leftBoundaryMeters: 0,
387
- rightBoundaryMeters: 0,
388
- onLegalSurface: false,
389
- surface: 'missing',
390
- },
391
- contactPatches: [],
392
- race: { position: 0, totalCars: 0, raceMode: 'missing', pitLaneOpen: false, redFlag: false, totalLaps: 0 },
393
- track: { lengthMeters: 0, widthMeters: 0, curvature: 0, lookahead: [] },
394
- rays: [],
395
- nearbyCars: [],
396
- events: [],
397
- },
398
- vector: [],
399
- schema: [],
400
- events: [],
401
- };
402
- }
403
-
404
- function withEnvironmentControlFlag(car, options) {
405
- if (!car || car.environmentControlled) return car;
406
- if (!Array.isArray(options?.controlledDrivers) || !options.controlledDrivers.includes(car.id)) return car;
407
- return {
408
- ...car,
409
- environmentControlled: true,
410
- };
411
- }
@@ -1,10 +1,11 @@
1
1
  import { CHAMPIONSHIP_ENTRY_BLUEPRINTS } from '../data/championship.js';
2
+ import { createRuntimeSeed } from '../config/runtimeSeed.js';
2
3
  import { normalizeSimulatorDrivers } from '../data/normalizeDrivers.js';
3
4
  import { normalizePhysicsMode } from '../simulation/vehicle/vehiclePhysics.js';
4
5
  import { normalizeWarmupOptions } from '../simulation/warmup/runtimeWarmup.js';
5
6
  import { normalizeObservationOptions } from './observationOptions.js';
6
7
  import { resolveScenarioPlacementConfig } from './scenarios.js';
7
- import { normalizeRayOptions } from './sensors.js';
8
+ import { mergeRayOptionOverrides, normalizeRayOptions } from './sensors/rayConfig.js';
8
9
 
9
10
  const DEFAULT_SEED = 1971;
10
11
  const DEFAULT_FRAME_SKIP = 1;
@@ -32,8 +33,8 @@ export function resolveEnvironmentOptions(options = {}) {
32
33
  throw new Error('PaddockJS environment controlledDrivers is required.');
33
34
  }
34
35
 
35
- const seed = normalizeSeed(options.seed, createGeneratedSeed);
36
- const trackSeed = normalizeSeed(options.trackSeed, createGeneratedSeed);
36
+ const seed = normalizeSeed(options.seed, createRuntimeSeed);
37
+ const trackSeed = normalizeSeed(options.trackSeed, createRuntimeSeed);
37
38
  const normalizedDrivers = normalizeSimulatorDrivers(options.drivers, {
38
39
  entries: options.entries ?? CHAMPIONSHIP_ENTRY_BLUEPRINTS,
39
40
  caller: 'createPaddockEnvironment',
@@ -58,13 +59,6 @@ export function resolveEnvironmentOptions(options = {}) {
58
59
  const scenario = resolveScenario(options.scenario, controlledDrivers, driverIds);
59
60
  const participants = resolveParticipants(scenario.participants, controlledDrivers, resolved.drivers);
60
61
  const participantDrivers = resolved.drivers.filter((driver) => participants.has(driver.id));
61
- const participantIds = new Set(participantDrivers.map((driver) => driver.id));
62
-
63
- controlledDrivers.forEach((driverId) => {
64
- if (!participantIds.has(driverId)) {
65
- throw new Error(`PaddockJS environment controlled driver must be included in scenario participants: ${driverId}`);
66
- }
67
- });
68
62
 
69
63
  const externalRenderer = normalizeExternalRenderer(options.externalRenderer);
70
64
 
@@ -110,16 +104,6 @@ function normalizeSeed(value, fallbackFactory) {
110
104
  return Number.isFinite(number) ? number : DEFAULT_SEED;
111
105
  }
112
106
 
113
- function createGeneratedSeed() {
114
- const values = new Uint32Array(1);
115
- try {
116
- globalThis.crypto?.getRandomValues?.(values);
117
- } catch {
118
- values[0] = 0;
119
- }
120
- return (values[0] || Math.floor(Date.now() + performance.now() * 1000)) >>> 0;
121
- }
122
-
123
107
  function resolveScenario(scenario = {}, controlledDrivers, driverIds) {
124
108
  const participants = scenario.participants ?? 'all';
125
109
  if (
@@ -168,10 +152,7 @@ function normalizePositiveInteger(value, fallback, label) {
168
152
 
169
153
  function mergeSensorOptions(sensors = {}) {
170
154
  return {
171
- rays: normalizeRayOptions({
172
- ...DEFAULT_SENSOR_OPTIONS.rays,
173
- ...(sensors.rays ?? {}),
174
- }),
155
+ rays: normalizeRayOptions(mergeRayOptionOverrides(DEFAULT_SENSOR_OPTIONS.rays, sensors.rays ?? {})),
175
156
  nearbyCars: {
176
157
  ...DEFAULT_SENSOR_OPTIONS.nearbyCars,
177
158
  ...(sensors.nearbyCars ?? {}),
@@ -0,0 +1,21 @@
1
+ export function emitExternalRenderFrame(host, result, meta = {}, observationSnapshot = null) {
2
+ const options = host.getOptions?.();
3
+ const onFrame = options?.externalRenderer?.onFrame;
4
+ if (typeof onFrame !== 'function') return;
5
+ try {
6
+ onFrame({
7
+ snapshot: result?.state?.snapshot ??
8
+ observationSnapshot ??
9
+ host.getSimulation?.()?.snapshotObservation?.() ??
10
+ host.getSimulation?.()?.snapshot?.(),
11
+ observation: result?.observation ?? {},
12
+ meta: {
13
+ ...meta,
14
+ step: result?.info?.step ?? null,
15
+ elapsedSeconds: result?.info?.elapsedSeconds ?? null,
16
+ },
17
+ });
18
+ } catch {
19
+ // External rendering hooks are observer-only and must not break stepping.
20
+ }
21
+ }
@@ -0,0 +1,41 @@
1
+ import { mergeRayOptionOverrides } from './sensors/rayConfig.js';
2
+
3
+ export function mergeEnvironmentResetOptions(currentOptions, nextOptions = {}) {
4
+ return mergePlainObjects(currentOptions, nextOptions);
5
+ }
6
+
7
+ const REPLACE_OBJECT_PATHS = new Set([
8
+ 'scenario.placements',
9
+ ]);
10
+
11
+ function mergePlainObjects(base, overrides, path = []) {
12
+ if (!isPlainObject(base) || !isPlainObject(overrides)) return overrides;
13
+ const merged = { ...base };
14
+ Object.entries(overrides).forEach(([key, value]) => {
15
+ if (value === undefined) return;
16
+ const nextPath = [...path, key];
17
+ if (isPlainObject(value) && isPlainObject(base[key]) && isRayOptionsPath(nextPath)) {
18
+ merged[key] = mergeRayOptionOverrides(base[key], value);
19
+ return;
20
+ }
21
+ merged[key] = isPlainObject(value) &&
22
+ isPlainObject(base[key]) &&
23
+ !REPLACE_OBJECT_PATHS.has(nextPath.join('.'))
24
+ ? mergePlainObjects(base[key], value, nextPath)
25
+ : value;
26
+ });
27
+ return merged;
28
+ }
29
+
30
+ function isPlainObject(value) {
31
+ return value != null &&
32
+ typeof value === 'object' &&
33
+ !Array.isArray(value) &&
34
+ (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
35
+ }
36
+
37
+
38
+ function isRayOptionsPath(path) {
39
+ return (path.length === 2 && path[0] === 'sensors' && path[1] === 'rays') ||
40
+ (path.length === 3 && path[0] === 'sensorsByDriver' && path[2] === 'rays');
41
+ }
@@ -0,0 +1,125 @@
1
+ import { buildDriverEpisodeInfo, evaluateEpisode, markDestroyedDriverEpisodes } from './episode.js';
2
+ import { buildDriverMetrics } from './metrics.js';
3
+ import { buildEnvironmentObservation } from './observations.js';
4
+
5
+ export function buildEnvironmentResult({
6
+ host,
7
+ episodeState,
8
+ events,
9
+ actionErrors,
10
+ actions = {},
11
+ controlledDrivers = null,
12
+ stateOutput = null,
13
+ rewardEnabled = false,
14
+ }) {
15
+ const options = host.getOptions();
16
+ const sim = host.getSimulation();
17
+ const resultDrivers = controlledDrivers ?? options.controlledDrivers;
18
+ const resolvedStateOutput = stateOutput ?? options.result.stateOutput;
19
+ const observationSnapshot = snapshotForResult(sim, options, resolvedStateOutput);
20
+ markDestroyedDriverEpisodes(episodeState, options.controlledDrivers, observationSnapshot);
21
+ const observation = buildEnvironmentObservation({
22
+ snapshot: observationSnapshot,
23
+ options,
24
+ events,
25
+ controlledDrivers: resultDrivers,
26
+ scratch: episodeState.observationScratch,
27
+ });
28
+ const episode = evaluateEpisode(observationSnapshot, options, episodeState, resultDrivers);
29
+ const metrics = buildDriverMetrics({
30
+ snapshot: observationSnapshot,
31
+ previousSnapshot: episodeState.previousSnapshot,
32
+ options: { ...options, controlledDrivers: resultDrivers },
33
+ events,
34
+ scratch: episodeState.metricScratch,
35
+ });
36
+ const rewardEpisodeInfo = buildDriverEpisodeInfo(episodeState, {
37
+ ...options,
38
+ controlledDrivers: resultDrivers,
39
+ }, episode);
40
+ const driverEpisode = resultDrivers === options.controlledDrivers
41
+ ? episode
42
+ : evaluateEpisode(observationSnapshot, options, episodeState, options.controlledDrivers);
43
+ const driverEpisodeInfo = buildDriverEpisodeInfo(episodeState, options, driverEpisode);
44
+ const { state, rewardSnapshot } = buildResultState(sim, observationSnapshot, resolvedStateOutput);
45
+ const reward = rewardEnabled
46
+ ? computeReward({
47
+ options: { ...options, controlledDrivers: resultDrivers },
48
+ observation,
49
+ events,
50
+ snapshot: rewardSnapshot,
51
+ actions,
52
+ previousSnapshot: episodeState.previousSnapshot,
53
+ metrics,
54
+ driverEpisodeInfo: rewardEpisodeInfo,
55
+ })
56
+ : null;
57
+ episodeState.lastObservationSnapshot = observationSnapshot;
58
+ episodeState.lastRewardSnapshot = rewardSnapshot;
59
+ return {
60
+ observation,
61
+ reward,
62
+ metrics,
63
+ terminated: episode.terminated,
64
+ truncated: episode.truncated,
65
+ done: episode.terminated || episode.truncated,
66
+ events,
67
+ state,
68
+ info: {
69
+ step: episodeState.step,
70
+ elapsedSeconds: observationSnapshot.time,
71
+ seed: options.seed,
72
+ trackSeed: options.trackSeed,
73
+ controlledDrivers: [...resultDrivers],
74
+ actionErrors,
75
+ endReason: episode.endReason,
76
+ drivers: driverEpisodeInfo,
77
+ },
78
+ };
79
+ }
80
+
81
+ function buildResultState(sim, observationSnapshot, stateOutput) {
82
+ if (stateOutput === 'none') return { state: null, rewardSnapshot: observationSnapshot };
83
+ if (stateOutput === 'minimal') return { state: { snapshot: observationSnapshot }, rewardSnapshot: observationSnapshot };
84
+ const snapshot = sim.snapshot();
85
+ return { state: { snapshot }, rewardSnapshot: snapshot };
86
+ }
87
+
88
+ export function snapshotForResult(sim, options, stateOutput) {
89
+ if (canUseTrainingSnapshot(options, stateOutput) && !hasExternalRenderer(options)) {
90
+ return sim.snapshotTraining?.() ?? sim.snapshotObservation?.() ?? sim.snapshot();
91
+ }
92
+ return sim.snapshotObservation?.() ?? sim.snapshot();
93
+ }
94
+
95
+ function hasExternalRenderer(options) {
96
+ return typeof options?.externalRenderer?.onFrame === 'function';
97
+ }
98
+
99
+ function canUseTrainingSnapshot(options, stateOutput) {
100
+ return stateOutput === 'none' &&
101
+ options.observation?.output === 'vector' &&
102
+ options.observation?.includeSchema === false;
103
+ }
104
+
105
+ function computeReward({ options, observation, events, snapshot, actions, previousSnapshot, metrics, driverEpisodeInfo }) {
106
+ if (!options.reward) return null;
107
+ return Object.fromEntries(options.controlledDrivers.map((driverId) => [
108
+ driverId,
109
+ normalizeRewardValue(options.reward({
110
+ driverId,
111
+ previous: previousSnapshot,
112
+ current: observation[driverId],
113
+ action: actions?.[driverId],
114
+ events: observation[driverId]?.events ?? events,
115
+ state: { snapshot },
116
+ metrics: metrics?.[driverId] ?? null,
117
+ episode: driverEpisodeInfo?.[driverId] ?? null,
118
+ })),
119
+ ]));
120
+ }
121
+
122
+ function normalizeRewardValue(value) {
123
+ const reward = Number(value ?? 0);
124
+ return Number.isFinite(reward) ? reward : 0;
125
+ }
@@ -1,3 +1,5 @@
1
+ import { isEnvironmentContactEvent, writeEventDriverIds } from './events.js';
2
+
1
3
  const DEFAULT_PROGRESS_REWARD_WEIGHTS = Object.freeze({
2
4
  progress: 1,
3
5
  speed: 0.01,
@@ -23,7 +25,7 @@ export function createProgressReward(options = {}) {
23
25
  const isOnTrack = context.metrics?.offTrack != null
24
26
  ? !context.metrics.offTrack
25
27
  : self.onTrack ?? isSnapshotCarOnTrack(currentCar);
26
- const collisionCount = countDriverCollisions(context.events, driverId);
28
+ const collisionCount = context.metrics?.contactCount ?? countDriverCollisions(context.events, driverId);
27
29
  const steering = Math.abs(Number(context.action?.steering ?? 0));
28
30
  const brake = Math.abs(Number(context.action?.brake ?? 0));
29
31
 
@@ -62,19 +64,11 @@ function isSnapshotCarOnTrack(car) {
62
64
 
63
65
  function countDriverCollisions(events = [], driverId) {
64
66
  if (!driverId || !Array.isArray(events)) return 0;
65
- return events.filter((event) => isCollisionEvent(event) && eventIncludesDriver(event, driverId)).length;
66
- }
67
-
68
- function isCollisionEvent(event) {
69
- return event?.type === 'collision' || event?.type === 'contact';
70
- }
71
-
72
- function eventIncludesDriver(event, driverId) {
73
- if (event.driverId === driverId ||
74
- event.carId === driverId ||
75
- event.primaryDriverId === driverId ||
76
- event.otherDriverId === driverId) {
77
- return true;
78
- }
79
- return Array.isArray(event.driverIds) && event.driverIds.includes(driverId);
67
+ const driverIds = [];
68
+ let count = 0;
69
+ events.forEach((event) => {
70
+ if (!isEnvironmentContactEvent(event)) return;
71
+ if (writeEventDriverIds(driverIds, event).includes(driverId)) count += 1;
72
+ });
73
+ return count;
80
74
  }