@inventure71/paddockjs 0.3.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 (202) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/INSTALL_AND_UPDATE.md +5 -2
  3. package/README.md +328 -13
  4. package/docs/architecture.md +150 -41
  5. package/docs/concepts.md +22 -3
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +474 -34
  8. package/docs/data_model.md +1 -1
  9. package/docs/index.md +1 -0
  10. package/docs/learnings.md +23 -1
  11. package/docs/rules.md +169 -21
  12. package/docs/system_specs.md +56 -20
  13. package/docs/training.md +369 -25
  14. package/examples/python/README.md +62 -0
  15. package/examples/python/base_policy_server.py +191 -0
  16. package/examples/python/requirements.txt +2 -0
  17. package/examples/train-basic-policy.mjs +5 -6
  18. package/examples/trainingData.mjs +133 -0
  19. package/package.json +15 -4
  20. package/src/api/PaddockSimulatorController.js +36 -2
  21. package/src/app/BrowserExpertAdapter.js +367 -9
  22. package/src/app/F1SimulatorApp.js +615 -940
  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 +14 -1
  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 +24 -4
  48. package/src/environment/actions.js +38 -7
  49. package/src/environment/controllerLoop.js +246 -0
  50. package/src/environment/episode.js +86 -2
  51. package/src/environment/evaluation.js +206 -0
  52. package/src/environment/index.d.ts +638 -6
  53. package/src/environment/index.js +14 -0
  54. package/src/environment/metrics.js +159 -0
  55. package/src/environment/observationOptions.js +33 -0
  56. package/src/environment/observations.js +500 -43
  57. package/src/environment/options.js +41 -2
  58. package/src/environment/recorder.js +28 -0
  59. package/src/environment/runtime.js +260 -24
  60. package/src/environment/scenarios.js +249 -0
  61. package/src/environment/sensors/bodySenses.js +23 -0
  62. package/src/environment/sensors/boundarySenses.js +17 -0
  63. package/src/environment/sensors/carRays.js +75 -0
  64. package/src/environment/sensors/contactSenses.js +32 -0
  65. package/src/environment/sensors/index.js +157 -0
  66. package/src/environment/sensors/indexedRayBands.js +103 -0
  67. package/src/environment/sensors/nearbyCars.js +52 -0
  68. package/src/environment/sensors/opponentRadar.js +65 -0
  69. package/src/environment/sensors/rayConfig.js +110 -0
  70. package/src/environment/sensors/rayDefaults.js +6 -0
  71. package/src/environment/sensors/rayGeometry.js +60 -0
  72. package/src/environment/sensors/rayGuards.js +19 -0
  73. package/src/environment/sensors/sensorTargets.js +60 -0
  74. package/src/environment/sensors/surfaceRays.js +296 -0
  75. package/src/environment/sensors/trackRays.js +261 -0
  76. package/src/environment/sensors.js +1 -257
  77. package/src/environment/specs.js +188 -9
  78. package/src/environment/workerProtocol.js +64 -0
  79. package/src/index.d.ts +533 -7
  80. package/src/index.js +26 -0
  81. package/src/rendering/proceduralTrackAsset.js +20 -510
  82. package/src/rendering/renderSnapshot.js +38 -12
  83. package/src/rendering/track/finishLineRenderer.js +34 -0
  84. package/src/rendering/track/gridRenderer.js +47 -0
  85. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  86. package/src/rendering/track/pitLaneRenderer.js +248 -0
  87. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  88. package/src/rendering/track/trackRenderConstants.js +38 -0
  89. package/src/rendering/track/trackRenderGeometry.js +94 -0
  90. package/src/simulation/collisionGeometry.js +214 -0
  91. package/src/simulation/driver/driverControlConstants.js +28 -0
  92. package/src/simulation/driver/driverController.js +47 -0
  93. package/src/simulation/driver/driverInput.js +28 -0
  94. package/src/simulation/driver/driverMath.js +6 -0
  95. package/src/simulation/driver/driverPersonality.js +25 -0
  96. package/src/simulation/driver/edgeRecovery.js +48 -0
  97. package/src/simulation/driver/racingControls.js +399 -0
  98. package/src/simulation/driver/racingLinePlan.js +166 -0
  99. package/src/simulation/driver/recoveryDynamics.js +38 -0
  100. package/src/simulation/driver/rejoinControls.js +213 -0
  101. package/src/simulation/driver/safetyCarControls.js +35 -0
  102. package/src/simulation/driver/trafficScan.js +51 -0
  103. package/src/simulation/driverController.js +4 -253
  104. package/src/simulation/participants/participantInteractions.js +120 -0
  105. package/src/simulation/pit/pitFlow.js +234 -0
  106. package/src/simulation/pit/pitIntent.js +92 -0
  107. package/src/simulation/pit/pitOccupancy.js +47 -0
  108. package/src/simulation/pit/pitPenaltyService.js +71 -0
  109. package/src/simulation/pit/pitQueue.js +55 -0
  110. package/src/simulation/pit/pitRouteMovement.js +160 -0
  111. package/src/simulation/pit/pitRouting.js +319 -0
  112. package/src/simulation/pit/pitService.js +41 -0
  113. package/src/simulation/pit/pitServiceConstants.js +21 -0
  114. package/src/simulation/pit/pitServiceProfile.js +45 -0
  115. package/src/simulation/pit/pitSnapshots.js +61 -0
  116. package/src/simulation/pit/pitState.js +113 -0
  117. package/src/simulation/pit/pitTireService.js +92 -0
  118. package/src/simulation/race/classification.js +104 -0
  119. package/src/simulation/race/gridPenalties.js +37 -0
  120. package/src/simulation/race/raceControlState.js +38 -0
  121. package/src/simulation/race/raceDistance.js +32 -0
  122. package/src/simulation/race/raceFinish.js +65 -0
  123. package/src/simulation/race/raceLifecycle.js +71 -0
  124. package/src/simulation/race/raceOrder.js +113 -0
  125. package/src/simulation/race/raceProgress.js +124 -0
  126. package/src/simulation/race/raceSetup.js +93 -0
  127. package/src/simulation/race/raceStep.js +80 -0
  128. package/src/simulation/race/redFlag.js +15 -0
  129. package/src/simulation/race/retirements.js +44 -0
  130. package/src/simulation/race/safetyCar.js +86 -0
  131. package/src/simulation/race/stalledDnf.js +86 -0
  132. package/src/simulation/raceSimulation.js +386 -1012
  133. package/src/simulation/replay/replayGhosts.js +128 -0
  134. package/src/simulation/rules/collisionSteward.js +69 -0
  135. package/src/simulation/rules/moduleConfig.js +72 -0
  136. package/src/simulation/rules/penaltyConfig.js +99 -0
  137. package/src/simulation/rules/penaltyEvents.js +18 -0
  138. package/src/simulation/rules/penaltyLedger.js +153 -0
  139. package/src/simulation/rules/penaltyStats.js +26 -0
  140. package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
  141. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  142. package/src/simulation/rules/ruleDefaults.js +85 -0
  143. package/src/simulation/rules/rulesReview.js +141 -0
  144. package/src/simulation/rules/rulesetPresets.js +47 -0
  145. package/src/simulation/rules/tireRequirementSteward.js +29 -0
  146. package/src/simulation/rules/trackLimitsSteward.js +85 -0
  147. package/src/simulation/rulesConfig.js +34 -0
  148. package/src/simulation/snapshots/raceSnapshots.js +192 -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/trackQueryIndex.js +945 -0
  171. package/src/simulation/track/trackSectors.js +19 -0
  172. package/src/simulation/track/trackStatePolicy.js +24 -0
  173. package/src/simulation/track/trackValidation.js +98 -0
  174. package/src/simulation/trackModel.js +1 -628
  175. package/src/simulation/units.js +6 -4
  176. package/src/simulation/vehicle/contactResolution.js +116 -0
  177. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  178. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  179. package/src/simulation/vehicle/runoffResponse.js +88 -0
  180. package/src/simulation/vehicle/surfacePriority.js +28 -0
  181. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  182. package/src/simulation/vehicle/vehiclePhysics.js +363 -0
  183. package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
  184. package/src/simulation/vehicle/vehicleState.js +186 -0
  185. package/src/simulation/vehicle/wheelSurface.js +150 -0
  186. package/src/simulation/vehicleGeometry.js +1 -0
  187. package/src/simulation/vehiclePhysics.js +1 -104
  188. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  189. package/src/simulation/wheelSurface.js +1 -0
  190. package/src/styles.css +342 -42
  191. package/src/ui/bannerTemplates.js +69 -0
  192. package/src/ui/cameraControlsTemplate.js +20 -0
  193. package/src/ui/carOverviewTemplate.js +33 -0
  194. package/src/ui/componentTemplates.js +20 -426
  195. package/src/ui/raceCanvasTemplate.js +69 -0
  196. package/src/ui/raceControlStatusBanner.js +39 -0
  197. package/src/ui/raceControlsTemplate.js +36 -0
  198. package/src/ui/shellTemplate.js +6 -5
  199. package/src/ui/telemetryDrawerTemplate.js +55 -0
  200. package/src/ui/telemetryTemplates.js +136 -0
  201. package/src/ui/templateUtils.js +23 -0
  202. package/src/ui/timingTowerTemplate.js +34 -0
