@inventure71/paddockjs 2.0.0 → 3.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 (110) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/INSTALL_AND_UPDATE.md +10 -1
  3. package/README.md +92 -17
  4. package/docs/architecture.md +26 -16
  5. package/docs/component_inventory.md +72 -0
  6. package/docs/concepts.md +7 -5
  7. package/docs/custom_model_controller.md +2 -2
  8. package/docs/data_contract.md +121 -33
  9. package/docs/data_model.md +1 -1
  10. package/docs/index.md +10 -0
  11. package/docs/learnings.md +6 -6
  12. package/docs/rules.md +39 -9
  13. package/docs/sense_contract.md +59 -0
  14. package/docs/system_specs.md +76 -20
  15. package/docs/training.md +27 -20
  16. package/package.json +1 -1
  17. package/src/api/PaddockSimulatorController.js +75 -36
  18. package/src/app/BrowserExpertAdapter.js +9 -300
  19. package/src/app/BrowserExpertExternalRenderer.js +303 -0
  20. package/src/app/F1SimulatorApp.js +190 -68
  21. package/src/app/banners/raceDataBannerController.js +71 -36
  22. package/src/app/banners/stewardMessageController.js +5 -4
  23. package/src/app/camera/cameraConstants.js +2 -0
  24. package/src/app/camera/cameraController.js +86 -29
  25. package/src/app/camera/cameraGeometry.js +12 -1
  26. package/src/app/domBindings.js +89 -9
  27. package/src/app/layoutScheduler.js +27 -0
  28. package/src/app/layoutSupport.js +165 -0
  29. package/src/app/raceOverlayClearanceSupport.js +158 -0
  30. package/src/app/readouts/carOverviewRenderer.js +16 -8
  31. package/src/app/readouts/controlStateRenderer.js +1 -1
  32. package/src/app/readouts/raceStatusRenderer.js +30 -21
  33. package/src/app/readouts/readoutFormatters.js +2 -0
  34. package/src/app/readouts/telemetryRenderer.js +17 -8
  35. package/src/app/readouts/timingTowerRenderer.js +62 -10
  36. package/src/app/runtime/runtimeVisibility.js +66 -5
  37. package/src/config/cssValues.js +16 -0
  38. package/src/config/defaultOptions.js +51 -38
  39. package/src/config/restartOptions.js +34 -0
  40. package/src/config/themeOptions.js +825 -0
  41. package/src/config/timingGapMode.js +13 -0
  42. package/src/config/urlOptions.js +25 -0
  43. package/src/data/championship.js +14 -9
  44. package/src/environment/controllerLoop.js +34 -5
  45. package/src/environment/episode.js +14 -2
  46. package/src/environment/index.d.ts +17 -4
  47. package/src/environment/observationVector.js +200 -0
  48. package/src/environment/observations.js +58 -285
  49. package/src/environment/options.js +1 -1
  50. package/src/environment/recorder.js +13 -6
  51. package/src/environment/rewards.js +13 -2
  52. package/src/environment/runtime.js +65 -25
  53. package/src/environment/scenarios.js +22 -3
  54. package/src/environment/sensors/index.js +3 -3
  55. package/src/environment/sensors/opponentRadar.js +13 -3
  56. package/src/environment/sensors/sensorTargets.js +8 -2
  57. package/src/environment/sensors/surfaceRays.js +84 -12
  58. package/src/environment/sensors/trackRays.js +173 -15
  59. package/src/environment/specs.js +78 -40
  60. package/src/index.d.ts +112 -9
  61. package/src/index.js +26 -33
  62. package/src/simulation/driver/driverController.js +1 -1
  63. package/src/simulation/driver/edgeRecovery.js +1 -1
  64. package/src/simulation/driver/racingControls.js +2 -2
  65. package/src/simulation/driver/rejoinControls.js +2 -2
  66. package/src/simulation/pit/pitFlow.js +5 -0
  67. package/src/simulation/pit/pitKinematics.js +24 -0
  68. package/src/simulation/pit/pitQueue.js +5 -0
  69. package/src/simulation/pit/pitRouteMovement.js +7 -0
  70. package/src/simulation/pit/pitRouting.js +2 -4
  71. package/src/simulation/pit/pitService.js +2 -0
  72. package/src/simulation/pit/pitTireService.js +2 -0
  73. package/src/simulation/race/raceLifecycle.js +2 -6
  74. package/src/simulation/race/raceProgress.js +6 -1
  75. package/src/simulation/race/raceSetup.js +2 -6
  76. package/src/simulation/race/raceStep.js +2 -2
  77. package/src/simulation/race/redFlag.js +3 -3
  78. package/src/simulation/race/safetyCar.js +10 -4
  79. package/src/simulation/race/stalledDnf.js +8 -17
  80. package/src/simulation/raceSimulation.js +1 -0
  81. package/src/simulation/rules/rulesReview.js +3 -0
  82. package/src/simulation/snapshots/raceSnapshots.js +1 -0
  83. package/src/simulation/timing/lapTelemetry.js +28 -9
  84. package/src/simulation/track/pitLaneLayout.js +7 -3
  85. package/src/simulation/track/spatialQueries.js +5 -28
  86. package/src/simulation/track/trackQueryGrid.js +94 -0
  87. package/src/simulation/track/trackQueryIndex.js +193 -434
  88. package/src/simulation/track/trackQueryPitIndex.js +140 -0
  89. package/src/simulation/track/trackQueryScratch.js +46 -0
  90. package/src/simulation/track/trackQueryStats.js +28 -0
  91. package/src/simulation/track/trackStatePolicy.js +0 -1
  92. package/src/simulation/vehicle/contactResolution.js +67 -2
  93. package/src/simulation/vehicle/runoffResponse.js +11 -12
  94. package/src/simulation/vehicle/vehicleKinematics.js +37 -0
  95. package/src/simulation/vehicle/vehiclePhysics.js +5 -4
  96. package/src/simulation/vehicle/vehicleSnapshots.js +22 -0
  97. package/src/simulation/vehicle/vehicleState.js +36 -2
  98. package/src/simulation/vehicle/wheelSurface.js +3 -1
  99. package/src/simulation/warmup/runtimeWarmup.js +0 -1
  100. package/src/styles.css +1185 -136
  101. package/src/ui/bannerTemplates.js +38 -28
  102. package/src/ui/cameraControlsTemplate.js +16 -5
  103. package/src/ui/carOverviewTemplate.js +12 -5
  104. package/src/ui/raceCanvasTemplate.js +31 -8
  105. package/src/ui/raceControlsTemplate.js +27 -10
  106. package/src/ui/shellTemplate.js +3 -1
  107. package/src/ui/telemetryDrawerTemplate.js +23 -10
  108. package/src/ui/telemetryTemplates.js +58 -32
  109. package/src/ui/templateUtils.js +48 -0
  110. package/src/ui/timingTowerTemplate.js +29 -12
