@inventure71/paddockjs 1.0.0 → 2.0.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 (190) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/INSTALL_AND_UPDATE.md +4 -3
  3. package/README.md +194 -12
  4. package/docs/architecture.md +119 -44
  5. package/docs/concepts.md +4 -2
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +299 -17
  8. package/docs/index.md +1 -0
  9. package/docs/learnings.md +22 -0
  10. package/docs/rules.md +37 -10
  11. package/docs/system_specs.md +23 -10
  12. package/docs/training.md +272 -30
  13. package/examples/python/README.md +62 -0
  14. package/examples/python/base_policy_server.py +191 -0
  15. package/examples/python/requirements.txt +2 -0
  16. package/package.json +12 -4
  17. package/src/api/PaddockSimulatorController.js +4 -0
  18. package/src/app/BrowserExpertAdapter.js +367 -9
  19. package/src/app/F1SimulatorApp.js +305 -1603
  20. package/src/app/banners/bannerFormatters.js +55 -0
  21. package/src/app/banners/raceDataBannerController.js +141 -0
  22. package/src/app/banners/stewardMessageController.js +99 -0
  23. package/src/app/camera/cameraConstants.js +21 -0
  24. package/src/app/camera/cameraController.js +265 -0
  25. package/src/app/camera/cameraGeometry.js +176 -0
  26. package/src/app/domBindings.js +4 -0
  27. package/src/app/readouts/carOverviewRenderer.js +109 -0
  28. package/src/app/readouts/controlStateRenderer.js +37 -0
  29. package/src/app/readouts/raceStatusRenderer.js +133 -0
  30. package/src/app/readouts/readoutFormatters.js +58 -0
  31. package/src/app/readouts/telemetryRenderer.js +113 -0
  32. package/src/app/readouts/timingTowerRenderer.js +177 -0
  33. package/src/app/rendering/appAssets.js +38 -0
  34. package/src/app/rendering/carRenderer.js +286 -0
  35. package/src/app/rendering/displayUtils.js +22 -0
  36. package/src/app/rendering/drsTrailRenderer.js +90 -0
  37. package/src/app/rendering/expertSensorRenderer.js +126 -0
  38. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  39. package/src/app/rendering/replayGhostRenderer.js +124 -0
  40. package/src/app/rendering/trackRenderer.js +25 -0
  41. package/src/app/runtime/frameLoop.js +66 -0
  42. package/src/app/runtime/runtimeTiming.js +34 -0
  43. package/src/app/runtime/runtimeVisibility.js +45 -0
  44. package/src/config/defaultOptions.js +20 -0
  45. package/src/environment/actions.js +13 -5
  46. package/src/environment/controllerLoop.js +246 -0
  47. package/src/environment/episode.js +86 -2
  48. package/src/environment/evaluation.js +54 -32
  49. package/src/environment/index.d.ts +361 -9
  50. package/src/environment/index.js +1 -0
  51. package/src/environment/metrics.js +159 -0
  52. package/src/environment/observationOptions.js +22 -1
  53. package/src/environment/observations.js +325 -40
  54. package/src/environment/options.js +37 -5
  55. package/src/environment/runtime.js +222 -21
  56. package/src/environment/scenarios.js +37 -8
  57. package/src/environment/sensors/bodySenses.js +23 -0
  58. package/src/environment/sensors/boundarySenses.js +17 -0
  59. package/src/environment/sensors/carRays.js +75 -0
  60. package/src/environment/sensors/contactSenses.js +32 -0
  61. package/src/environment/sensors/index.js +157 -0
  62. package/src/environment/sensors/indexedRayBands.js +103 -0
  63. package/src/environment/sensors/nearbyCars.js +52 -0
  64. package/src/environment/sensors/opponentRadar.js +65 -0
  65. package/src/environment/sensors/rayConfig.js +110 -0
  66. package/src/environment/sensors/rayDefaults.js +6 -0
  67. package/src/environment/sensors/rayGeometry.js +60 -0
  68. package/src/environment/sensors/rayGuards.js +19 -0
  69. package/src/environment/sensors/sensorTargets.js +60 -0
  70. package/src/environment/sensors/surfaceRays.js +296 -0
  71. package/src/environment/sensors/trackRays.js +261 -0
  72. package/src/environment/sensors.js +1 -367
  73. package/src/environment/specs.js +100 -5
  74. package/src/environment/workerProtocol.js +13 -1
  75. package/src/index.d.ts +169 -6
  76. package/src/index.js +5 -0
  77. package/src/rendering/proceduralTrackAsset.js +20 -823
  78. package/src/rendering/renderSnapshot.js +38 -12
  79. package/src/rendering/track/finishLineRenderer.js +34 -0
  80. package/src/rendering/track/gridRenderer.js +47 -0
  81. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  82. package/src/rendering/track/pitLaneRenderer.js +248 -0
  83. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  84. package/src/rendering/track/trackRenderConstants.js +38 -0
  85. package/src/rendering/track/trackRenderGeometry.js +94 -0
  86. package/src/simulation/collisionGeometry.js +1 -1
  87. package/src/simulation/driver/driverControlConstants.js +28 -0
  88. package/src/simulation/driver/driverController.js +47 -0
  89. package/src/simulation/driver/driverInput.js +28 -0
  90. package/src/simulation/driver/driverMath.js +6 -0
  91. package/src/simulation/driver/driverPersonality.js +25 -0
  92. package/src/simulation/driver/edgeRecovery.js +48 -0
  93. package/src/simulation/driver/racingControls.js +399 -0
  94. package/src/simulation/driver/racingLinePlan.js +166 -0
  95. package/src/simulation/driver/recoveryDynamics.js +38 -0
  96. package/src/simulation/driver/rejoinControls.js +213 -0
  97. package/src/simulation/driver/safetyCarControls.js +35 -0
  98. package/src/simulation/driver/trafficScan.js +51 -0
  99. package/src/simulation/driverController.js +4 -619
  100. package/src/simulation/participants/participantInteractions.js +120 -0
  101. package/src/simulation/pit/pitFlow.js +234 -0
  102. package/src/simulation/pit/pitIntent.js +92 -0
  103. package/src/simulation/pit/pitOccupancy.js +47 -0
  104. package/src/simulation/pit/pitPenaltyService.js +71 -0
  105. package/src/simulation/pit/pitQueue.js +55 -0
  106. package/src/simulation/pit/pitRouteMovement.js +160 -0
  107. package/src/simulation/pit/pitRouting.js +319 -0
  108. package/src/simulation/pit/pitService.js +41 -0
  109. package/src/simulation/pit/pitServiceConstants.js +21 -0
  110. package/src/simulation/pit/pitServiceProfile.js +45 -0
  111. package/src/simulation/pit/pitSnapshots.js +61 -0
  112. package/src/simulation/pit/pitState.js +113 -0
  113. package/src/simulation/pit/pitTireService.js +92 -0
  114. package/src/simulation/race/classification.js +104 -0
  115. package/src/simulation/race/gridPenalties.js +37 -0
  116. package/src/simulation/race/raceControlState.js +38 -0
  117. package/src/simulation/race/raceDistance.js +32 -0
  118. package/src/simulation/race/raceFinish.js +65 -0
  119. package/src/simulation/race/raceLifecycle.js +71 -0
  120. package/src/simulation/race/raceOrder.js +113 -0
  121. package/src/simulation/race/raceProgress.js +124 -0
  122. package/src/simulation/race/raceSetup.js +93 -0
  123. package/src/simulation/race/raceStep.js +80 -0
  124. package/src/simulation/race/redFlag.js +15 -0
  125. package/src/simulation/race/retirements.js +44 -0
  126. package/src/simulation/race/safetyCar.js +86 -0
  127. package/src/simulation/race/stalledDnf.js +86 -0
  128. package/src/simulation/raceSimulation.js +230 -2817
  129. package/src/simulation/replay/replayGhosts.js +128 -0
  130. package/src/simulation/rules/moduleConfig.js +72 -0
  131. package/src/simulation/rules/penaltyConfig.js +99 -0
  132. package/src/simulation/rules/penaltyEvents.js +18 -0
  133. package/src/simulation/rules/penaltyStats.js +26 -0
  134. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  135. package/src/simulation/rules/ruleDefaults.js +85 -0
  136. package/src/simulation/rules/rulesReview.js +141 -0
  137. package/src/simulation/rules/rulesetPresets.js +47 -0
  138. package/src/simulation/rules/trackLimitsSteward.js +1 -1
  139. package/src/simulation/rulesConfig.js +5 -323
  140. package/src/simulation/snapshots/raceSnapshots.js +192 -0
  141. package/src/simulation/timing/drsTiming.js +87 -0
  142. package/src/simulation/timing/gapEstimation.js +38 -0
  143. package/src/simulation/timing/lapTelemetry.js +182 -0
  144. package/src/simulation/timing/raceTiming.js +15 -0
  145. package/src/simulation/timing/sectorPerformance.js +60 -0
  146. package/src/simulation/timing/timingConstants.js +5 -0
  147. package/src/simulation/timing/timingHistory.js +46 -0
  148. package/src/simulation/timing/timingLines.js +79 -0
  149. package/src/simulation/track/centerlineSpline.js +59 -0
  150. package/src/simulation/track/drsZones.js +67 -0
  151. package/src/simulation/track/pitLaneAccess.js +103 -0
  152. package/src/simulation/track/pitLaneLayout.js +395 -0
  153. package/src/simulation/track/pitLaneState.js +160 -0
  154. package/src/simulation/track/proceduralCenterline.js +237 -0
  155. package/src/simulation/track/sampleGeometry.js +49 -0
  156. package/src/simulation/track/spatialQueries.js +154 -0
  157. package/src/simulation/track/startStraight.js +141 -0
  158. package/src/simulation/track/trackConstants.js +109 -0
  159. package/src/simulation/track/trackGenerationOptions.js +283 -0
  160. package/src/simulation/track/trackMath.js +241 -0
  161. package/src/simulation/track/trackModel.js +240 -0
  162. package/src/simulation/track/trackQueryIndex.js +945 -0
  163. package/src/simulation/track/trackSectors.js +19 -0
  164. package/src/simulation/track/trackStatePolicy.js +24 -0
  165. package/src/simulation/track/trackValidation.js +98 -0
  166. package/src/simulation/trackModel.js +1 -1706
  167. package/src/simulation/vehicle/contactResolution.js +116 -0
  168. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  169. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  170. package/src/simulation/vehicle/runoffResponse.js +88 -0
  171. package/src/simulation/vehicle/surfacePriority.js +28 -0
  172. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  173. package/src/simulation/vehicle/vehiclePhysics.js +363 -0
  174. package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
  175. package/src/simulation/vehicle/vehicleState.js +186 -0
  176. package/src/simulation/vehicle/wheelSurface.js +150 -0
  177. package/src/simulation/vehicleGeometry.js +1 -247
  178. package/src/simulation/vehiclePhysics.js +1 -136
  179. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  180. package/src/simulation/wheelSurface.js +1 -338
  181. package/src/styles.css +42 -0
  182. package/src/ui/cameraControlsTemplate.js +20 -0
  183. package/src/ui/carOverviewTemplate.js +33 -0
  184. package/src/ui/componentTemplates.js +14 -360
  185. package/src/ui/raceCanvasTemplate.js +69 -0
  186. package/src/ui/raceControlsTemplate.js +36 -0
  187. package/src/ui/shellTemplate.js +5 -4
  188. package/src/ui/telemetryDrawerTemplate.js +55 -0
  189. package/src/ui/telemetryTemplates.js +136 -0
  190. package/src/ui/timingTowerTemplate.js +34 -0