@@ -1,78 +1,496 @@
1
- import { simSpeedToMetersPerSecond, simUnitsToMeters } from '../simulation/units.js';
2
- import { buildNearbyCars, buildRaySensors } from './sensors.js';
3
-
4
- export function buildEnvironmentObservation({ snapshot, options, events = [] }) {
5
- return Object.fromEntries(options.controlledDrivers.map((driverId) => {
6
- const car = snapshot.cars.find((entry) => entry.id === driverId);
7
- if (!car) return [driverId, emptyObservation(driverId)];
8
-
9
- const driverEvents = events.filter((event) =>
10
- event.driverId === driverId ||
11
- event.carId === driverId ||
12
- event.otherCarId === driverId ||
13
- event.driverIds?.includes?.(driverId));
14
- const object = buildDriverObservationObject(car, snapshot, options, driverEvents);
15
- const { vector, schema } = buildDriverVector(object);
16
- return [driverId, { object, vector, schema, events: driverEvents }];
17
- }));
18
- }
19
-
20
- function buildDriverObservationObject(car, snapshot, options, events) {
21
- const sensors = effectiveSensorOptions(options, car.id);
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, buildRaySensors, createRayBatchContext, normalizeRayOptions } from './sensors.js';
10
+
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 }) {
15
+ const carsById = new Map(snapshot.cars.map((entry) => [entry.id, entry]));
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) => {
26
+ const car = carsById.get(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
+ }
58
+
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;
67
+ }
68
+
69
+ function groupEventsByDriver(events, controlledDrivers) {
70
+ if (!events.length) return null;
71
+ const byDriver = new Map(controlledDrivers.map((driverId) => [driverId, []]));
72
+ const controlledSet = new Set(controlledDrivers);
73
+ events.forEach((event) => {
74
+ const driverIds = new Set([
75
+ event.driverId,
76
+ event.carId,
77
+ event.otherCarId,
78
+ ...(event.driverIds ?? []),
79
+ ].filter(Boolean));
80
+ driverIds.forEach((driverId) => {
81
+ if (controlledSet.has(driverId)) byDriver.get(driverId)?.push(event);
82
+ });
83
+ });
84
+ return byDriver;
85
+ }
86
+
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
+ : [];
22
99
  return {
100
+ profile: options.observation?.profile ?? 'default',
23
101
  self: {
24
- id: car.id,
25
- speedKph: car.speedKph,
26
- speedMetersPerSecond: simSpeedToMetersPerSecond(car.speed ?? 0),
27
- headingRadians: car.heading,
28
- steeringAngleRadians: car.steeringAngle ?? 0,
29
- throttle: car.throttle ?? 0,
30
- brake: car.brake ?? 0,
102
+ ...body,
31
103
  lap: car.lap,
32
104
  completedLaps: car.lapTelemetry?.completedLaps ?? 0,
33
105
  lapProgressMeters: simUnitsToMeters(car.progress ?? 0),
34
- trackOffsetMeters: simUnitsToMeters(car.signedOffset ?? 0),
35
- trackHeadingErrorRadians: car.trackHeadingError ?? 0,
36
- onTrack: (car.surface ?? 'track') === 'track',
106
+ trackOffsetMeters: trackRelation.lateralOffsetMeters,
107
+ trackHeadingErrorRadians: trackHeadingError,
108
+ onTrack,
37
109
  surface: car.surface ?? 'track',
110
+ inPitLane: Boolean(car.inPitLane),
111
+ pitLanePart: car.pitLanePart ?? null,
112
+ pitBoxId: car.pitBoxId ?? null,
38
113
  tireEnergy: car.tireEnergy ?? null,
114
+ pitIntent: car.pitIntent ?? car.pitStop?.intent ?? 0,
115
+ pitTargetCompound: car.pitStop?.targetTire ?? null,
116
+ pitStopStatus: car.pitStop?.status ?? null,
117
+ pitStopPhase: car.pitStop?.phase ?? null,
118
+ pitStopServiceRemainingSeconds: car.pitStop?.serviceRemainingSeconds ?? null,
119
+ pitStopPenaltyServiceRemainingSeconds: car.pitStop?.penaltyServiceRemainingSeconds ?? null,
120
+ pitStopsCompleted: car.pitStop?.stopsCompleted ?? 0,
39
121
  },
122
+ trackRelation,
123
+ contactPatches: buildContactPatchSenses(car),
40
124
  race: {
41
125
  position: car.rank,
42
126
  totalCars: snapshot.cars.length,
43
127
  raceMode: snapshot.raceControl.mode,
128
+ pitLaneOpen: Boolean(snapshot.raceControl.pitLaneOpen),
129
+ redFlag: Boolean(snapshot.raceControl.redFlag),
44
130
  totalLaps: snapshot.totalLaps,
45
131
  },
46
- rays: sensors.rays.enabled ? buildRaySensors(car, snapshot, sensors.rays) : [],
47
- nearbyCars: sensors.nearbyCars.enabled ? buildNearbyCars(car, snapshot, sensors.nearbyCars) : [],
132
+ track: {
133
+ lengthMeters: simUnitsToMeters(snapshot.track.length ?? 0),
134
+ widthMeters: simUnitsToMeters(snapshot.track.width ?? 0),
135
+ curvature: car.trackState?.curvature ?? pointAt(snapshot.track, car.progress ?? 0).curvature ?? 0,
136
+ lookahead: buildTrackLookahead(car, snapshot, options),
137
+ },
138
+ rays: sensors.rays.enabled ? buildRaySensors(sensorCar, snapshot, sensors.rays, rayBatchContextForSensors(sensors, getRayBatchContext)) : [],
139
+ nearbyCars,
48
140
  events,
49
141
  };