@@ -7,9 +7,9 @@ import { buildBoundarySenses } from './sensors/boundarySenses.js';
7
7
  import { buildContactPatchSenses } from './sensors/contactSenses.js';
8
8
  import { enrichOpponentRadar } from './sensors/opponentRadar.js';
9
9
  import { buildNearbyCars, buildRaySensors, createRayBatchContext, normalizeRayOptions } from './sensors.js';
10
+ import { buildObservationVector } from './observationVector.js';
10
11
 
11
12
  const EMPTY_EVENTS = Object.freeze([]);
12
- const PHYSICAL_RAY_SURFACE_CHANNELS = Object.freeze(['kerb', 'illegalSurface']);
13
13
 
14
14
  export function buildEnvironmentObservation({ snapshot, options, events = [], controlledDrivers = options.controlledDrivers }) {
15
15
  const carsById = new Map(snapshot.cars.map((entry) => [entry.id, entry]));
@@ -171,168 +171,7 @@ function isCarLegallyOnTrack(car) {
171
171
  }
172
172
 
173
173
  function buildDriverVector(object, sensors, { includeSchema = true, vectorType = 'array' } = {}) {
174
- const includePhysicalDriverSenses = object.profile === 'physical-driver';
175
- const schema = includeSchema ? [
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' },
187
- { name: 'self.trackOffsetMeters', unit: 'm', scale: 'fixed:meters' },
188
- { name: 'self.trackHeadingErrorRadians', unit: 'rad', scale: 'fixed:pi' },
189
- { name: 'self.onTrack', scale: 'boolean' },
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;
201
- const vector = [
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),
213
- object.self.trackOffsetMeters,
214
- object.self.trackHeadingErrorRadians / Math.PI,
215
- object.self.onTrack ? 1 : 0,
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,
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
- }
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 ?? [] };
174
+ return buildObservationVector(object, sensors, { includeSchema, vectorType });
336
175
  }
