@inventure71/paddockjs 1.0.0 → 2.0.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 (203) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/INSTALL_AND_UPDATE.md +4 -3
  3. package/README.md +219 -12
  4. package/docs/architecture.md +126 -45
  5. package/docs/concepts.md +9 -5
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +317 -22
  8. package/docs/data_model.md +1 -1
  9. package/docs/index.md +10 -0
  10. package/docs/learnings.md +22 -0
  11. package/docs/rules.md +71 -14
  12. package/docs/sense_contract.md +59 -0
  13. package/docs/system_specs.md +75 -13
  14. package/docs/training.md +280 -32
  15. package/examples/python/README.md +62 -0
  16. package/examples/python/base_policy_server.py +191 -0
  17. package/examples/python/requirements.txt +2 -0
  18. package/package.json +12 -4
  19. package/src/api/PaddockSimulatorController.js +6 -33
  20. package/src/app/BrowserExpertAdapter.js +76 -9
  21. package/src/app/BrowserExpertExternalRenderer.js +303 -0
  22. package/src/app/F1SimulatorApp.js +291 -1630
  23. package/src/app/banners/bannerFormatters.js +55 -0
  24. package/src/app/banners/raceDataBannerController.js +141 -0
  25. package/src/app/banners/stewardMessageController.js +99 -0
  26. package/src/app/camera/cameraConstants.js +21 -0
  27. package/src/app/camera/cameraController.js +265 -0
  28. package/src/app/camera/cameraGeometry.js +176 -0
  29. package/src/app/domBindings.js +4 -0
  30. package/src/app/readouts/carOverviewRenderer.js +109 -0
  31. package/src/app/readouts/controlStateRenderer.js +37 -0
  32. package/src/app/readouts/raceStatusRenderer.js +133 -0
  33. package/src/app/readouts/readoutFormatters.js +58 -0
  34. package/src/app/readouts/telemetryRenderer.js +113 -0
  35. package/src/app/readouts/timingTowerRenderer.js +177 -0
  36. package/src/app/rendering/appAssets.js +38 -0
  37. package/src/app/rendering/carRenderer.js +286 -0
  38. package/src/app/rendering/displayUtils.js +22 -0
  39. package/src/app/rendering/drsTrailRenderer.js +90 -0
  40. package/src/app/rendering/expertSensorRenderer.js +126 -0
  41. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  42. package/src/app/rendering/replayGhostRenderer.js +124 -0
  43. package/src/app/rendering/trackRenderer.js +25 -0
  44. package/src/app/runtime/frameLoop.js +66 -0
  45. package/src/app/runtime/runtimeTiming.js +34 -0
  46. package/src/app/runtime/runtimeVisibility.js +45 -0
  47. package/src/config/defaultOptions.js +20 -0
  48. package/src/config/restartOptions.js +34 -0
  49. package/src/environment/actions.js +13 -5
  50. package/src/environment/controllerLoop.js +261 -0
  51. package/src/environment/episode.js +98 -2
  52. package/src/environment/evaluation.js +54 -32
  53. package/src/environment/index.d.ts +376 -10
  54. package/src/environment/index.js +1 -0
  55. package/src/environment/metrics.js +159 -0
  56. package/src/environment/observationOptions.js +22 -1
  57. package/src/environment/observationVector.js +200 -0
  58. package/src/environment/observations.js +199 -141
  59. package/src/environment/options.js +37 -5
  60. package/src/environment/recorder.js +13 -6
  61. package/src/environment/rewards.js +13 -2
  62. package/src/environment/runtime.js +272 -23
  63. package/src/environment/scenarios.js +57 -9
  64. package/src/environment/sensors/bodySenses.js +23 -0
  65. package/src/environment/sensors/boundarySenses.js +17 -0
  66. package/src/environment/sensors/carRays.js +75 -0
  67. package/src/environment/sensors/contactSenses.js +32 -0
  68. package/src/environment/sensors/index.js +157 -0
  69. package/src/environment/sensors/indexedRayBands.js +103 -0
  70. package/src/environment/sensors/nearbyCars.js +52 -0
  71. package/src/environment/sensors/opponentRadar.js +75 -0
  72. package/src/environment/sensors/rayConfig.js +110 -0
  73. package/src/environment/sensors/rayDefaults.js +6 -0
  74. package/src/environment/sensors/rayGeometry.js +60 -0
  75. package/src/environment/sensors/rayGuards.js +19 -0
  76. package/src/environment/sensors/sensorTargets.js +66 -0
  77. package/src/environment/sensors/surfaceRays.js +360 -0
  78. package/src/environment/sensors/trackRays.js +424 -0
  79. package/src/environment/sensors.js +1 -367
  80. package/src/environment/specs.js +159 -26
  81. package/src/environment/workerProtocol.js +13 -1
  82. package/src/index.d.ts +171 -6
  83. package/src/index.js +6 -32
  84. package/src/rendering/proceduralTrackAsset.js +20 -823
  85. package/src/rendering/renderSnapshot.js +38 -12
  86. package/src/rendering/track/finishLineRenderer.js +34 -0
  87. package/src/rendering/track/gridRenderer.js +47 -0
  88. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  89. package/src/rendering/track/pitLaneRenderer.js +248 -0
  90. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  91. package/src/rendering/track/trackRenderConstants.js +38 -0
  92. package/src/rendering/track/trackRenderGeometry.js +94 -0
  93. package/src/simulation/collisionGeometry.js +1 -1
  94. package/src/simulation/driver/driverControlConstants.js +28 -0
  95. package/src/simulation/driver/driverController.js +47 -0
  96. package/src/simulation/driver/driverInput.js +28 -0
  97. package/src/simulation/driver/driverMath.js +6 -0
  98. package/src/simulation/driver/driverPersonality.js +25 -0
  99. package/src/simulation/driver/edgeRecovery.js +48 -0
  100. package/src/simulation/driver/racingControls.js +399 -0
  101. package/src/simulation/driver/racingLinePlan.js +166 -0
  102. package/src/simulation/driver/recoveryDynamics.js +38 -0
  103. package/src/simulation/driver/rejoinControls.js +213 -0
  104. package/src/simulation/driver/safetyCarControls.js +35 -0
  105. package/src/simulation/driver/trafficScan.js +51 -0
  106. package/src/simulation/driverController.js +4 -619
  107. package/src/simulation/participants/participantInteractions.js +120 -0
  108. package/src/simulation/pit/pitFlow.js +239 -0
  109. package/src/simulation/pit/pitIntent.js +92 -0
  110. package/src/simulation/pit/pitKinematics.js +24 -0
  111. package/src/simulation/pit/pitOccupancy.js +47 -0
  112. package/src/simulation/pit/pitPenaltyService.js +71 -0
  113. package/src/simulation/pit/pitQueue.js +60 -0
  114. package/src/simulation/pit/pitRouteMovement.js +167 -0
  115. package/src/simulation/pit/pitRouting.js +319 -0
  116. package/src/simulation/pit/pitService.js +43 -0
  117. package/src/simulation/pit/pitServiceConstants.js +21 -0
  118. package/src/simulation/pit/pitServiceProfile.js +45 -0
  119. package/src/simulation/pit/pitSnapshots.js +61 -0
  120. package/src/simulation/pit/pitState.js +113 -0
  121. package/src/simulation/pit/pitTireService.js +94 -0
  122. package/src/simulation/race/classification.js +104 -0
  123. package/src/simulation/race/gridPenalties.js +37 -0
  124. package/src/simulation/race/raceControlState.js +38 -0
  125. package/src/simulation/race/raceDistance.js +32 -0
  126. package/src/simulation/race/raceFinish.js +65 -0
  127. package/src/simulation/race/raceLifecycle.js +67 -0
  128. package/src/simulation/race/raceOrder.js +113 -0
  129. package/src/simulation/race/raceProgress.js +129 -0
  130. package/src/simulation/race/raceSetup.js +93 -0
  131. package/src/simulation/race/raceStep.js +80 -0
  132. package/src/simulation/race/redFlag.js +15 -0
  133. package/src/simulation/race/retirements.js +44 -0
  134. package/src/simulation/race/safetyCar.js +92 -0
  135. package/src/simulation/race/stalledDnf.js +77 -0
  136. package/src/simulation/raceSimulation.js +231 -2817
  137. package/src/simulation/replay/replayGhosts.js +128 -0
  138. package/src/simulation/rules/moduleConfig.js +72 -0
  139. package/src/simulation/rules/penaltyConfig.js +99 -0
  140. package/src/simulation/rules/penaltyEvents.js +18 -0
  141. package/src/simulation/rules/penaltyStats.js +26 -0
  142. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  143. package/src/simulation/rules/ruleDefaults.js +85 -0
  144. package/src/simulation/rules/rulesReview.js +144 -0
  145. package/src/simulation/rules/rulesetPresets.js +47 -0
  146. package/src/simulation/rules/trackLimitsSteward.js +1 -1
  147. package/src/simulation/rulesConfig.js +5 -323
  148. package/src/simulation/snapshots/raceSnapshots.js +193 -0
  149. package/src/simulation/timing/drsTiming.js +87 -0
  150. package/src/simulation/timing/gapEstimation.js +38 -0
  151. package/src/simulation/timing/lapTelemetry.js +182 -0
  152. package/src/simulation/timing/raceTiming.js +15 -0
  153. package/src/simulation/timing/sectorPerformance.js +60 -0
  154. package/src/simulation/timing/timingConstants.js +5 -0
  155. package/src/simulation/timing/timingHistory.js +46 -0
  156. package/src/simulation/timing/timingLines.js +79 -0
  157. package/src/simulation/track/centerlineSpline.js +59 -0
  158. package/src/simulation/track/drsZones.js +67 -0
  159. package/src/simulation/track/pitLaneAccess.js +103 -0
  160. package/src/simulation/track/pitLaneLayout.js +395 -0
  161. package/src/simulation/track/pitLaneState.js +160 -0
  162. package/src/simulation/track/proceduralCenterline.js +237 -0
  163. package/src/simulation/track/sampleGeometry.js +49 -0
  164. package/src/simulation/track/spatialQueries.js +154 -0
  165. package/src/simulation/track/startStraight.js +141 -0
  166. package/src/simulation/track/trackConstants.js +109 -0
  167. package/src/simulation/track/trackGenerationOptions.js +283 -0
  168. package/src/simulation/track/trackMath.js +241 -0
  169. package/src/simulation/track/trackModel.js +240 -0
  170. package/src/simulation/track/trackQueryGrid.js +94 -0
  171. package/src/simulation/track/trackQueryIndex.js +659 -0
  172. package/src/simulation/track/trackQueryPitIndex.js +140 -0
  173. package/src/simulation/track/trackQueryScratch.js +46 -0
  174. package/src/simulation/track/trackQueryStats.js +28 -0
  175. package/src/simulation/track/trackSectors.js +19 -0
  176. package/src/simulation/track/trackStatePolicy.js +24 -0
  177. package/src/simulation/track/trackValidation.js +98 -0
  178. package/src/simulation/trackModel.js +1 -1706
  179. package/src/simulation/vehicle/contactResolution.js +181 -0
  180. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  181. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  182. package/src/simulation/vehicle/runoffResponse.js +87 -0
  183. package/src/simulation/vehicle/surfacePriority.js +28 -0
  184. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  185. package/src/simulation/vehicle/vehicleKinematics.js +37 -0
  186. package/src/simulation/vehicle/vehiclePhysics.js +364 -0
  187. package/src/simulation/vehicle/vehicleSnapshots.js +327 -0
  188. package/src/simulation/vehicle/vehicleState.js +220 -0
  189. package/src/simulation/vehicle/wheelSurface.js +152 -0
  190. package/src/simulation/vehicleGeometry.js +1 -247
  191. package/src/simulation/vehiclePhysics.js +1 -136
  192. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  193. package/src/simulation/wheelSurface.js +1 -338
  194. package/src/styles.css +42 -0
  195. package/src/ui/cameraControlsTemplate.js +20 -0
  196. package/src/ui/carOverviewTemplate.js +33 -0
  197. package/src/ui/componentTemplates.js +14 -360
  198. package/src/ui/raceCanvasTemplate.js +69 -0
  199. package/src/ui/raceControlsTemplate.js +36 -0
  200. package/src/ui/shellTemplate.js +5 -4
  201. package/src/ui/telemetryDrawerTemplate.js +55 -0
  202. package/src/ui/telemetryTemplates.js +136 -0
  203. 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';
