@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
@@ -0,0 +1,13 @@
1
+ export const TIMING_GAP_MODES = new Set(['interval', 'leader']);
2
+
3
+ export function normalizeTimingGapMode(value) {
4
+ return value === 'leader' ? 'leader' : 'interval';
5
+ }
6
+
7
+ export function getTimingGapModeLabel(mode) {
8
+ return normalizeTimingGapMode(mode) === 'leader' ? 'Gap' : 'Int';
9
+ }
10
+
11
+ export function getNextTimingGapMode(mode) {
12
+ return normalizeTimingGapMode(mode) === 'leader' ? 'interval' : 'leader';
13
+ }
@@ -0,0 +1,25 @@
1
+ const SAFE_ABSOLUTE_URL_PROTOCOLS = new Set(['http:', 'https:']);
2
+
3
+ export function normalizePublicUrlOption(value, fallback) {
4
+ const fallbackValue = typeof fallback === 'string' && fallback.trim() !== ''
5
+ ? fallback.trim()
6
+ : '';
7
+ if (typeof value !== 'string') return fallbackValue;
8
+
9
+ const candidate = value.trim();
10
+ if (candidate === '') return fallbackValue;
11
+ if (/[\u0000-\u001F\u007F]/.test(candidate)) return fallbackValue;
12
+ if (candidate.includes('\\')) return fallbackValue;
13
+ if (candidate.startsWith('#')) return candidate;
14
+ if (candidate.startsWith('//')) return fallbackValue;
15
+
16
+ const schemeMatch = /^[A-Za-z][A-Za-z0-9+.-]*:/.exec(candidate);
17
+ if (!schemeMatch) return candidate;
18
+
19
+ try {
20
+ const url = new URL(candidate);
21
+ return SAFE_ABSOLUTE_URL_PROTOCOLS.has(url.protocol) ? candidate : fallbackValue;
22
+ } catch {
23
+ return fallbackValue;
24
+ }
25
+ }
@@ -1,6 +1,7 @@
1
1
  import { DriverData } from './driverData.js';
2
2
  import { DEMO_PROJECT_DRIVERS } from './demoDrivers.js';
3
3
  import { VehicleData } from './vehicleData.js';
4
+ import { DEFAULT_DARK_THEME_TOKENS, normalizeCssColorToken } from '../config/themeOptions.js';
4
5
 
5
6
  export { DriverData, DRIVER_STAT_DEFINITIONS } from './driverData.js';
6
7
  export { VehicleData, VEHICLE_STAT_DEFINITIONS } from './vehicleData.js';
@@ -187,7 +188,7 @@ function normalizeTeam(team, driver, timingCode) {
187
188
  ...source,
188
189
  id,
189
190
  name,
190
- color: source.color ?? driver.color,
191
+ color: normalizeCssColorToken(source.color ?? driver.color, driver.color),
191
192
  icon: source.icon ?? normalizeTeamIcon(source.name ?? source.id ?? timingCode ?? driver.name),
192
193
  };
193
194
  }