337
176
 
338
177
  function buildDriverVectorDirect(car, snapshot, options, events, sensors, getRayBatchContext = null) {
@@ -353,95 +192,40 @@ function buildDriverVectorDirect(car, snapshot, options, events, sensors, getRay
353
192
  : [];
354
193
  const trackLengthMeters = simUnitsToMeters(snapshot.track.length ?? 0);
355
194
  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
- }
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,
419
228
  });
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
229
  }
446
230
 
447
231
  function rayBatchContextForSensors(sensors, getRayBatchContext) {
@@ -449,48 +233,18 @@ function rayBatchContextForSensors(sensors, getRayBatchContext) {
449
233
  return getRayBatchContext();
450
234
  }
451
235
 
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) {
236
+ export function defaultSensorOptions(options) {
481
237
  return {
482
238
  rays: normalizeRayOptions(options.sensors.rays),
483
239
  nearbyCars: options.sensors.nearbyCars,
484
240
  };
485
241
  }
486
242
 
487
- function effectiveSensorOptions(options, driverId, defaultSensors = defaultSensorOptions(options), hasSensorOverrides = true) {
243
+ export function effectiveSensorOptions(options, driverId, defaultSensors = defaultSensorOptions(options), hasSensorOverrides = true) {
488
244
  if (!hasSensorOverrides || !options.sensorsByDriver?.[driverId]) return defaultSensors;
245
+ const rayOverrides = options.sensorsByDriver?.[driverId]?.rays ?? {};
489
246
  return {
490
- rays: normalizeRayOptions({
491
- ...options.sensors.rays,
492
- ...(options.sensorsByDriver?.[driverId]?.rays ?? {}),
493
- }),
247
+ rays: normalizeRayOptions(mergeDriverRayOptions(options.sensors.rays, rayOverrides)),
494
248
  nearbyCars: {
495
249
  ...options.sensors.nearbyCars,
496
250
  ...(options.sensorsByDriver?.[driverId]?.nearbyCars ?? {}),
@@ -498,6 +252,25 @@ function effectiveSensorOptions(options, driverId, defaultSensors = defaultSenso
498
252
  };
499
253
  }
500
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
+
501
274
  function emptyObservation(driverId) {
502
275
  return {
503
276
  object: {
@@ -8,7 +8,7 @@ import { normalizeRayOptions } from './sensors.js';
8
8
 
9
9
  const DEFAULT_SEED = 1971;
10
10
  const DEFAULT_FRAME_SKIP = 1;
11
- const DEFAULT_MAX_STEPS = 10000;
11
+ const DEFAULT_MAX_STEPS = 1_000_000;
12
12
  const RESULT_STATE_OUTPUTS = new Set(['full', 'minimal', 'none']);
13
13
  const RESET_OBSERVATION_SCOPES = new Set(['all', 'reset']);
14
14
 
@@ -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;
@@ -38,16 +38,14 @@ export function createPaddockEnvironment(options = {}) {
38
38
  }
39
39
 
40
40
  function createSimulationWithEnvironmentScenario(options) {
41
- const simulationOptions = {
42
- ...options,
43
- trackQueryIndex: shouldUseTrackQueryIndex(options),
44
- };
45
41
  runWarmupWithGuard({
46
- options: simulationOptions,
42
+ options,
47
43
  surface: 'environment',
48
44
  execute: ({ warmup }) => {
49
- const warmupSimulation = createRaceSimulation(disableWarmupOptions(simulationOptions, warmup.surface));
45
+ const warmupSimulation = createRaceSimulation(disableWarmupOptions(options, warmup.surface));
46
+ initializeControlledPitIntentForSimulation(warmupSimulation, options);
50
47
  applyEnvironmentScenario(warmupSimulation, options);
48
+ applyControlledRunoffResponse(warmupSimulation, options);
51
49
  for (let index = 0; index < warmup.steps; index += 1) warmupSimulation.step(FIXED_STEP);
52
50
  const warmupSnapshot = warmupSimulation.snapshotObservation?.() ?? warmupSimulation.snapshot();
53
51
  buildEnvironmentObservation({
@@ -57,15 +55,13 @@ function createSimulationWithEnvironmentScenario(options) {
57
55
  });
58
56
  },
59
57
  });
60
- const sim = createRaceSimulation(disableWarmupOptions(simulationOptions, 'environment'));
58
+ const sim = createRaceSimulation(disableWarmupOptions(options, 'environment'));
59
+ initializeControlledPitIntentForSimulation(sim, options);
61
60
  applyEnvironmentScenario(sim, options);
61
+ applyControlledRunoffResponse(sim, options);
62
62
  return sim;
63
63
  }
64
64
 
65
- function shouldUseTrackQueryIndex(options) {
66
- return options.trackQueryIndex !== false;
67
- }
68
-
69
65
  export function createEnvironmentRuntime(host) {
70
66
  const episodeState = createEpisodeState();
71
67
 
@@ -120,7 +116,14 @@ export function createEnvironmentRuntime(host) {
120
116
  }
121
117
  episodeState.step += 1;
122
118
  advanceDriverEpisodes(episodeState, options.controlledDrivers);
123
- const result = buildResult({ host, episodeState, events: stepEvents, actionErrors: errors, actions });
119
+ const result = buildResult({
120
+ host,
121
+ episodeState,
122
+ events: stepEvents,
123
+ actionErrors: errors,
124
+ actions,
125
+ rewardEnabled: true,
126
+ });
124
127
  episodeState.lastResult = result;
125
128
  emitExternalRenderFrame(host, result, { source: 'step', actions });
126
129
  host.afterStep(result);
@@ -150,8 +153,9 @@ export function createEnvironmentRuntime(host) {
150
153
  Object.keys(normalizedPlacements).forEach((driverId) => {
151
154
  sim.clearCarControls?.(driverId);
152
155
  sim.setAutomaticPitIntentEnabled?.(driverId, false);
153
- sim.setPitIntent?.(driverId, 0);
154
156
  const car = sim.cars?.find?.((item) => item.id === driverId);
157
+ resetEnvironmentPitStop(car);
158
+ sim.setPitIntent?.(driverId, 0);
155
159
  if (car) sim.applyRunoffResponse?.(car);
156
160
  });
157
161
  resetDriverEpisodes(episodeState, Object.keys(normalizedPlacements));
@@ -208,6 +212,7 @@ function mergePlainObjects(base, overrides, path = []) {
208
212
  if (!isPlainObject(base) || !isPlainObject(overrides)) return overrides;
209
213
  const merged = { ...base };
210
214
  Object.entries(overrides).forEach(([key, value]) => {
215
+ if (value === undefined) return;
211
216
  const nextPath = [...path, key];
212
217
  merged[key] = isPlainObject(value) &&
213
218
  isPlainObject(base[key]) &&
@@ -235,6 +240,10 @@ function isNoopPitIntent(pitIntent) {
235
240
  function initializeControlledPitIntent(host) {
236
241
  const sim = host.getSimulation?.();
237
242
  const options = host.getOptions?.();
243
+ initializeControlledPitIntentForSimulation(sim, options);
244
+ }
245
+
246
+ function initializeControlledPitIntentForSimulation(sim, options) {
238
247
  options?.controlledDrivers?.forEach?.((driverId) => {
239
248
  sim?.setAutomaticPitIntentEnabled?.(driverId, false);
240
249
  sim?.setPitIntent?.(driverId, 0);
@@ -243,6 +252,24 @@ function initializeControlledPitIntent(host) {
243
252
  });
244
253
  }
245
254
 
255
+ function resetEnvironmentPitStop(car) {
256
+ const stop = car?.pitStop;
257
+ if (!stop) return;
258
+ stop.status = 'pending';
259
+ stop.phase = null;
260
+ stop.intent = 0;
261
+ stop.queueingForService = false;
262
+ stop.route = null;
263
+ stop.routeProgress = 0;
264
+ stop.routeStartRaceDistance = null;
265
+ stop.routeEndRaceDistance = null;
266
+ stop.serviceRemaining = 0;
267
+ stop.penaltyServiceRemaining = 0;
268
+ stop.penaltyServiceTotal = 0;
269
+ stop.servingPenaltyIds = [];
270
+ stop.serviceProfile = null;
271
+ }
272
+
246
273
  function buildResult({
247
274
  host,
248
275
  episodeState,
@@ -251,6 +278,7 @@ function buildResult({
251
278
  actions = {},
252
279
  controlledDrivers = null,
253
280
  stateOutput = null,
281
+ rewardEnabled = false,
254
282
  }) {
255
283
  const options = host.getOptions();
256
284
  const sim = host.getSimulation();
@@ -276,18 +304,23 @@ function buildResult({
276
304
  ...options,
277
305
  controlledDrivers: resultDrivers,
278
306
  }, episode);
279
- const driverEpisodeInfo = buildDriverEpisodeInfo(episodeState, options, episode);
307
+ const driverEpisode = resultDrivers === options.controlledDrivers
308
+ ? episode
309
+ : evaluateEpisode(observationSnapshot, options, episodeState, options.controlledDrivers);
310
+ const driverEpisodeInfo = buildDriverEpisodeInfo(episodeState, options, driverEpisode);
280
311
  const { state, rewardSnapshot } = buildResultState(sim, observationSnapshot, resolvedStateOutput);
281
- const reward = computeReward({
282
- options: { ...options, controlledDrivers: resultDrivers },
283
- observation,
284
- events,
285
- snapshot: rewardSnapshot,
286
- actions,
287
- previousSnapshot: episodeState.previousSnapshot,
288
- metrics,
289
- driverEpisodeInfo: rewardEpisodeInfo,
290
- });
312
+ const reward = rewardEnabled
313
+ ? computeReward({
314
+ options: { ...options, controlledDrivers: resultDrivers },
315
+ observation,
316
+ events,
317
+ snapshot: rewardSnapshot,
318
+ actions,
319
+ previousSnapshot: episodeState.previousSnapshot,
320
+ metrics,
321
+ driverEpisodeInfo: rewardEpisodeInfo,
322
+ })
323
+ : null;
291
324
  episodeState.lastObservationSnapshot = observationSnapshot;
292
325
  episodeState.lastRewardSnapshot = rewardSnapshot;
293
326
  return {
@@ -304,7 +337,7 @@ function buildResult({
304
337
  elapsedSeconds: observationSnapshot.time,
305
338
  seed: options.seed,
306
339
  trackSeed: options.trackSeed,
307
- controlledDrivers: [...options.controlledDrivers],
340
+ controlledDrivers: [...resultDrivers],
308
341
  actionErrors,
309
342
  endReason: episode.endReason,
310
343
  drivers: driverEpisodeInfo,
@@ -312,6 +345,13 @@ function buildResult({
312
345
  };
313
346
  }
314
347
 
348
+ function applyControlledRunoffResponse(sim, options) {
349
+ options?.controlledDrivers?.forEach?.((driverId) => {
350
+ const car = sim?.cars?.find?.((item) => item.id === driverId);
351
+ if (car) sim?.applyRunoffResponse?.(car);
352
+ });
353
+ }
354
+
315
355
  function buildResultState(sim, observationSnapshot, stateOutput) {
316
356
  if (stateOutput === 'none') return { state: null, rewardSnapshot: observationSnapshot };
317
357
  if (stateOutput === 'minimal') return { state: { snapshot: observationSnapshot }, rewardSnapshot: observationSnapshot };