@@ -1,27 +1,74 @@
1
- import { metersToSimUnits, simSpeedToMetersPerSecond, simUnitsToMeters } from '../simulation/units.js';
1
+ import { metersToSimUnits, simUnitsToMeters } from '../simulation/units.js';
2
2
  import { normalizeAngle } from '../simulation/simMath.js';
3
- import { pointAt } from '../simulation/trackModel.js';
3
+ import { pointAt } from '../simulation/track/trackModel.js';
4
4
  import { normalizeLookaheadMeters } from './observationOptions.js';
5
- import { buildNearbyCars, buildRaySensors } from './sensors.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, buildRaySensors, createRayBatchContext, normalizeRayOptions } from './sensors.js';
6
10
 
7
- export function buildEnvironmentObservation({ snapshot, options, events = [] }) {
11
+ const EMPTY_EVENTS = Object.freeze([]);
12
+ const PHYSICAL_RAY_SURFACE_CHANNELS = Object.freeze(['kerb', 'illegalSurface']);
13
+
14
+ export function buildEnvironmentObservation({ snapshot, options, events = [], controlledDrivers = options.controlledDrivers }) {
8
15
  const carsById = new Map(snapshot.cars.map((entry) => [entry.id, entry]));
9
- const eventsByDriver = groupEventsByDriver(events, options.controlledDrivers);
10
- return Object.fromEntries(options.controlledDrivers.map((driverId) => {
16
+ const eventsByDriver = groupEventsByDriver(events, controlledDrivers);
17
+ const defaultSensors = defaultSensorOptions(options);
18
+ const hasSensorOverrides = Object.keys(options.sensorsByDriver ?? {}).length > 0;
19
+ let rayBatchContext = null;
20
+ const getRayBatchContext = () => {
21
+ rayBatchContext ??= createRayBatchContext(snapshot);
22
+ return rayBatchContext;
23
+ };
24
+ const observations = {};
25
+ controlledDrivers.forEach((driverId) => {
11
26
  const car = carsById.get(driverId);
12
- if (!car) return [driverId, emptyObservation(driverId)];
27
+ if (!car) {
28
+ observations[driverId] = formatObservation(emptyObservation(driverId), options.observation);
29
+ return;
30
+ }
31
+
32
+ const driverEvents = eventsByDriver?.get(driverId) ?? EMPTY_EVENTS;
33
+ const sensors = effectiveSensorOptions(options, car.id, defaultSensors, hasSensorOverrides);
34
+ const output = options.observation?.output ?? 'full';
35
+ const includeSchema = options.observation?.includeSchema !== false;
36
+ const wantsObject = output === 'full' || output === 'object';
37
+ const wantsVector = output === 'full' || output === 'vector';
38
+ const object = wantsObject
39
+ ? buildDriverObservationObject(car, snapshot, options, driverEvents, sensors, getRayBatchContext)
40
+ : null;
41
+ const { vector, schema } = wantsVector
42
+ ? object
43
+ ? buildDriverVector(object, sensors, {
44
+ includeSchema,
45
+ vectorType: options.observation?.vectorType,
46
+ })
47
+ : includeSchema
48
+ ? buildDriverVector(buildDriverObservationObject(car, snapshot, options, driverEvents, sensors, getRayBatchContext), sensors, {
49
+ includeSchema,
50
+ vectorType: options.observation?.vectorType,
51
+ })
52
+ : buildDriverVectorDirect(car, snapshot, options, driverEvents, sensors, getRayBatchContext)
53
+ : { vector: undefined, schema: [] };
54
+ observations[driverId] = formatObservation({ object, vector, schema, events: driverEvents }, options.observation);
55
+ });
56
+ return observations;
57
+ }
13
58
 
14
- const driverEvents = eventsByDriver.get(driverId) ?? [];
15
- const sensors = effectiveSensorOptions(options, car.id);
16
- const object = buildDriverObservationObject(car, snapshot, options, driverEvents, sensors);
17
- const { vector, schema } = buildDriverVector(object, sensors);
18
- return [driverId, { object, vector, schema, events: driverEvents }];
19
- }));
59
+ function formatObservation(observation, options = {}) {
60
+ const output = options.output ?? 'full';
61
+ const includeSchema = options.includeSchema !== false;
62
+ const formatted = { events: observation.events ?? [] };
63
+ if ((output === 'full' || output === 'object') && observation.object) formatted.object = observation.object;
64
+ if ((output === 'full' || output === 'vector') && observation.vector) formatted.vector = observation.vector;
65
+ if (includeSchema) formatted.schema = observation.schema;
66
+ return formatted;
20
67
  }
21
68
 
22
69
  function groupEventsByDriver(events, controlledDrivers) {
70
+ if (!events.length) return null;
23
71
  const byDriver = new Map(controlledDrivers.map((driverId) => [driverId, []]));
24
- if (!events.length) return byDriver;
25
72
  const controlledSet = new Set(controlledDrivers);
26
73
  events.forEach((event) => {
27
74
  const driverIds = new Set([
@@ -37,22 +84,28 @@ function groupEventsByDriver(events, controlledDrivers) {
37
84
  return byDriver;
38
85
  }
39
86
 
40
- function buildDriverObservationObject(car, snapshot, options, events, sensors) {
87
+ function buildDriverObservationObject(car, snapshot, options, events, sensors, getRayBatchContext = null) {
88
+ const sensorCar = withEnvironmentControlFlag(car, options);
89
+ const onTrack = isCarLegallyOnTrack(car);
90
+ const body = buildBodySenses(car);
91
+ const trackHeadingError = car.trackHeadingError ?? estimateTrackHeadingError(car, snapshot);
92
+ const trackRelation = {
93
+ ...buildBoundarySenses(car, snapshot, onTrack),
94
+ headingErrorRadians: trackHeadingError,
95
+ };
96
+ const nearbyCars = sensors.nearbyCars.enabled
97
+ ? enrichOpponentRadar(car, buildNearbyCars(car, snapshot, sensors.nearbyCars), snapshot)
98
+ : [];
41
99
  return {
100
+ profile: options.observation?.profile ?? 'default',
42
101
  self: {
43
- id: car.id,
44
- speedKph: car.speedKph,
45
- speedMetersPerSecond: simSpeedToMetersPerSecond(car.speed ?? 0),
46
- headingRadians: car.heading,
47
- steeringAngleRadians: car.steeringAngle ?? 0,
48
- throttle: car.throttle ?? 0,
49
- brake: car.brake ?? 0,
102
+ ...body,
50
103
  lap: car.lap,
51
104
  completedLaps: car.lapTelemetry?.completedLaps ?? 0,
52
105
  lapProgressMeters: simUnitsToMeters(car.progress ?? 0),
53
- trackOffsetMeters: simUnitsToMeters(car.signedOffset ?? 0),
54
- trackHeadingErrorRadians: car.trackHeadingError ?? estimateTrackHeadingError(car, snapshot),
55
- onTrack: isCarLegallyOnTrack(car),
106
+ trackOffsetMeters: trackRelation.lateralOffsetMeters,
107
+ trackHeadingErrorRadians: trackHeadingError,
108
+ onTrack,
56
109
  surface: car.surface ?? 'track',
57
110
  inPitLane: Boolean(car.inPitLane),
58
111
  pitLanePart: car.pitLanePart ?? null,
@@ -66,6 +119,8 @@ function buildDriverObservationObject(car, snapshot, options, events, sensors) {
66
119
  pitStopPenaltyServiceRemainingSeconds: car.pitStop?.penaltyServiceRemainingSeconds ?? null,
67
120
  pitStopsCompleted: car.pitStop?.stopsCompleted ?? 0,
68
121
  },
122
+ trackRelation,
123
+ contactPatches: buildContactPatchSenses(car),
69
124
  race: {
70
125
  position: car.rank,
71
126
  totalCars: snapshot.cars.length,
@@ -80,8 +135,8 @@ function buildDriverObservationObject(car, snapshot, options, events, sensors) {
80
135
  curvature: car.trackState?.curvature ?? pointAt(snapshot.track, car.progress ?? 0).curvature ?? 0,
81
136
  lookahead: buildTrackLookahead(car, snapshot, options),
82
137
  },
83
- rays: sensors.rays.enabled ? buildRaySensors(car, snapshot, sensors.rays) : [],
84
- nearbyCars: sensors.nearbyCars.enabled ? buildNearbyCars(car, snapshot, sensors.nearbyCars) : [],
138
+ rays: sensors.rays.enabled ? buildRaySensors(sensorCar, snapshot, sensors.rays, rayBatchContextForSensors(sensors, getRayBatchContext)) : [],
139
+ nearbyCars,
85
140
  events,
86
141
  };
87
142
  }
@@ -92,7 +147,10 @@ function estimateTrackHeadingError(car, snapshot) {
92
147
  }
93
148
 
94
149
  function buildTrackLookahead(car, snapshot, options) {
95
- const distances = normalizeLookaheadMeters(options.observation?.lookaheadMeters);
150
+ const distances = Array.isArray(options.observation?.lookaheadMeters)
151
+ ? options.observation.lookaheadMeters
152
+ : normalizeLookaheadMeters(options.observation?.lookaheadMeters);
153
+ if (!distances.length) return [];
96
154
  const base = pointAt(snapshot.track, car.progress ?? 0);
97
155
  return distances.map((distanceMeters) => {
98
156
  const sample = pointAt(snapshot.track, (car.progress ?? 0) + metersToSimUnits(distanceMeters));
@@ -112,13 +170,19 @@ function isCarLegallyOnTrack(car) {
112
170
  return ['track', 'kerb', 'pit-entry', 'pit-lane', 'pit-exit', 'pit-box'].includes(car.surface ?? 'track');
113
171
  }
114
172
 
115
- function buildDriverVector(object, sensors) {
116
- const schema = [
173
+ function buildDriverVector(object, sensors, { includeSchema = true, vectorType = 'array' } = {}) {
174
+ const includePhysicalDriverSenses = object.profile === 'physical-driver';
175
+ const schema = includeSchema ? [
117
176
  { name: 'self.speedKph', unit: 'kph', scale: 'fixed:400' },
118
177
  { name: 'self.speedMetersPerSecond', unit: 'm/s', scale: 'fixed:120' },
119
178
  { name: 'self.steeringAngleRadians', unit: 'rad', scale: 'fixed:pi' },
120
179
  { name: 'self.throttle', scale: '0..1' },
121
180
  { name: 'self.brake', scale: '0..1' },
181
+ { name: 'self.lateralG', scale: 'fixed:8g' },
182
+ { name: 'self.longitudinalG', scale: 'fixed:6g' },
183
+ { name: 'self.gripUsage', scale: '0..2' },
184
+ { name: 'self.slipAngleRadians', unit: 'rad', scale: 'fixed:pi' },
185
+ { name: 'self.tractionLimited', scale: 'boolean' },
122
186
  { name: 'self.lapProgressRatio', scale: '0..1' },
123
187
  { name: 'self.trackOffsetMeters', unit: 'm', scale: 'fixed:meters' },
124
188
  { name: 'self.trackHeadingErrorRadians', unit: 'rad', scale: 'fixed:pi' },
@@ -133,13 +197,18 @@ function buildDriverVector(object, sensors) {
133
197
  { name: 'race.redFlag', scale: 'boolean' },
134
198
  { name: 'race.pitLaneOpen', scale: 'boolean' },
135
199
  { name: 'track.curvature', scale: 'track-curvature' },
136
- ];
200
+ ] : null;
137
201
  const vector = [
138
202
  object.self.speedKph / 400,
139
203
  object.self.speedMetersPerSecond / 120,
140
204
  object.self.steeringAngleRadians / Math.PI,
141
205
  object.self.throttle,
142
206
  object.self.brake,
207
+ object.self.lateralG / 8,
208
+ object.self.longitudinalG / 6,
209
+ object.self.gripUsage / 2,
210
+ object.self.slipAngleRadians / Math.PI,
211
+ object.self.tractionLimited ? 1 : 0,
143
212
  normalizeLapProgress(object),
144
213
  object.self.trackOffsetMeters,
145
214
  object.self.trackHeadingErrorRadians / Math.PI,
@@ -155,15 +224,43 @@ function buildDriverVector(object, sensors) {
155
224
  object.race.pitLaneOpen ? 1 : 0,
156
225
  object.track.curvature ?? 0,
157
226
  ];
227
+ if (includePhysicalDriverSenses) {
228
+ pushSchema(schema,
229
+ { name: 'self.yawRateRadiansPerSecond', unit: 'rad/s', scale: 'fixed:pi' },
230
+ { name: 'trackRelation.leftBoundaryMeters', unit: 'm', scale: 'fixed:meters' },
231
+ { name: 'trackRelation.rightBoundaryMeters', unit: 'm', scale: 'fixed:meters' },
232
+ { name: 'trackRelation.legalWidthMeters', unit: 'm', scale: 'fixed:meters' },
233
+ );
234
+ vector.push(
235
+ object.self.yawRateRadiansPerSecond / Math.PI,
236
+ object.trackRelation.leftBoundaryMeters,
237
+ object.trackRelation.rightBoundaryMeters,
238
+ object.trackRelation.legalWidthMeters,
239
+ );
240
+ object.contactPatches.forEach((patch, index) => {
241
+ pushSchema(schema,
242
+ { name: `contactPatches[${index}].present`, scale: 'boolean' },
243
+ { name: `contactPatches[${index}].surfaceCode`, scale: 'surface-code' },
244
+ { name: `contactPatches[${index}].onLegalSurface`, scale: 'boolean' },
245
+ { name: `contactPatches[${index}].signedOffsetMeters`, unit: 'm', scale: 'fixed:meters' },
246
+ );
247
+ vector.push(
248
+ patch.present ? 1 : 0,
249
+ patch.surfaceCode / 5,
250
+ patch.onLegalSurface ? 1 : 0,
251
+ patch.signedOffsetMeters,
252
+ );
253
+ });
254
+ }
158
255
  object.track.lookahead.forEach((sample, index) => {
159
- schema.push(
256
+ pushSchema(schema,
160
257
  { name: `track.lookahead[${index}].curvature`, scale: 'track-curvature' },
161
258
  { name: `track.lookahead[${index}].headingDeltaRadians`, unit: 'rad', scale: 'fixed:pi' },
162
259
  );
163
260
  vector.push(sample.curvature ?? 0, (sample.headingDeltaRadians ?? 0) / Math.PI);
164
261
  });
165
262
  object.rays.forEach((ray, index) => {
166
- schema.push(
263
+ pushSchema(schema,
167
264
  { name: `rays[${index}].track.distanceRatio`, scale: '0..1' },
168
265
  { name: `rays[${index}].track.hit`, scale: 'boolean' },
169
266
  { name: `rays[${index}].track.kindExit`, scale: 'boolean' },
@@ -181,12 +278,24 @@ function buildDriverVector(object, sensors) {
181
278
  ray.car.hit ? 1 : 0,
182
279
  ray.car.relativeSpeedKph / 200,
183
280
  );
281
+ if (includePhysicalDriverSenses) {
282
+ PHYSICAL_RAY_SURFACE_CHANNELS.forEach((channel) => {
283
+ pushSchema(schema,
284
+ { name: `rays[${index}].${channel}.distanceRatio`, scale: '0..1' },
285
+ { name: `rays[${index}].${channel}.hit`, scale: 'boolean' },
286
+ );
287
+ vector.push(
288
+ ratio(ray[channel]?.distanceMeters ?? ray.lengthMeters, ray.lengthMeters),
289
+ ray[channel]?.hit ? 1 : 0,
290
+ );
291
+ });
292
+ }
184
293
  });
185
294
  const nearbyLimit = sensors.nearbyCars.enabled ? (sensors.nearbyCars.maxCars ?? object.nearbyCars.length) : 0;
186
295
  const nearbyRadius = sensors.nearbyCars.radiusMeters ?? 150;
187
296
  for (let index = 0; index < nearbyLimit; index += 1) {
188
297
  const nearby = object.nearbyCars[index] ?? null;
189
- schema.push(
298
+ pushSchema(schema,
190
299
  { name: `nearbyCars[${index}].present`, scale: 'boolean' },
191
300
  { name: `nearbyCars[${index}].relativeForwardRatio`, scale: '-1..1' },
192
301
  { name: `nearbyCars[${index}].relativeRightRatio`, scale: '-1..1' },
@@ -206,8 +315,146 @@ function buildDriverVector(object, sensors) {
206
315
  nearby?.ahead ? 1 : 0,
207
316
  nearby?.sameLap ? 1 : 0,
208
317
  );
318
+ if (includePhysicalDriverSenses) {
319
+ pushSchema(schema,
320
+ { name: `nearbyCars[${index}].behind`, scale: 'boolean' },
321
+ { name: `nearbyCars[${index}].closingRateMetersPerSecond`, unit: 'm/s', scale: 'fixed:100' },
322
+ { name: `nearbyCars[${index}].timeToContactSeconds`, unit: 's', scale: 'fixed:10' },
323
+ { name: `nearbyCars[${index}].leftOverlap`, scale: 'boolean' },
324
+ { name: `nearbyCars[${index}].rightOverlap`, scale: 'boolean' },
325
+ );
326
+ vector.push(
327
+ nearby?.behind ? 1 : 0,
328
+ (nearby?.closingRateMetersPerSecond ?? 0) / 100,
329
+ ratio(nearby?.timeToContactSeconds ?? 10, 10),
330
+ nearby?.leftOverlap ? 1 : 0,
331
+ nearby?.rightOverlap ? 1 : 0,
332
+ );
333
+ }
209
334
  }
210
- return { vector, schema };
335
+ return { vector: finalizeVector(vector, vectorType), schema: schema ?? [] };
336
+ }
337
+
338
+ function buildDriverVectorDirect(car, snapshot, options, events, sensors, getRayBatchContext = null) {
339
+ const profile = options.observation?.profile ?? 'default';
340
+ const includePhysicalDriverSenses = profile === 'physical-driver';
341
+ const onTrack = isCarLegallyOnTrack(car);
342
+ const body = buildBodySenses(car);
343
+ const trackHeadingError = car.trackHeadingError ?? estimateTrackHeadingError(car, snapshot);
344
+ const sensorCar = withEnvironmentControlFlag(car, options);
345
+ const trackRelation = {
346
+ ...buildBoundarySenses(car, snapshot, onTrack),
347
+ headingErrorRadians: trackHeadingError,
348
+ };
349
+ const contactPatches = includePhysicalDriverSenses ? buildContactPatchSenses(car) : [];
350
+ const rays = sensors.rays.enabled ? buildRaySensors(sensorCar, snapshot, sensors.rays, rayBatchContextForSensors(sensors, getRayBatchContext)) : [];
351
+ const nearbyCars = sensors.nearbyCars.enabled
352
+ ? enrichOpponentRadar(car, buildNearbyCars(car, snapshot, sensors.nearbyCars), snapshot)
353
+ : [];
354
+ const trackLengthMeters = simUnitsToMeters(snapshot.track.length ?? 0);
355
+ const trackCurvature = car.trackState?.curvature ?? pointAt(snapshot.track, car.progress ?? 0).curvature ?? 0;
356
+ const vector = [
357
+ body.speedKph / 400,
358
+ body.speedMetersPerSecond / 120,
359
+ body.steeringAngleRadians / Math.PI,
360
+ body.throttle,
361
+ body.brake,
362
+ body.lateralG / 8,
363
+ body.longitudinalG / 6,
364
+ body.gripUsage / 2,
365
+ body.slipAngleRadians / Math.PI,
366
+ body.tractionLimited ? 1 : 0,
367
+ ratio(simUnitsToMeters(car.progress ?? 0), trackLengthMeters || 1),
368
+ trackRelation.lateralOffsetMeters,
369
+ trackHeadingError / Math.PI,
370
+ onTrack ? 1 : 0,
371
+ car.inPitLane ? 1 : 0,
372
+ (car.tireEnergy ?? 0) / 100,
373
+ (car.pitIntent ?? car.pitStop?.intent ?? 0) / 2,
374
+ car.pitStop?.status && car.pitStop.status !== 'pending' && car.pitStop.status !== 'completed' ? 1 : 0,
375
+ snapshot.cars.length <= 1 ? 0 : ((car.rank ?? 1) - 1) / Math.max(1, snapshot.cars.length - 1),
376
+ snapshot.raceControl.mode === 'green' ? 1 : 0,
377
+ snapshot.raceControl.mode === 'safety-car' ? 1 : 0,
378
+ snapshot.raceControl.redFlag ? 1 : 0,
379
+ snapshot.raceControl.pitLaneOpen ? 1 : 0,
380
+ trackCurvature,
381
+ ];
382
+ if (includePhysicalDriverSenses) {
383
+ vector.push(
384
+ body.yawRateRadiansPerSecond / Math.PI,
385
+ trackRelation.leftBoundaryMeters,
386
+ trackRelation.rightBoundaryMeters,
387
+ trackRelation.legalWidthMeters,
388
+ );
389
+ contactPatches.forEach((patch) => {
390
+ vector.push(
391
+ patch.present ? 1 : 0,
392
+ patch.surfaceCode / 5,
393
+ patch.onLegalSurface ? 1 : 0,
394
+ patch.signedOffsetMeters,
395
+ );
396
+ });
397
+ }
398
+ buildTrackLookahead(car, snapshot, options).forEach((sample) => {
399
+ vector.push(sample.curvature ?? 0, (sample.headingDeltaRadians ?? 0) / Math.PI);
400
+ });
401
+ rays.forEach((ray) => {
402
+ vector.push(
403
+ ratio(ray.track.distanceMeters, ray.lengthMeters),
404
+ ray.track.hit ? 1 : 0,
405
+ ray.track.kind === 'exit' ? 1 : 0,
406
+ ray.track.kind === 'entry' ? 1 : 0,
407
+ ratio(ray.car.distanceMeters, ray.lengthMeters),
408
+ ray.car.hit ? 1 : 0,
409
+ ray.car.relativeSpeedKph / 200,
410
+ );
411
+ if (includePhysicalDriverSenses) {
412
+ PHYSICAL_RAY_SURFACE_CHANNELS.forEach((channel) => {
413
+ vector.push(
414
+ ratio(ray[channel]?.distanceMeters ?? ray.lengthMeters, ray.lengthMeters),
415
+ ray[channel]?.hit ? 1 : 0,
416
+ );
417
+ });
418
+ }
419
+ });
420
+ const nearbyLimit = sensors.nearbyCars.enabled ? (sensors.nearbyCars.maxCars ?? nearbyCars.length) : 0;
421
+ const nearbyRadius = sensors.nearbyCars.radiusMeters ?? 150;
422
+ for (let index = 0; index < nearbyLimit; index += 1) {
423
+ const nearby = nearbyCars[index] ?? null;
424
+ vector.push(
425
+ nearby ? 1 : 0,
426
+ clampRatio((nearby?.relativeForwardMeters ?? 0) / nearbyRadius),
427
+ clampRatio((nearby?.relativeRightMeters ?? 0) / nearbyRadius),
428
+ ratio(nearby?.relativeDistanceMeters ?? nearbyRadius, nearbyRadius),
429
+ (nearby?.relativeSpeedKph ?? 0) / 200,
430
+ (nearby?.relativeHeadingRadians ?? 0) / Math.PI,
431
+ nearby?.ahead ? 1 : 0,
432
+ nearby?.sameLap ? 1 : 0,
433
+ );
434
+ if (includePhysicalDriverSenses) {
435
+ vector.push(
436
+ nearby?.behind ? 1 : 0,
437
+ (nearby?.closingRateMetersPerSecond ?? 0) / 100,
438
+ ratio(nearby?.timeToContactSeconds ?? 10, 10),
439
+ nearby?.leftOverlap ? 1 : 0,
440
+ nearby?.rightOverlap ? 1 : 0,
441
+ );
442
+ }
443
+ }
444
+ return { vector: finalizeVector(vector, options.observation?.vectorType), schema: [] };
445
+ }
446
+
447
+ function rayBatchContextForSensors(sensors, getRayBatchContext) {
448
+ if (!getRayBatchContext || !sensors.rays?.channels?.includes('car')) return null;
449
+ return getRayBatchContext();
450
+ }
451
+
452
+ function finalizeVector(vector, vectorType = 'array') {
453
+ return vectorType === 'float32' ? Float32Array.from(vector) : vector;
454
+ }
455
+
456
+ function pushSchema(schema, ...entries) {
457
+ if (schema) schema.push(...entries);
211
458
  }
212
459
 
213
460
  function normalizeLapProgress(object) {
@@ -215,8 +462,10 @@ function normalizeLapProgress(object) {
215
462
  }
216
463
 
217
464
  function normalizeRacePosition(object) {
218
- const total = Math.max(1, object.race.totalCars - 1);
219
- return object.race.totalCars <= 1 ? 0 : (object.race.position - 1) / total;
465
+ const totalCars = Number(object.race.totalCars);
466
+ const position = Number(object.race.position);
467
+ if (!Number.isFinite(totalCars) || totalCars <= 1 || !Number.isFinite(position)) return 0;
468
+ return ratio(position - 1, totalCars - 1);
220
469
  }
221
470
 
222
471
  function ratio(value, max) {
@@ -228,12 +477,20 @@ function clampRatio(value) {
228
477
  return Math.max(-1, Math.min(1, Number.isFinite(value) ? value : 0));
229
478
  }
230
479
 
231
- function effectiveSensorOptions(options, driverId) {
480
+ function defaultSensorOptions(options) {
481
+ return {
482
+ rays: normalizeRayOptions(options.sensors.rays),
483
+ nearbyCars: options.sensors.nearbyCars,
484
+ };
485
+ }
486
+
487
+ function effectiveSensorOptions(options, driverId, defaultSensors = defaultSensorOptions(options), hasSensorOverrides = true) {
488
+ if (!hasSensorOverrides || !options.sensorsByDriver?.[driverId]) return defaultSensors;
232
489
  return {
233
- rays: {
490
+ rays: normalizeRayOptions({
234
491
  ...options.sensors.rays,
235
492
  ...(options.sensorsByDriver?.[driverId]?.rays ?? {}),
236
- },
493
+ }),
237
494
  nearbyCars: {
238
495
  ...options.sensors.nearbyCars,
239
496
  ...(options.sensorsByDriver?.[driverId]?.nearbyCars ?? {}),
@@ -249,9 +506,18 @@ function emptyObservation(driverId) {
249
506
  speedKph: 0,
250
507
  speedMetersPerSecond: 0,
251
508
  headingRadians: 0,
509
+ yawRateRadiansPerSecond: 0,
252
510
  steeringAngleRadians: 0,
253
511
  throttle: 0,
254
512
  brake: 0,
513
+ lateralG: 0,
514
+ longitudinalG: 0,
515
+ gripUsage: 0,
516
+ slipAngleRadians: 0,
517
+ tractionLimited: false,
518
+ stabilityState: 'stable',
519
+ destroyed: false,
520
+ destroyReason: null,
255
521
  lap: 0,
256
522
  completedLaps: 0,
257
523
  lapProgressMeters: 0,
@@ -271,6 +537,16 @@ function emptyObservation(driverId) {
271
537
  pitStopPenaltyServiceRemainingSeconds: null,
272
538
  pitStopsCompleted: 0,
273
539
  },
540
+ trackRelation: {
541
+ lateralOffsetMeters: 0,
542
+ headingErrorRadians: 0,
543
+ legalWidthMeters: 0,
544
+ leftBoundaryMeters: 0,
545
+ rightBoundaryMeters: 0,
546
+ onLegalSurface: false,
547
+ surface: 'missing',
548
+ },
549
+ contactPatches: [],
274
550
  race: { position: 0, totalCars: 0, raceMode: 'missing', pitLaneOpen: false, redFlag: false, totalLaps: 0 },
275
551
  track: { lengthMeters: 0, widthMeters: 0, curvature: 0, lookahead: [] },
276
552
  rays: [],
@@ -282,3 +558,12 @@ function emptyObservation(driverId) {
282
558
  events: [],
283
559
  };
284
560
  }
561
+
562
+ function withEnvironmentControlFlag(car, options) {
563
+ if (!car || car.environmentControlled) return car;
564
+ if (!Array.isArray(options?.controlledDrivers) || !options.controlledDrivers.includes(car.id)) return car;
565
+ return {
566
+ ...car,
567
+ environmentControlled: true,
568
+ };
569
+ }
@@ -1,11 +1,16 @@
1
1
  import { CHAMPIONSHIP_ENTRY_BLUEPRINTS } from '../data/championship.js';
2
2
  import { normalizeSimulatorDrivers } from '../data/normalizeDrivers.js';
3
- import { normalizeLookaheadMeters } from './observationOptions.js';
3
+ import { normalizePhysicsMode } from '../simulation/vehicle/vehiclePhysics.js';
4
+ import { normalizeWarmupOptions } from '../simulation/warmup/runtimeWarmup.js';
5
+ import { normalizeObservationOptions } from './observationOptions.js';
4
6
  import { resolveScenarioPlacementConfig } from './scenarios.js';
7
+ import { normalizeRayOptions } from './sensors.js';
5
8
 
6
9
  const DEFAULT_SEED = 1971;
7
10
  const DEFAULT_FRAME_SKIP = 1;
8
11
  const DEFAULT_MAX_STEPS = 10000;
12
+ const RESULT_STATE_OUTPUTS = new Set(['full', 'minimal', 'none']);
13
+ const RESET_OBSERVATION_SCOPES = new Set(['all', 'reset']);
9
14
 
10
15
  const DEFAULT_SENSOR_OPTIONS = {
11
16
  rays: {
@@ -37,6 +42,7 @@ export function resolveEnvironmentOptions(options = {}) {
37
42
  ...options,
38
43
  seed,
39
44
  trackSeed,
45
+ trackGeneration: options.trackGeneration ?? {},
40
46
  totalLaps: options.totalLaps,
41
47
  drivers: normalizedDrivers,
42
48
  };
@@ -60,16 +66,19 @@ export function resolveEnvironmentOptions(options = {}) {
60
66
  }
61
67
  });
62
68
 
69
+ const externalRenderer = normalizeExternalRenderer(options.externalRenderer);
70
+
63
71
  return {
64
72
  ...resolved,
73
+ physicsMode: normalizePhysicsMode(options.physicsMode),
74
+ warmup: normalizeWarmupOptions(options.warmup, 'environment'),
65
75
  drivers: participantDrivers,
66
76
  controlledDrivers,
67
77
  frameSkip: normalizePositiveInteger(options.frameSkip, DEFAULT_FRAME_SKIP, 'frameSkip'),
68
78
  actionPolicy: options.actionPolicy === 'report' ? 'report' : 'strict',
69
79
  scenario,
70
80
  observation: {
71
- ...(options.observation ?? {}),
72
- lookaheadMeters: normalizeLookaheadMeters(options.observation?.lookaheadMeters),
81
+ ...normalizeObservationOptions(options.observation ?? {}),
73
82
  },
74
83
  sensors: mergeSensorOptions(options.sensors),
75
84
  sensorsByDriver: options.sensorsByDriver ?? {},
@@ -77,7 +86,21 @@ export function resolveEnvironmentOptions(options = {}) {
77
86
  maxSteps: normalizePositiveInteger(options.episode?.maxSteps, DEFAULT_MAX_STEPS, 'episode.maxSteps'),
78
87
  endOnRaceFinish: options.episode?.endOnRaceFinish !== false,
79
88
  },
89
+ result: normalizeResultOptions(options.result),
80
90
  reward: typeof options.reward === 'function' ? options.reward : null,
91
+ externalRenderer,
92
+ };
93
+ }
94
+
95
+ export function normalizeResultOptions(result = {}) {
96
+ const stateOutput = RESULT_STATE_OUTPUTS.has(result.stateOutput) ? result.stateOutput : 'full';
97
+ const resetDriversObservationScope = RESET_OBSERVATION_SCOPES.has(result.resetDriversObservationScope)
98
+ ? result.resetDriversObservationScope
99
+ : 'all';
100
+ return {
101
+ ...result,
102
+ stateOutput,
103
+ resetDriversObservationScope,
81
104
  };
82
105
  }
83
106
 
@@ -145,13 +168,22 @@ function normalizePositiveInteger(value, fallback, label) {
145
168
 
146
169
  function mergeSensorOptions(sensors = {}) {
147
170
  return {
148
- rays: {
171
+ rays: normalizeRayOptions({
149
172
  ...DEFAULT_SENSOR_OPTIONS.rays,
150
173
  ...(sensors.rays ?? {}),
151
- },
174
+ }),
152
175
  nearbyCars: {
153
176
  ...DEFAULT_SENSOR_OPTIONS.nearbyCars,
154
177
  ...(sensors.nearbyCars ?? {}),
155
178
  },
156
179
  };
157
180
  }
181
+
182
+ function normalizeExternalRenderer(value) {
183
+ if (value == null) return null;
184
+ if (typeof value === 'function') return { onFrame: value };
185
+ if (typeof value === 'object' && typeof value.onFrame === 'function') {
186
+ return { onFrame: value.onFrame };
187
+ }
188
+ throw new Error('PaddockJS environment externalRenderer must be a function or an object with onFrame(frame).');
189
+ }