50
142
  }
51
143
 
52
- function buildDriverVector(object) {
53
- const schema = [
144
+ function estimateTrackHeadingError(car, snapshot) {
145
+ const base = pointAt(snapshot.track, car.progress ?? 0);
146
+ return normalizeAngle((car.heading ?? base.heading) - base.heading);
147
+ }
148
+
149
+ function buildTrackLookahead(car, snapshot, options) {
150
+ const distances = Array.isArray(options.observation?.lookaheadMeters)
151
+ ? options.observation.lookaheadMeters
152
+ : normalizeLookaheadMeters(options.observation?.lookaheadMeters);
153
+ if (!distances.length) return [];
154
+ const base = pointAt(snapshot.track, car.progress ?? 0);
155
+ return distances.map((distanceMeters) => {
156
+ const sample = pointAt(snapshot.track, (car.progress ?? 0) + metersToSimUnits(distanceMeters));
157
+ return {
158
+ distanceMeters,
159
+ curvature: sample.curvature ?? 0,
160
+ headingDeltaRadians: normalizeAngle(sample.heading - base.heading),
161
+ };
162
+ });
163
+ }
164
+
165
+ function isCarLegallyOnTrack(car) {
166
+ if (Array.isArray(car.wheels) && car.wheels.length > 0) {
167
+ return car.wheels.every((wheel) => wheel.onTrack || wheel.inPitLane);
168
+ }
169
+ if (car.inPitLane) return true;
170
+ return ['track', 'kerb', 'pit-entry', 'pit-lane', 'pit-exit', 'pit-box'].includes(car.surface ?? 'track');
171
+ }
172
+
173
+ function buildDriverVector(object, sensors, { includeSchema = true, vectorType = 'array' } = {}) {
174
+ const includePhysicalDriverSenses = object.profile === 'physical-driver';
175
+ const schema = includeSchema ? [
54
176
  { name: 'self.speedKph', unit: 'kph', scale: 'fixed:400' },
177
+ { name: 'self.speedMetersPerSecond', unit: 'm/s', scale: 'fixed:120' },
178
+ { name: 'self.steeringAngleRadians', unit: 'rad', scale: 'fixed:pi' },
179
+ { name: 'self.throttle', scale: '0..1' },
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' },
186
+ { name: 'self.lapProgressRatio', scale: '0..1' },
55
187
  { name: 'self.trackOffsetMeters', unit: 'm', scale: 'fixed:meters' },
56
188
  { name: 'self.trackHeadingErrorRadians', unit: 'rad', scale: 'fixed:pi' },
57
189
  { name: 'self.onTrack', scale: 'boolean' },
58
- { name: 'race.position', scale: 'fixed:field-position' },
59
- ];
190
+ { name: 'self.inPitLane', scale: 'boolean' },
191
+ { name: 'self.tireEnergy', scale: '0..100' },
192
+ { name: 'self.pitIntent', scale: '0..2' },
193
+ { name: 'self.pitStopActive', scale: 'boolean' },
194
+ { name: 'race.positionNormalized', scale: '0..1' },
195
+ { name: 'race.raceModeGreen', scale: 'boolean' },
196
+ { name: 'race.raceModeSafetyCar', scale: 'boolean' },
197
+ { name: 'race.redFlag', scale: 'boolean' },
198
+ { name: 'race.pitLaneOpen', scale: 'boolean' },
199
+ { name: 'track.curvature', scale: 'track-curvature' },
200
+ ] : null;
60
201
  const vector = [
61
202
  object.self.speedKph / 400,
203
+ object.self.speedMetersPerSecond / 120,
204
+ object.self.steeringAngleRadians / Math.PI,
205
+ object.self.throttle,
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,
212
+ normalizeLapProgress(object),
62
213
  object.self.trackOffsetMeters,
63
214
  object.self.trackHeadingErrorRadians / Math.PI,
64
215
  object.self.onTrack ? 1 : 0,
65
- object.race.position,
216
+ object.self.inPitLane ? 1 : 0,
217
+ (object.self.tireEnergy ?? 0) / 100,
218
+ (object.self.pitIntent ?? 0) / 2,
219
+ object.self.pitStopStatus && object.self.pitStopStatus !== 'pending' && object.self.pitStopStatus !== 'completed' ? 1 : 0,
220
+ normalizeRacePosition(object),
221
+ object.race.raceMode === 'green' ? 1 : 0,
222
+ object.race.raceMode === 'safety-car' ? 1 : 0,
223
+ object.race.redFlag ? 1 : 0,
224
+ object.race.pitLaneOpen ? 1 : 0,
225
+ object.track.curvature ?? 0,
66
226
  ];
67
- return { vector, schema };
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
+ }
255
+ object.track.lookahead.forEach((sample, index) => {
256
+ pushSchema(schema,
257
+ { name: `track.lookahead[${index}].curvature`, scale: 'track-curvature' },
258
+ { name: `track.lookahead[${index}].headingDeltaRadians`, unit: 'rad', scale: 'fixed:pi' },
259
+ );
260
+ vector.push(sample.curvature ?? 0, (sample.headingDeltaRadians ?? 0) / Math.PI);
261
+ });
262
+ object.rays.forEach((ray, index) => {
263
+ pushSchema(schema,
264
+ { name: `rays[${index}].track.distanceRatio`, scale: '0..1' },
265
+ { name: `rays[${index}].track.hit`, scale: 'boolean' },
266
+ { name: `rays[${index}].track.kindExit`, scale: 'boolean' },
267
+ { name: `rays[${index}].track.kindEntry`, scale: 'boolean' },
268
+ { name: `rays[${index}].car.distanceRatio`, scale: '0..1' },
269
+ { name: `rays[${index}].car.hit`, scale: 'boolean' },
270
+ { name: `rays[${index}].car.relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
271
+ );
272
+ vector.push(
273
+ ratio(ray.track.distanceMeters, ray.lengthMeters),
274
+ ray.track.hit ? 1 : 0,
275
+ ray.track.kind === 'exit' ? 1 : 0,
276
+ ray.track.kind === 'entry' ? 1 : 0,
277
+ ratio(ray.car.distanceMeters, ray.lengthMeters),
278
+ ray.car.hit ? 1 : 0,
279
+ ray.car.relativeSpeedKph / 200,
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
+ }
293
+ });
294
+ const nearbyLimit = sensors.nearbyCars.enabled ? (sensors.nearbyCars.maxCars ?? object.nearbyCars.length) : 0;
295
+ const nearbyRadius = sensors.nearbyCars.radiusMeters ?? 150;
296
+ for (let index = 0; index < nearbyLimit; index += 1) {
297
+ const nearby = object.nearbyCars[index] ?? null;
298
+ pushSchema(schema,
299
+ { name: `nearbyCars[${index}].present`, scale: 'boolean' },
300
+ { name: `nearbyCars[${index}].relativeForwardRatio`, scale: '-1..1' },
301
+ { name: `nearbyCars[${index}].relativeRightRatio`, scale: '-1..1' },
302
+ { name: `nearbyCars[${index}].relativeDistanceRatio`, scale: '0..1' },
303
+ { name: `nearbyCars[${index}].relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
304
+ { name: `nearbyCars[${index}].relativeHeadingRadians`, unit: 'rad', scale: 'fixed:pi' },
305
+ { name: `nearbyCars[${index}].ahead`, scale: 'boolean' },
306
+ { name: `nearbyCars[${index}].sameLap`, scale: 'boolean' },
307
+ );
308
+ vector.push(
309
+ nearby ? 1 : 0,
310
+ clampRatio((nearby?.relativeForwardMeters ?? 0) / nearbyRadius),
311
+ clampRatio((nearby?.relativeRightMeters ?? 0) / nearbyRadius),
312
+ ratio(nearby?.relativeDistanceMeters ?? nearbyRadius, nearbyRadius),
313
+ (nearby?.relativeSpeedKph ?? 0) / 200,
314
+ (nearby?.relativeHeadingRadians ?? 0) / Math.PI,
315
+ nearby?.ahead ? 1 : 0,
316
+ nearby?.sameLap ? 1 : 0,
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
+ }
334
+ }
335
+ return { vector: finalizeVector(vector, vectorType), schema: schema ?? [] };
68
336
  }
69
337
 
70
- function effectiveSensorOptions(options, driverId) {
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);
458
+ }
459
+
460
+ function normalizeLapProgress(object) {
461
+ return ratio(object.self.lapProgressMeters, object.track.lengthMeters || 1);
462
+ }
463
+
464
+ function normalizeRacePosition(object) {
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);
469
+ }
470
+
471
+ function ratio(value, max) {
472
+ const finite = Number.isFinite(value) ? value : max;
473
+ return Math.max(0, Math.min(1, finite / Math.max(1e-9, max)));
474
+ }
475
+
476
+ function clampRatio(value) {
477
+ return Math.max(-1, Math.min(1, Number.isFinite(value) ? value : 0));
478
+ }
479
+
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;
71
489
  return {
72
- rays: {
490
+ rays: normalizeRayOptions({
73
491
  ...options.sensors.rays,
74
492
  ...(options.sensorsByDriver?.[driverId]?.rays ?? {}),
75
- },
493
+ }),
76
494
  nearbyCars: {
77
495
  ...options.sensors.nearbyCars,
78
496
  ...(options.sensorsByDriver?.[driverId]?.nearbyCars ?? {}),
@@ -88,9 +506,18 @@ function emptyObservation(driverId) {
88
506
  speedKph: 0,
89
507
  speedMetersPerSecond: 0,
90
508
  headingRadians: 0,
509
+ yawRateRadiansPerSecond: 0,
91
510
  steeringAngleRadians: 0,
92
511
  throttle: 0,
93
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,
94
521
  lap: 0,
95
522
  completedLaps: 0,
96
523
  lapProgressMeters: 0,
@@ -98,9 +525,30 @@ function emptyObservation(driverId) {
98
525
  trackHeadingErrorRadians: 0,
99
526
  onTrack: false,
100
527
  surface: 'missing',
528
+ inPitLane: false,
529
+ pitLanePart: null,
530
+ pitBoxId: null,
101
531
  tireEnergy: null,
532
+ pitIntent: 0,
533
+ pitTargetCompound: null,
534
+ pitStopStatus: null,
535
+ pitStopPhase: null,
536
+ pitStopServiceRemainingSeconds: null,
537
+ pitStopPenaltyServiceRemainingSeconds: null,
538
+ pitStopsCompleted: 0,
102
539
  },
103
- race: { position: 0, totalCars: 0, raceMode: 'missing', totalLaps: 0 },
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: [],
550
+ race: { position: 0, totalCars: 0, raceMode: 'missing', pitLaneOpen: false, redFlag: false, totalLaps: 0 },
551
+ track: { lengthMeters: 0, widthMeters: 0, curvature: 0, lookahead: [] },
104
552
  rays: [],
105
553
  nearbyCars: [],
106
554
  events: [],
@@ -110,3 +558,12 @@ function emptyObservation(driverId) {
110
558
  events: [],
111
559
  };
112
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
+ }