@@ -201,22 +202,26 @@ export function buildChampionshipDriverGrid(drivers = DEMO_PROJECT_DRIVERS, entr
201
202
  const usedTimingCodes = new Set();
202
203
 
203
204
  return drivers.map((driver, index) => {
205
+ const safeDriver = {
206
+ ...driver,
207
+ color: normalizeCssColorToken(driver.color, DEFAULT_DARK_THEME_TOKENS.primary),
208
+ };
204
209
  const entry = {
205
- ...(blueprintByDriverId.get(driver.id) ?? {}),
206
- ...(entryByDriverId.get(driver.id) ?? {}),
210
+ ...(blueprintByDriverId.get(safeDriver.id) ?? {}),
211
+ ...(entryByDriverId.get(safeDriver.id) ?? {}),
207
212
  };
208
- const timingCode = pickUniqueTimingCode(entry.timingName ?? driver.timingName ?? driver.name ?? driver.code, usedTimingCodes);
209
- const driverNumber = entry.driverNumber ?? driver.driverNumber ?? index + 1;
210
- const driverArgs = buildDriverConstructorArgs({ driverId: driver.id, ...entry });
211
- const vehicleArgs = buildVehicleConstructorArgs({ driverId: driver.id, ...entry });
213
+ const timingCode = pickUniqueTimingCode(entry.timingName ?? safeDriver.timingName ?? safeDriver.name ?? safeDriver.code, usedTimingCodes);
214
+ const driverNumber = entry.driverNumber ?? safeDriver.driverNumber ?? index + 1;
215
+ const driverArgs = buildDriverConstructorArgs({ driverId: safeDriver.id, ...entry });
216
+ const vehicleArgs = buildVehicleConstructorArgs({ driverId: safeDriver.id, ...entry });
212
217
 
213
218
  return {
214
- ...driver,
219
+ ...safeDriver,
215
220
  code: timingCode,
216
221
  timingCode,
217
222
  raceName: timingCode,
218
223
  driverNumber,
219
- team: normalizeTeam(entry.team, driver, timingCode),
224
+ team: normalizeTeam(entry.team, safeDriver, timingCode),
220
225
  pace: driverArgs.pace,
221
226
  racecraft: driverArgs.racecraft,
222
227
  consistency: driverArgs.consistency,
@@ -22,6 +22,7 @@ export function createPaddockDriverControllerLoop({
22
22
  let heldFramesRemaining = 0;
23
23
  let policyStep = 0;
24
24
  let runtimeStep = 0;
25
+ let observationCache = {};
25
26
  let running = false;
26
27
  let scheduled = null;
27
28
  let previousActions = {};
@@ -33,6 +34,11 @@ export function createPaddockDriverControllerLoop({
33
34
  if (!observationSpec) observationSpec = runtime.getObservationSpec();
34
35
  }
35
36
 
37
+ async function refreshSpecs() {
38
+ actionSpec = runtime.getActionSpec();
39
+ observationSpec = runtime.getObservationSpec();
40
+ }
41
+
36
42
  async function ensureInitialized(context) {
37
43
  if (initialized) return;
38
44
  await controller.init?.(context);
@@ -47,9 +53,10 @@ export function createPaddockDriverControllerLoop({
47
53
  heldFramesRemaining = 0;
48
54
  policyStep = 0;
49
55
  runtimeStep = Number(result?.info?.step ?? 0);
56
+ observationCache = { ...(result?.observation ?? {}) };
50
57
  previousActions = {};
51
58
  lastError = null;
52
- await ensureSpecs();
59
+ await refreshSpecs();
53
60
  const context = buildContext({ resetDriverIds: controlledDrivers() });
54
61
  await ensureInitialized(context);
55
62
  await controller.reset?.(context);
@@ -71,6 +78,10 @@ export function createPaddockDriverControllerLoop({
71
78
  const resetDriverIds = Object.keys(placements);
72
79
  previousResult = result;
73
80
  result = runtime.resetDrivers(placements, resultOptions);
81
+ observationCache = {
82
+ ...observationCache,
83
+ ...(result?.observation ?? {}),
84
+ };
74
85
  heldActions = null;
75
86
  heldFramesRemaining = 0;
76
87
  lastError = null;
@@ -106,6 +117,7 @@ export function createPaddockDriverControllerLoop({
106
117
  const previousActionsForStep = previousActions;
107
118
  previousResult = result;
108
119
  result = runtime.step(actions);
120
+ observationCache = { ...(result?.observation ?? {}) };
109
121
  heldFramesRemaining -= 1;
110
122
  runtimeStep = Number(result?.info?.step ?? runtimeStep + 1);
111
123
  previousActions = actions;
@@ -156,8 +168,7 @@ export function createPaddockDriverControllerLoop({
156
168
 
157
169
  function stop() {
158
170
  running = false;
159
- if (scheduled && typeof scheduled.cancel === 'function') scheduled.cancel();
160
- else if (typeof scheduled === 'number' && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(scheduled);
171
+ cancelScheduled(scheduled);
161
172
  scheduled = null;
162
173
  }
163
174
 
@@ -167,7 +178,10 @@ export function createPaddockDriverControllerLoop({
167
178
 
168
179
  function buildContext(extra = {}) {
169
180
  const drivers = controlledDrivers();
170
- const observation = result?.observation ?? runtime.getObservation?.() ?? {};
181
+ const observation = {
182
+ ...observationCache,
183
+ ...(result?.observation ?? runtime.getObservation?.() ?? {}),
184
+ };
171
185
  return {
172
186
  runtime,
173
187
  mode,
@@ -235,9 +249,24 @@ function now() {
235
249
  : Date.now();
236
250
  }
237
251
 
252
+ function cancelScheduled(handle) {
253
+ if (handle == null) return;
254
+ if (typeof handle.cancel === 'function') {
255
+ handle.cancel();
256
+ return;
257
+ }
258
+ if (typeof handle === 'number') {
259
+ if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(handle);
260
+ if (typeof clearTimeout === 'function') clearTimeout(handle);
261
+ }
262
+ }
263
+
238
264
  function defaultScheduler() {
239
265
  if (typeof requestAnimationFrame === 'function') {
240
- return (callback) => requestAnimationFrame(callback);
266
+ return (callback) => {
267
+ const id = requestAnimationFrame(callback);
268
+ return { cancel: () => cancelAnimationFrame(id) };
269
+ };
241
270
  }
242
271
  return (callback) => {
243
272
  const id = setTimeout(callback, 0);
@@ -45,8 +45,9 @@ export function buildDriverEpisodeInfo(episodeState, options, episode) {
45
45
  return Object.fromEntries(options.controlledDrivers.map((driverId) => {
46
46
  const state = ensureDriverEpisodeState(episodeState, driverId);
47
47
  const maxStepTruncated = state.episodeStep >= options.episode.maxSteps;
48
+ const aggregateTruncated = episode.truncated && episode.endReason !== 'all-drivers-ended';
48
49
  const terminated = Boolean(state.terminated || episode.terminated);
49
- const truncated = Boolean(state.truncated || maxStepTruncated || episode.truncated);
50
+ const truncated = Boolean(state.truncated || maxStepTruncated || aggregateTruncated);
50
51
  const endReason = state.endReason ??
51
52
  (terminated ? episode.endReason : null) ??
52
53
  (maxStepTruncated ? 'max-steps' : null) ??
@@ -67,13 +68,21 @@ export function evaluateEpisode(snapshot, options, episodeState, controlledDrive
67
68
  }
68
69
  let allTerminated = controlledDrivers.length > 0;
69
70
  let allTruncated = controlledDrivers.length > 0;
71
+ let allEnded = controlledDrivers.length > 0;
72
+ let anyTruncated = false;
70
73
  let firstTerminatedReason = null;
71
74
  for (const driverId of controlledDrivers) {
72
75
  const state = ensureDriverEpisodeState(episodeState, driverId);
76
+ if (!state.terminated && state.episodeStep >= options.episode.maxSteps) {
77
+ state.truncated = true;
78
+ state.endReason ??= 'max-steps';
79
+ }
73
80
  if (!state.terminated) allTerminated = false;
74
81
  else firstTerminatedReason ??= state.endReason;
75
82
  if (!state.truncated && state.episodeStep < options.episode.maxSteps) allTruncated = false;
76
- if (!allTerminated && !allTruncated) break;
83
+ if (state.truncated || state.episodeStep >= options.episode.maxSteps) anyTruncated = true;
84
+ if (!state.terminated && !state.truncated && state.episodeStep < options.episode.maxSteps) allEnded = false;
85
+ if (!allTerminated && !allTruncated && !allEnded) break;
77
86
  }
78
87
  if (allTerminated) {
79
88
  return { terminated: true, truncated: false, endReason: controlledDrivers.length === 1 ? firstTerminatedReason : 'all-drivers-terminated' };
@@ -81,6 +90,9 @@ export function evaluateEpisode(snapshot, options, episodeState, controlledDrive
81
90
  if (allTruncated) {
82
91
  return { terminated: false, truncated: true, endReason: 'max-steps' };
83
92
  }
93
+ if (allEnded) {
94
+ return { terminated: false, truncated: anyTruncated, endReason: 'all-drivers-ended' };
95
+ }
84
96
  return { terminated: false, truncated: false, endReason: null };
85
97
  }
86
98
 
@@ -1,5 +1,5 @@
1
1
  export type TireCompound = 'S' | 'M' | 'H';
2
- export type PaddockPhysicsMode = 'arcade' | 'simulator';
2
+ export type PaddockPhysicsMode = 'arcade' | 'advanced';
3
3
  export type PaddockProceduralTrackProfile = 'race' | 'training-short' | 'training-medium' | 'training-technical';
4
4
 
5
5
  export interface PaddockProceduralTrackOptions {
@@ -364,6 +364,13 @@ export interface PaddockTrackLookaheadObservation {
364
364
  headingDeltaRadians: number;
365
365
  }
366
366
 
367
+ export interface PaddockAppliedControlsObservation {
368
+ steering: number;
369
+ steeringRadians: number;
370
+ throttle: number;
371
+ brake: number;
372
+ }
373
+
367
374
  export interface PaddockDriverObservationObject {
368
375
  profile: string;
369
376
  self: {
@@ -375,6 +382,7 @@ export interface PaddockDriverObservationObject {
375
382
  steeringAngleRadians: number;
376
383
  throttle: number;
377
384
  brake: number;
385
+ appliedControls: PaddockAppliedControlsObservation | null;
378
386
  lap: number;
379
387
  completedLaps: number;
380
388
  lapProgressMeters: number;
@@ -413,7 +421,7 @@ export interface PaddockDriverObservationObject {
413
421
  };
414
422
  contactPatches: PaddockContactPatchObservation[];
415
423
  race: {
416
- position: number;
424
+ position: number | null;
417
425
  totalCars: number;
418
426
  raceMode: string;
419
427
  pitLaneOpen: boolean;
@@ -470,7 +478,6 @@ export interface PaddockEnvironmentOptions {
470
478
  seed?: number;
471
479
  trackSeed?: number;
472
480
  trackGeneration?: PaddockProceduralTrackOptions;
473
- trackQueryIndex?: boolean;
474
481
  totalLaps?: number;
475
482
  frameSkip?: number;
476
483
  physicsMode?: PaddockPhysicsMode;
@@ -590,12 +597,18 @@ export interface PaddockActionSpec {
590
597
  }
591
598
 
592
599
  export interface PaddockObservationSpec {
593
- version: 2 | 4;
600
+ version: 3 | 6;
594
601
  controlledDrivers: string[];
595
602
  object: Record<string, unknown>;
596
603
  vector: {
597
604
  schema: PaddockObservationSchemaEntry[];
598
605
  };
606
+ perDriver: Record<string, {
607
+ object: Record<string, unknown>;
608
+ vector: {
609
+ schema: PaddockObservationSchemaEntry[];
610
+ };
611
+ }>;
599
612
  }
600
613
 
601
614
  export interface PaddockRolloutTransition {
@@ -0,0 +1,200 @@
1
+ const MODEL_RAY_SURFACE_CHANNELS = Object.freeze(['kerb', 'illegalSurface']);
2
+
3
+ export function buildObservationVector(source, sensors, { includeSchema = true, vectorType = 'array' } = {}) {
4
+ const includePhysicalDriverSenses = source.profile === 'physical-driver';
5
+ const schema = includeSchema ? [
6
+ { name: 'self.speedKph', unit: 'kph', scale: 'fixed:400' },
7
+ { name: 'self.speedMetersPerSecond', unit: 'm/s', scale: 'fixed:120' },
8
+ { name: 'self.steeringAngleRadians', unit: 'rad', scale: 'fixed:pi' },
9
+ { name: 'self.throttle', scale: '0..1' },
10
+ { name: 'self.brake', scale: '0..1' },
11
+ { name: 'self.lateralG', scale: 'fixed:8g' },
12
+ { name: 'self.longitudinalG', scale: 'fixed:6g' },
13
+ { name: 'self.gripUsage', scale: '0..2' },
14
+ { name: 'self.slipAngleRadians', unit: 'rad', scale: 'fixed:pi' },
15
+ { name: 'self.tractionLimited', scale: 'boolean' },
16
+ { name: 'self.lapProgressRatio', scale: '0..1' },
17
+ { name: 'self.trackOffsetMeters', unit: 'm', scale: 'fixed:meters' },
18
+ { name: 'self.trackHeadingErrorRadians', unit: 'rad', scale: 'fixed:pi' },
19
+ { name: 'self.onTrack', scale: 'boolean' },
20
+ { name: 'self.inPitLane', scale: 'boolean' },
21
+ { name: 'self.tireEnergy', scale: '0..100' },
22
+ { name: 'self.pitIntent', scale: '0..2' },
23
+ { name: 'self.pitStopActive', scale: 'boolean' },
24
+ { name: 'race.positionNormalized', scale: '0..1' },
25
+ { name: 'race.raceModeGreen', scale: 'boolean' },
26
+ { name: 'race.raceModeSafetyCar', scale: 'boolean' },
27
+ { name: 'race.redFlag', scale: 'boolean' },
28
+ { name: 'race.pitLaneOpen', scale: 'boolean' },
29
+ { name: 'track.curvature', scale: 'track-curvature' },
30
+ ] : null;
31
+ const vector = [
32
+ source.self.speedKph / 400,
33
+ source.self.speedMetersPerSecond / 120,
34
+ source.self.steeringAngleRadians / Math.PI,
35
+ source.self.throttle,
36
+ source.self.brake,
37
+ source.self.lateralG / 8,
38
+ source.self.longitudinalG / 6,
39
+ source.self.gripUsage / 2,
40
+ source.self.slipAngleRadians / Math.PI,
41
+ source.self.tractionLimited ? 1 : 0,
42
+ normalizeLapProgress(source),
43
+ source.self.trackOffsetMeters,
44
+ source.self.trackHeadingErrorRadians / Math.PI,
45
+ source.self.onTrack ? 1 : 0,
46
+ source.self.inPitLane ? 1 : 0,
47
+ (source.self.tireEnergy ?? 0) / 100,
48
+ (source.self.pitIntent ?? 0) / 2,
49
+ source.self.pitStopStatus && source.self.pitStopStatus !== 'pending' && source.self.pitStopStatus !== 'completed' ? 1 : 0,
50
+ normalizeRacePosition(source),
51
+ source.race.raceMode === 'green' ? 1 : 0,
52
+ source.race.raceMode === 'safety-car' ? 1 : 0,
53
+ source.race.redFlag ? 1 : 0,
54
+ source.race.pitLaneOpen ? 1 : 0,
55
+ source.track.curvature ?? 0,
56
+ ];
57
+ if (includePhysicalDriverSenses) {
58
+ pushSchema(schema,
59
+ { name: 'self.yawRateRadiansPerSecond', unit: 'rad/s', scale: 'fixed:pi' },
60
+ { name: 'trackRelation.leftBoundaryMeters', unit: 'm', scale: 'fixed:meters' },
61
+ { name: 'trackRelation.rightBoundaryMeters', unit: 'm', scale: 'fixed:meters' },
62
+ { name: 'trackRelation.legalWidthMeters', unit: 'm', scale: 'fixed:meters' },
63
+ );
64
+ vector.push(
65
+ source.self.yawRateRadiansPerSecond / Math.PI,
66
+ source.trackRelation.leftBoundaryMeters,
67
+ source.trackRelation.rightBoundaryMeters,
68
+ source.trackRelation.legalWidthMeters,
69
+ );
70
+ source.contactPatches.forEach((patch, index) => {
71
+ pushSchema(schema,
72
+ { name: `contactPatches[${index}].present`, scale: 'boolean' },
73
+ { name: `contactPatches[${index}].surfaceCode`, scale: 'surface-code' },
74
+ { name: `contactPatches[${index}].onLegalSurface`, scale: 'boolean' },
75
+ { name: `contactPatches[${index}].signedOffsetMeters`, unit: 'm', scale: 'fixed:meters' },
76
+ { name: `contactPatches[${index}].crossTrackErrorMeters`, unit: 'm', scale: 'fixed:meters' },
77
+ );
78
+ vector.push(
79
+ patch.present ? 1 : 0,
80
+ patch.surfaceCode / 5,
81
+ patch.onLegalSurface ? 1 : 0,
82
+ patch.signedOffsetMeters,
83
+ patch.crossTrackErrorMeters,
84
+ );
85
+ });
86
+ }
87
+ source.track.lookahead.forEach((sample, index) => {
88
+ pushSchema(schema,
89
+ { name: `track.lookahead[${index}].curvature`, scale: 'track-curvature' },
90
+ { name: `track.lookahead[${index}].headingDeltaRadians`, unit: 'rad', scale: 'fixed:pi' },
91
+ );
92
+ vector.push(sample.curvature ?? 0, (sample.headingDeltaRadians ?? 0) / Math.PI);
93
+ });
94
+ source.rays.forEach((ray, index) => {
95
+ pushSchema(schema,
96
+ { name: `rays[${index}].track.distanceRatio`, scale: '0..1' },
97
+ { name: `rays[${index}].track.hit`, scale: 'boolean' },
98
+ { name: `rays[${index}].track.kindExit`, scale: 'boolean' },
99
+ { name: `rays[${index}].track.kindEntry`, scale: 'boolean' },
100
+ { name: `rays[${index}].car.distanceRatio`, scale: '0..1' },
101
+ { name: `rays[${index}].car.hit`, scale: 'boolean' },
102
+ { name: `rays[${index}].car.relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
103
+ { name: `rays[${index}].car.targetTypeReplayGhost`, scale: 'boolean' },
104
+ );
105
+ vector.push(
106
+ ratio(ray.track.distanceMeters, ray.lengthMeters),
107
+ ray.track.hit ? 1 : 0,
108
+ ray.track.kind === 'exit' ? 1 : 0,
109
+ ray.track.kind === 'entry' ? 1 : 0,
110
+ ratio(ray.car.distanceMeters, ray.lengthMeters),
111
+ ray.car.hit ? 1 : 0,
112
+ ray.car.relativeSpeedKph / 200,
113
+ ray.car.targetType === 'replayGhost' ? 1 : 0,
114
+ );
115
+ MODEL_RAY_SURFACE_CHANNELS.forEach((channel) => {
116
+ if (sensors.rays.channels?.includes?.(channel)) {
117
+ pushSchema(schema,
118
+ { name: `rays[${index}].${channel}.distanceRatio`, scale: '0..1' },
119
+ { name: `rays[${index}].${channel}.hit`, scale: 'boolean' },
120
+ );
121
+ vector.push(
122
+ ratio(ray[channel]?.distanceMeters ?? ray.lengthMeters, ray.lengthMeters),
123
+ ray[channel]?.hit ? 1 : 0,
124
+ );
125
+ }
126
+ });
127
+ });
128
+ const nearbyLimit = sensors.nearbyCars.enabled ? (sensors.nearbyCars.maxCars ?? source.nearbyCars.length) : 0;
129
+ const nearbyRadius = sensors.nearbyCars.radiusMeters ?? 150;
130
+ for (let index = 0; index < nearbyLimit; index += 1) {
131
+ const nearby = source.nearbyCars[index] ?? null;
132
+ pushSchema(schema,
133
+ { name: `nearbyCars[${index}].present`, scale: 'boolean' },
134
+ { name: `nearbyCars[${index}].relativeForwardRatio`, scale: '-1..1' },
135
+ { name: `nearbyCars[${index}].relativeRightRatio`, scale: '-1..1' },
136
+ { name: `nearbyCars[${index}].relativeDistanceRatio`, scale: '0..1' },
137
+ { name: `nearbyCars[${index}].relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
138
+ { name: `nearbyCars[${index}].relativeHeadingRadians`, unit: 'rad', scale: 'fixed:pi' },
139
+ { name: `nearbyCars[${index}].ahead`, scale: 'boolean' },
140
+ { name: `nearbyCars[${index}].sameLap`, scale: 'boolean' },
141
+ { name: `nearbyCars[${index}].entityTypeReplayGhost`, scale: 'boolean' },
142
+ );
143
+ vector.push(
144
+ nearby ? 1 : 0,
145
+ clampRatio((nearby?.relativeForwardMeters ?? 0) / nearbyRadius),
146
+ clampRatio((nearby?.relativeRightMeters ?? 0) / nearbyRadius),
147
+ ratio(nearby?.relativeDistanceMeters ?? nearbyRadius, nearbyRadius),
148
+ (nearby?.relativeSpeedKph ?? 0) / 200,
149
+ (nearby?.relativeHeadingRadians ?? 0) / Math.PI,
150
+ nearby?.ahead ? 1 : 0,
151
+ nearby?.sameLap ? 1 : 0,
152
+ nearby?.entityType === 'replayGhost' ? 1 : 0,
153
+ );
154
+ if (includePhysicalDriverSenses) {
155
+ pushSchema(schema,
156
+ { name: `nearbyCars[${index}].behind`, scale: 'boolean' },
157
+ { name: `nearbyCars[${index}].closingRateMetersPerSecond`, unit: 'm/s', scale: 'fixed:100' },
158
+ { name: `nearbyCars[${index}].timeToContactSeconds`, unit: 's', scale: 'fixed:10' },
159
+ { name: `nearbyCars[${index}].leftOverlap`, scale: 'boolean' },
160
+ { name: `nearbyCars[${index}].rightOverlap`, scale: 'boolean' },
161
+ );
162
+ vector.push(
163
+ nearby?.behind ? 1 : 0,
164
+ (nearby?.closingRateMetersPerSecond ?? 0) / 100,
165
+ ratio(nearby?.timeToContactSeconds ?? 10, 10),
166
+ nearby?.leftOverlap ? 1 : 0,
167
+ nearby?.rightOverlap ? 1 : 0,
168
+ );
169
+ }
170
+ }
171
+ return { vector: finalizeVector(vector, vectorType), schema: schema ?? [] };
172
+ }
173
+
174
+ function finalizeVector(vector, vectorType = 'array') {
175
+ return vectorType === 'float32' ? Float32Array.from(vector) : vector;
176
+ }
177
+
178
+ function pushSchema(schema, ...entries) {
179
+ if (schema) schema.push(...entries);
180
+ }
181
+
182
+ function normalizeLapProgress(source) {
183
+ return ratio(source.self.lapProgressMeters, source.track.lengthMeters || 1);
184
+ }
185
+
186
+ function normalizeRacePosition(source) {
187
+ const totalCars = Number(source.race.totalCars);
188
+ const position = Number(source.race.position);
189
+ if (!Number.isFinite(totalCars) || totalCars <= 1 || !Number.isFinite(position)) return 0;
190
+ return ratio(position - 1, totalCars - 1);
191
+ }
192
+
193
+ function ratio(value, max) {
194
+ const finite = Number.isFinite(value) ? value : max;
195
+ return Math.max(0, Math.min(1, finite / Math.max(1e-9, max)));
196
+ }
197
+
198
+ function clampRatio(value) {
199
+ return Math.max(-1, Math.min(1, Number.isFinite(value) ? value : 0));
200
+ }