@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
@@ -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
- }
@@ -0,0 +1,100 @@
1
+ import { simUnitsToMeters } from '../simulation/units.js';
2
+ import { isEnvironmentCarOffTrack } from './metrics.js';
3
+ import { isEnvironmentContactEvent, writeEventDriverIds } from './events.js';
4
+
5
+ export function createEvaluationTracker(initialResult, { lapScope = 'since-start' } = {}) {
6
+ if (!initialResult?.state?.snapshot) {
7
+ throw new Error('PaddockJS environment evaluation tracker requires result.state.snapshot; use result.stateOutput "minimal" or "full".');
8
+ }
9
+ const initialCars = carsById(initialResult.state.snapshot);
10
+ const metrics = Object.fromEntries(initialResult.info.controlledDrivers.map((driverId) => {
11
+ const car = initialCars.get(driverId);
12
+ const offTrack = isEnvironmentCarOffTrack(car);
13
+ return [driverId, {
14
+ distanceMeters: 0,
15
+ lapProgressMeters: lapProgressMetersFromSnapshotCar(car),
16
+ offTrackSteps: offTrack ? 1 : 0,
17
+ contactCount: 0,
18
+ recoverySuccess: false,
19
+ passCount: 0,
20
+ lapTimeSeconds: lapScope === 'include-history' && completedLaps(car) > 0 ? recordedLapTime(car) : null,
21
+ previousCompletedLaps: completedLaps(car),
22
+ startedOffTrack: offTrack,
23
+ startPosition: car?.rank ?? null,
24
+ bestPosition: car?.rank ?? null,
25
+ startDistanceMeters: cumulativeDistanceMetersFromSnapshotCar(car),
26
+ }];
27
+ }));
28
+
29
+ return {
30
+ update(result) {
31
+ const currentCars = carsById(result.state.snapshot);
32
+ result.info.controlledDrivers.forEach((driverId) => {
33
+ const car = currentCars.get(driverId);
34
+ const entry = metrics[driverId];
35
+ if (!car || !entry) return;
36
+ entry.distanceMeters = Math.max(0, cumulativeDistanceMetersFromSnapshotCar(car) - entry.startDistanceMeters);
37
+ entry.lapProgressMeters = lapProgressMetersFromSnapshotCar(car);
38
+ const offTrack = isEnvironmentCarOffTrack(car);
39
+ if (offTrack) entry.offTrackSteps += 1;
40
+ if (entry.startedOffTrack && !offTrack) entry.recoverySuccess = true;
41
+ if (Number.isFinite(car.rank)) {
42
+ entry.bestPosition = entry.bestPosition == null ? car.rank : Math.min(entry.bestPosition, car.rank);
43
+ entry.passCount = entry.startPosition == null ? 0 : Math.max(0, entry.startPosition - entry.bestPosition);
44
+ }
45
+ const currentCompletedLaps = completedLaps(car);
46
+ const newCompletion = currentCompletedLaps > entry.previousCompletedLaps;
47
+ if (newCompletion && entry.lapTimeSeconds == null) {
48
+ entry.lapTimeSeconds = recordedLapTime(car);
49
+ }
50
+ entry.previousCompletedLaps = currentCompletedLaps;
51
+ });
52
+ const eventDriverIds = [];
53
+ result.events.forEach((event) => {
54
+ if (!isEnvironmentContactEvent(event)) return;
55
+ writeEventDriverIds(eventDriverIds, event).forEach((driverId) => {
56
+ if (metrics[driverId]) metrics[driverId].contactCount += 1;
57
+ });
58
+ });
59
+ },
60
+ finish() {
61
+ return Object.fromEntries(Object.entries(metrics).map(([driverId, entry]) => [
62
+ driverId,
63
+ {
64
+ distanceMeters: entry.distanceMeters,
65
+ lapProgressMeters: entry.lapProgressMeters,
66
+ offTrackSteps: entry.offTrackSteps,
67
+ contactCount: entry.contactCount,
68
+ recoverySuccess: entry.recoverySuccess,
69
+ passCount: entry.passCount,
70
+ lapTimeSeconds: entry.lapTimeSeconds,
71
+ },
72
+ ]));
73
+ },
74
+ };
75
+ }
76
+
77
+ function cumulativeDistanceMetersFromSnapshotCar(car) {
78
+ const distance = Number(car?.distanceMeters ?? car?.raceDistanceMeters);
79
+ if (Number.isFinite(distance)) return distance;
80
+ return simUnitsToMeters(car?.raceDistance ?? 0);
81
+ }
82
+
83
+ function lapProgressMetersFromSnapshotCar(car) {
84
+ const progressMeters = Number(car?.lapProgressMeters ?? car?.progressMeters);
85
+ if (Number.isFinite(progressMeters)) return progressMeters;
86
+ return simUnitsToMeters(car?.progress ?? 0);
87
+ }
88
+
89
+ function carsById(snapshot) {
90
+ return new Map(snapshot.cars.map((car) => [car.id, car]));
91
+ }
92
+
93
+ function completedLaps(car) {
94
+ return car?.lapTelemetry?.completedLaps ?? 0;
95
+ }
96
+
97
+ function recordedLapTime(car) {
98
+ const duration = car?.lapTelemetry?.lastLapTime;
99
+ return Number.isFinite(duration) && duration >= 0 ? duration : null;
100
+ }
@@ -1,7 +1,30 @@
1
+ export function isEnvironmentContactEvent(event) {
2
+ return ['collision', 'contact', 'car-contact'].includes(event?.type);
3
+ }
4
+
1
5
  export function collectStepEvents(events = []) {
2
6
  return events.map((event) => normalizeEnvironmentEvent(event));
3
7
  }
4
8
 
9
+ export function writeEventDriverIds(target, event) {
10
+ target.length = 0;
11
+ pushEventDriverId(target, event.driverId);
12
+ pushEventDriverId(target, event.carId);
13
+ pushEventDriverId(target, event.otherCarId);
14
+ pushEventDriverId(target, event.primaryDriverId);
15
+ pushEventDriverId(target, event.otherDriverId);
16
+ const extraDriverIds = event.driverIds ?? [];
17
+ for (let index = 0; index < extraDriverIds.length; index += 1) {
18
+ pushEventDriverId(target, extraDriverIds[index]);
19
+ }
20
+ return target;
21
+ }
22
+
23
+ function pushEventDriverId(target, driverId) {
24
+ if (!driverId || target.includes(driverId)) return;
25
+ target.push(driverId);
26
+ }
27
+
5
28
  function normalizeEnvironmentEvent(event) {
6
29
  if (event.type === 'contact') {
7
30
  return {