10
+ import { buildObservationVector } from './observationVector.js';
6
11
 
7
- export function buildEnvironmentObservation({ snapshot, options, events = [] }) {
12
+ const EMPTY_EVENTS = Object.freeze([]);
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,128 +170,81 @@ 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 = [
117
- { name: 'self.speedKph', unit: 'kph', scale: 'fixed:400' },
118
- { name: 'self.speedMetersPerSecond', unit: 'm/s', scale: 'fixed:120' },
119
- { name: 'self.steeringAngleRadians', unit: 'rad', scale: 'fixed:pi' },
120
- { name: 'self.throttle', scale: '0..1' },
121
- { name: 'self.brake', scale: '0..1' },
122
- { name: 'self.lapProgressRatio', scale: '0..1' },
123
- { name: 'self.trackOffsetMeters', unit: 'm', scale: 'fixed:meters' },
124
- { name: 'self.trackHeadingErrorRadians', unit: 'rad', scale: 'fixed:pi' },
125
- { name: 'self.onTrack', scale: 'boolean' },
126
- { name: 'self.inPitLane', scale: 'boolean' },
127
- { name: 'self.tireEnergy', scale: '0..100' },
128
- { name: 'self.pitIntent', scale: '0..2' },
129
- { name: 'self.pitStopActive', scale: 'boolean' },
130
- { name: 'race.positionNormalized', scale: '0..1' },
131
- { name: 'race.raceModeGreen', scale: 'boolean' },
132
- { name: 'race.raceModeSafetyCar', scale: 'boolean' },
133
- { name: 'race.redFlag', scale: 'boolean' },
134
- { name: 'race.pitLaneOpen', scale: 'boolean' },
135
- { name: 'track.curvature', scale: 'track-curvature' },
136
- ];
137
- const vector = [
138
- object.self.speedKph / 400,
139
- object.self.speedMetersPerSecond / 120,
140
- object.self.steeringAngleRadians / Math.PI,
141
- object.self.throttle,
142
- object.self.brake,
143
- normalizeLapProgress(object),
144
- object.self.trackOffsetMeters,
145
- object.self.trackHeadingErrorRadians / Math.PI,
146
- object.self.onTrack ? 1 : 0,
147
- object.self.inPitLane ? 1 : 0,
148
- (object.self.tireEnergy ?? 0) / 100,
149
- (object.self.pitIntent ?? 0) / 2,
150
- object.self.pitStopStatus && object.self.pitStopStatus !== 'pending' && object.self.pitStopStatus !== 'completed' ? 1 : 0,
151
- normalizeRacePosition(object),
152
- object.race.raceMode === 'green' ? 1 : 0,
153
- object.race.raceMode === 'safety-car' ? 1 : 0,
154
- object.race.redFlag ? 1 : 0,
155
- object.race.pitLaneOpen ? 1 : 0,
156
- object.track.curvature ?? 0,
157
- ];
158
- object.track.lookahead.forEach((sample, index) => {
159
- schema.push(
160
- { name: `track.lookahead[${index}].curvature`, scale: 'track-curvature' },
161
- { name: `track.lookahead[${index}].headingDeltaRadians`, unit: 'rad', scale: 'fixed:pi' },
162
- );
163
- vector.push(sample.curvature ?? 0, (sample.headingDeltaRadians ?? 0) / Math.PI);
164
- });
165
- object.rays.forEach((ray, index) => {
166
- schema.push(
167
- { name: `rays[${index}].track.distanceRatio`, scale: '0..1' },
168
- { name: `rays[${index}].track.hit`, scale: 'boolean' },
169
- { name: `rays[${index}].track.kindExit`, scale: 'boolean' },
170
- { name: `rays[${index}].track.kindEntry`, scale: 'boolean' },
171
- { name: `rays[${index}].car.distanceRatio`, scale: '0..1' },
172
- { name: `rays[${index}].car.hit`, scale: 'boolean' },
173
- { name: `rays[${index}].car.relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
174
- );
175
- vector.push(
176
- ratio(ray.track.distanceMeters, ray.lengthMeters),
177
- ray.track.hit ? 1 : 0,
178
- ray.track.kind === 'exit' ? 1 : 0,
179
- ray.track.kind === 'entry' ? 1 : 0,
180
- ratio(ray.car.distanceMeters, ray.lengthMeters),
181
- ray.car.hit ? 1 : 0,
182
- ray.car.relativeSpeedKph / 200,
183
- );
184
- });
185
- const nearbyLimit = sensors.nearbyCars.enabled ? (sensors.nearbyCars.maxCars ?? object.nearbyCars.length) : 0;
186
- const nearbyRadius = sensors.nearbyCars.radiusMeters ?? 150;
187
- for (let index = 0; index < nearbyLimit; index += 1) {
188
- const nearby = object.nearbyCars[index] ?? null;
189
- schema.push(
190
- { name: `nearbyCars[${index}].present`, scale: 'boolean' },
191
- { name: `nearbyCars[${index}].relativeForwardRatio`, scale: '-1..1' },
192
- { name: `nearbyCars[${index}].relativeRightRatio`, scale: '-1..1' },
193
- { name: `nearbyCars[${index}].relativeDistanceRatio`, scale: '0..1' },
194
- { name: `nearbyCars[${index}].relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
195
- { name: `nearbyCars[${index}].relativeHeadingRadians`, unit: 'rad', scale: 'fixed:pi' },
196
- { name: `nearbyCars[${index}].ahead`, scale: 'boolean' },
197
- { name: `nearbyCars[${index}].sameLap`, scale: 'boolean' },
198
- );
199
- vector.push(
200
- nearby ? 1 : 0,
201
- clampRatio((nearby?.relativeForwardMeters ?? 0) / nearbyRadius),
202
- clampRatio((nearby?.relativeRightMeters ?? 0) / nearbyRadius),
203
- ratio(nearby?.relativeDistanceMeters ?? nearbyRadius, nearbyRadius),
204
- (nearby?.relativeSpeedKph ?? 0) / 200,
205
- (nearby?.relativeHeadingRadians ?? 0) / Math.PI,
206
- nearby?.ahead ? 1 : 0,
207
- nearby?.sameLap ? 1 : 0,
208
- );
209
- }
210
- return { vector, schema };
173
+ function buildDriverVector(object, sensors, { includeSchema = true, vectorType = 'array' } = {}) {
174
+ return buildObservationVector(object, sensors, { includeSchema, vectorType });
211
175
  }
212
176
 
213
- function normalizeLapProgress(object) {
214
- return ratio(object.self.lapProgressMeters, object.track.lengthMeters || 1);
215
- }
216
-
217
- 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;
177
+ function buildDriverVectorDirect(car, snapshot, options, events, sensors, getRayBatchContext = null) {
178
+ const profile = options.observation?.profile ?? 'default';
179
+ const includePhysicalDriverSenses = profile === 'physical-driver';
180
+ const onTrack = isCarLegallyOnTrack(car);
181
+ const body = buildBodySenses(car);
182
+ const trackHeadingError = car.trackHeadingError ?? estimateTrackHeadingError(car, snapshot);
183
+ const sensorCar = withEnvironmentControlFlag(car, options);
184
+ const trackRelation = {
185
+ ...buildBoundarySenses(car, snapshot, onTrack),
186
+ headingErrorRadians: trackHeadingError,
187
+ };
188
+ const contactPatches = includePhysicalDriverSenses ? buildContactPatchSenses(car) : [];
189
+ const rays = sensors.rays.enabled ? buildRaySensors(sensorCar, snapshot, sensors.rays, rayBatchContextForSensors(sensors, getRayBatchContext)) : [];
190
+ const nearbyCars = sensors.nearbyCars.enabled
191
+ ? enrichOpponentRadar(car, buildNearbyCars(car, snapshot, sensors.nearbyCars), snapshot)
192
+ : [];
193
+ const trackLengthMeters = simUnitsToMeters(snapshot.track.length ?? 0);
194
+ const trackCurvature = car.trackState?.curvature ?? pointAt(snapshot.track, car.progress ?? 0).curvature ?? 0;
195
+ const source = {
196
+ profile,
197
+ self: {
198
+ ...body,
199
+ lapProgressMeters: simUnitsToMeters(car.progress ?? 0),
200
+ trackOffsetMeters: trackRelation.lateralOffsetMeters,
201
+ trackHeadingErrorRadians: trackHeadingError,
202
+ onTrack,
203
+ inPitLane: Boolean(car.inPitLane),
204
+ tireEnergy: car.tireEnergy ?? null,
205
+ pitIntent: car.pitIntent ?? car.pitStop?.intent ?? 0,
206
+ pitStopStatus: car.pitStop?.status ?? null,
207
+ },
208
+ trackRelation,
209
+ contactPatches,
210
+ race: {
211
+ position: car.rank,
212
+ totalCars: snapshot.cars.length,
213
+ raceMode: snapshot.raceControl.mode,
214
+ pitLaneOpen: Boolean(snapshot.raceControl.pitLaneOpen),
215
+ redFlag: Boolean(snapshot.raceControl.redFlag),
216
+ },
217
+ track: {
218
+ lengthMeters: trackLengthMeters,
219
+ curvature: trackCurvature,
220
+ lookahead: buildTrackLookahead(car, snapshot, options),
221
+ },
222
+ rays,
223
+ nearbyCars,
224
+ };
225
+ return buildObservationVector(source, sensors, {
226
+ includeSchema: false,
227
+ vectorType: options.observation?.vectorType,
228
+ });
220
229
  }
221
230
 
222
- function ratio(value, max) {
223
- const finite = Number.isFinite(value) ? value : max;
224
- return Math.max(0, Math.min(1, finite / Math.max(1e-9, max)));
231
+ function rayBatchContextForSensors(sensors, getRayBatchContext) {
232
+ if (!getRayBatchContext || !sensors.rays?.channels?.includes('car')) return null;
233
+ return getRayBatchContext();
225
234
  }
226
235
 
227
- function clampRatio(value) {
228
- return Math.max(-1, Math.min(1, Number.isFinite(value) ? value : 0));
236
+ export function defaultSensorOptions(options) {
237
+ return {
238
+ rays: normalizeRayOptions(options.sensors.rays),
239
+ nearbyCars: options.sensors.nearbyCars,
240
+ };
229
241
  }
230
242
 
231
- function effectiveSensorOptions(options, driverId) {
243
+ export function effectiveSensorOptions(options, driverId, defaultSensors = defaultSensorOptions(options), hasSensorOverrides = true) {
244
+ if (!hasSensorOverrides || !options.sensorsByDriver?.[driverId]) return defaultSensors;
245
+ const rayOverrides = options.sensorsByDriver?.[driverId]?.rays ?? {};
232
246
  return {
233
- rays: {
234
- ...options.sensors.rays,
235
- ...(options.sensorsByDriver?.[driverId]?.rays ?? {}),
236
- },
247
+ rays: normalizeRayOptions(mergeDriverRayOptions(options.sensors.rays, rayOverrides)),
237
248
  nearbyCars: {
238
249
  ...options.sensors.nearbyCars,
239
250
  ...(options.sensorsByDriver?.[driverId]?.nearbyCars ?? {}),
@@ -241,6 +252,25 @@ function effectiveSensorOptions(options, driverId) {
241
252
  };
242
253
  }
243
254
 
255
+ function mergeDriverRayOptions(baseRays, rayOverrides = {}) {
256
+ const merged = {
257
+ ...baseRays,
258
+ ...rayOverrides,
259
+ };
260
+ if (Object.hasOwn(rayOverrides, 'rays')) return merged;
261
+ if (Object.hasOwn(rayOverrides, 'layout') || Object.hasOwn(rayOverrides, 'anglesDegrees')) {
262
+ delete merged.rays;
263
+ return merged;
264
+ }
265
+ if (Object.hasOwn(rayOverrides, 'lengthMeters') || Object.hasOwn(rayOverrides, 'defaultLengthMeters')) {
266
+ merged.rays = (baseRays.rays ?? []).map((ray) => ({
267
+ id: ray.id,
268
+ angleDegrees: ray.angleDegrees,
269
+ }));
270
+ }
271
+ return merged;
272
+ }
273
+
244
274
  function emptyObservation(driverId) {
245
275
  return {
246
276
  object: {
@@ -249,9 +279,18 @@ function emptyObservation(driverId) {
249
279
  speedKph: 0,
250
280
  speedMetersPerSecond: 0,
251
281
  headingRadians: 0,
282
+ yawRateRadiansPerSecond: 0,
252
283
  steeringAngleRadians: 0,
253
284
  throttle: 0,
254
285
  brake: 0,
286
+ lateralG: 0,
287
+ longitudinalG: 0,
288
+ gripUsage: 0,
289
+ slipAngleRadians: 0,
290
+ tractionLimited: false,
291
+ stabilityState: 'stable',
292
+ destroyed: false,
293
+ destroyReason: null,
255
294
  lap: 0,
256
295
  completedLaps: 0,
257
296
  lapProgressMeters: 0,
@@ -271,6 +310,16 @@ function emptyObservation(driverId) {
271
310
  pitStopPenaltyServiceRemainingSeconds: null,
272
311
  pitStopsCompleted: 0,
273
312
  },
313
+ trackRelation: {
314
+ lateralOffsetMeters: 0,
315
+ headingErrorRadians: 0,
316
+ legalWidthMeters: 0,
317
+ leftBoundaryMeters: 0,
318
+ rightBoundaryMeters: 0,
319
+ onLegalSurface: false,
320
+ surface: 'missing',
321
+ },
322
+ contactPatches: [],
274
323
  race: { position: 0, totalCars: 0, raceMode: 'missing', pitLaneOpen: false, redFlag: false, totalLaps: 0 },
275
324
  track: { lengthMeters: 0, widthMeters: 0, curvature: 0, lookahead: [] },
276
325
  rays: [],
@@ -282,3 +331,12 @@ function emptyObservation(driverId) {
282
331
  events: [],
283
332
  };
284
333
  }
334
+
335
+ function withEnvironmentControlFlag(car, options) {
336
+ if (!car || car.environmentControlled) return car;
337
+ if (!Array.isArray(options?.controlledDrivers) || !options.controlledDrivers.includes(car.id)) return car;
338
+ return {
339
+ ...car,
340
+ environmentControlled: true,
341
+ };
342
+ }
@@ -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
+ }
@@ -10,19 +10,26 @@ export function createRolloutRecorder() {
10
10
  transitions.length = 0;
11
11
  },
12
12
  toJSON() {
13
- return transitions.map((entry) => ({ ...entry }));
13
+ return transitions.map((entry) => cloneTrainingValue(entry));
14
14
  },
15
15
  };
16
16
  }
17
17
 
18
18
  export function createRolloutTransition(previousResult, action, nextResult) {
19
19
  return {
20
- observation: previousResult.observation,
21
- action,
22
- reward: nextResult.reward,
23
- nextObservation: nextResult.observation,
20
+ observation: cloneTrainingValue(previousResult.observation),
21
+ action: cloneTrainingValue(action),
22
+ reward: cloneTrainingValue(nextResult.reward),
23
+ nextObservation: cloneTrainingValue(nextResult.observation),
24
24
  terminated: nextResult.terminated,
25
25
  truncated: nextResult.truncated,
26
- info: nextResult.info,
26
+ info: cloneTrainingValue(nextResult.info),
27
27
  };
28
28
  }
29
+
30
+ function cloneTrainingValue(value) {
31
+ if (value == null || typeof value !== 'object') return value;
32
+ if (ArrayBuffer.isView(value)) return Array.from(value);
33
+ if (Array.isArray(value)) return value.map(cloneTrainingValue);
34
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, cloneTrainingValue(entry)]));
35
+ }
@@ -19,8 +19,10 @@ export function createProgressReward(options = {}) {
19
19
  const currentCar = findSnapshotCar(context.state?.snapshot, driverId);
20
20
  const progressMeters = getProgressMeters(previousCar, currentCar);
21
21
  const self = context.current?.object?.self ?? {};
22
- const speedKph = Number(self.speedKph ?? 0);
23
- const isOnTrack = self.onTrack !== false;
22
+ const speedKph = Number(self.speedKph ?? currentCar?.speedKph ?? 0);
23
+ const isOnTrack = context.metrics?.offTrack != null
24
+ ? !context.metrics.offTrack
25
+ : self.onTrack ?? isSnapshotCarOnTrack(currentCar);
24
26
  const collisionCount = countDriverCollisions(context.events, driverId);
25
27
  const steering = Math.abs(Number(context.action?.steering ?? 0));
26
28
  const brake = Math.abs(Number(context.action?.brake ?? 0));
@@ -49,6 +51,15 @@ function getProgressMeters(previousCar, currentCar) {
49
51
  return current - previous;
50
52
  }
51
53
 
54
+ function isSnapshotCarOnTrack(car) {
55
+ if (!car) return true;
56
+ if (car.inPitLane) return true;
57
+ if (Array.isArray(car.wheels) && car.wheels.length > 0) {
58
+ return car.wheels.every((wheel) => wheel.onTrack || wheel.inPitLane);
59
+ }
60
+ return ['track', 'kerb', 'pit-entry', 'pit-lane', 'pit-exit', 'pit-box'].includes(car.surface ?? 'track');
61
+ }
62
+
52
63
  function countDriverCollisions(events = [], driverId) {
53
64
  if (!driverId || !Array.isArray(events)) return 0;
54
65
  return events.filter((event) => isCollisionEvent(event) && eventIncludesDriver(event, driverId)).length;