@inventure71/paddockjs 2.0.1 → 4.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 (136) hide show
  1. package/CHANGELOG.md +176 -1
  2. package/INSTALL_AND_UPDATE.md +24 -4
  3. package/README.md +84 -512
  4. package/docs/architecture.md +38 -24
  5. package/docs/component_inventory.md +72 -0
  6. package/docs/composable-layouts.md +77 -0
  7. package/docs/concepts.md +2 -2
  8. package/docs/custom_model_controller.md +6 -6
  9. package/docs/data-helpers.md +50 -0
  10. package/docs/data_contract.md +104 -27
  11. package/docs/getting-started.md +75 -0
  12. package/docs/headless-environment.md +68 -0
  13. package/docs/index.md +18 -4
  14. package/docs/learnings.md +6 -6
  15. package/docs/rules.md +12 -8
  16. package/docs/sense_contract.md +1 -1
  17. package/docs/system_specs.md +71 -21
  18. package/docs/theming.md +64 -0
  19. package/docs/training.md +26 -19
  20. package/docs/troubleshooting.md +76 -0
  21. package/docs/upgrading-hosts-to-3.md +42 -0
  22. package/examples/python/README.md +6 -0
  23. package/examples/python/base_policy_server.py +19 -0
  24. package/package.json +10 -3
  25. package/src/api/PaddockSimulatorController.js +150 -24
  26. package/src/api/runtimeTheme.js +59 -0
  27. package/src/app/F1SimulatorApp.js +213 -29
  28. package/src/app/banners/raceDataBannerController.js +71 -36
  29. package/src/app/banners/stewardMessageController.js +5 -4
  30. package/src/app/camera/cameraConstants.js +2 -0
  31. package/src/app/camera/cameraController.js +86 -29
  32. package/src/app/camera/cameraGeometry.js +12 -1
  33. package/src/app/domBindings.js +89 -9
  34. package/src/app/layoutScheduler.js +27 -0
  35. package/src/app/layoutSupport.js +165 -0
  36. package/src/app/raceOverlayClearanceSupport.js +158 -0
  37. package/src/app/readouts/carOverviewRenderer.js +16 -8
  38. package/src/app/readouts/controlStateRenderer.js +1 -1
  39. package/src/app/readouts/raceStatusRenderer.js +30 -21
  40. package/src/app/readouts/readoutFormatters.js +2 -0
  41. package/src/app/readouts/telemetryRenderer.js +17 -8
  42. package/src/app/readouts/timingTowerRenderer.js +262 -49
  43. package/src/app/runtime/frameLoop.js +5 -1
  44. package/src/app/runtime/runtimeVisibility.js +66 -5
  45. package/src/config/cssValues.js +16 -0
  46. package/src/config/defaultOptions.js +58 -38
  47. package/src/config/themeOptions.js +830 -0
  48. package/src/config/timingGapMode.js +13 -0
  49. package/src/config/urlOptions.js +25 -0
  50. package/src/data/championship.js +14 -9
  51. package/src/data/index.d.ts +212 -0
  52. package/src/data/index.js +14 -0
  53. package/src/environment/controllerLoop.js +17 -3
  54. package/src/environment/episode.js +11 -1
  55. package/src/environment/index.d.ts +1 -2
  56. package/src/environment/metrics.js +59 -16
  57. package/src/environment/observationVector.js +50 -33
  58. package/src/environment/observations.js +89 -20
  59. package/src/environment/options.js +1 -1
  60. package/src/environment/runtime.js +44 -11
  61. package/src/environment/scenarios.js +1 -1
  62. package/src/environment/sensors/carRays.js +48 -37
  63. package/src/environment/sensors/index.js +501 -97
  64. package/src/environment/sensors/indexedRayBands.js +134 -46
  65. package/src/environment/sensors/rayBandTrace.js +1171 -0
  66. package/src/environment/sensors/rayChannels.js +108 -0
  67. package/src/environment/sensors/rayConfig.js +71 -25
  68. package/src/environment/sensors/rayGeometry.js +23 -17
  69. package/src/environment/sensors/sensorTargets.js +67 -1
  70. package/src/environment/sensors/surfaceRays.js +167 -118
  71. package/src/environment/sensors/trackRays.js +157 -48
  72. package/src/index.d.ts +153 -9
  73. package/src/index.js +52 -3
  74. package/src/simulation/collisionGeometry.js +260 -96
  75. package/src/simulation/driver/driverController.js +1 -1
  76. package/src/simulation/driver/edgeRecovery.js +1 -1
  77. package/src/simulation/driver/racingControls.js +2 -2
  78. package/src/simulation/driver/rejoinControls.js +2 -2
  79. package/src/simulation/pit/pitFlow.js +21 -9
  80. package/src/simulation/pit/pitKinematics.js +2 -2
  81. package/src/simulation/pit/pitQueue.js +5 -3
  82. package/src/simulation/pit/pitRouteMovement.js +9 -4
  83. package/src/simulation/pit/pitRouting.js +79 -46
  84. package/src/simulation/pit/pitState.js +1 -0
  85. package/src/simulation/pit/pitTireService.js +7 -3
  86. package/src/simulation/race/raceFinish.js +25 -8
  87. package/src/simulation/race/raceOrder.js +189 -32
  88. package/src/simulation/race/raceProgress.js +129 -37
  89. package/src/simulation/race/raceSetup.js +4 -6
  90. package/src/simulation/race/raceStep.js +26 -14
  91. package/src/simulation/race/safetyCar.js +1 -1
  92. package/src/simulation/race/stalledDnf.js +1 -1
  93. package/src/simulation/raceSimulation.js +20 -17
  94. package/src/simulation/rules/collisionSteward.js +40 -23
  95. package/src/simulation/rules/rulesReview.js +77 -56
  96. package/src/simulation/snapshots/raceSnapshots.js +37 -0
  97. package/src/simulation/timing/drsTiming.js +143 -30
  98. package/src/simulation/timing/gapEstimation.js +25 -3
  99. package/src/simulation/timing/lapTelemetry.js +138 -40
  100. package/src/simulation/timing/raceTiming.js +5 -1
  101. package/src/simulation/timing/sectorPerformance.js +104 -26
  102. package/src/simulation/timing/timingHistory.js +229 -14
  103. package/src/simulation/timing/timingLines.js +65 -7
  104. package/src/simulation/track/pitLaneLayout.js +30 -3
  105. package/src/simulation/track/pitLaneState.js +218 -19
  106. package/src/simulation/track/spatialQueries.js +66 -33
  107. package/src/simulation/track/trackMath.js +91 -24
  108. package/src/simulation/track/trackModel.js +2 -0
  109. package/src/simulation/track/trackQueryIndex.js +1468 -189
  110. package/src/simulation/track/trackQueryPitIndex.js +138 -14
  111. package/src/simulation/track/trackQueryScratch.js +7 -0
  112. package/src/simulation/track/trackQueryStats.js +25 -0
  113. package/src/simulation/track/trackSnapshotJson.js +139 -0
  114. package/src/simulation/track/trackStatePolicy.js +249 -2
  115. package/src/simulation/vehicle/contactResolution.js +99 -62
  116. package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
  117. package/src/simulation/vehicle/pitWheelSurface.js +81 -28
  118. package/src/simulation/vehicle/runoffResponse.js +54 -29
  119. package/src/simulation/vehicle/vehicleGeometry.js +245 -51
  120. package/src/simulation/vehicle/vehicleKinematics.js +2 -2
  121. package/src/simulation/vehicle/vehiclePhysics.js +45 -30
  122. package/src/simulation/vehicle/vehicleSnapshots.js +35 -1
  123. package/src/simulation/vehicle/vehicleState.js +7 -1
  124. package/src/simulation/vehicle/wheelSurface.js +807 -92
  125. package/src/simulation/warmup/runtimeWarmup.js +0 -1
  126. package/src/styles.css +1568 -519
  127. package/src/ui/bannerTemplates.js +38 -28
  128. package/src/ui/cameraControlsTemplate.js +16 -5
  129. package/src/ui/carOverviewTemplate.js +12 -5
  130. package/src/ui/raceCanvasTemplate.js +31 -8
  131. package/src/ui/raceControlsTemplate.js +27 -10
  132. package/src/ui/shellTemplate.js +3 -1
  133. package/src/ui/telemetryDrawerTemplate.js +23 -10
  134. package/src/ui/telemetryTemplates.js +58 -32
  135. package/src/ui/templateUtils.js +48 -0
  136. 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,
@@ -0,0 +1,212 @@
1
+ export type TireCompound = 'S' | 'M' | 'H';
2
+ export type PaddockProceduralTrackProfile = 'race' | 'training-short' | 'training-medium' | 'training-technical';
3
+
4
+ export interface PaddockProceduralTrackOptions {
5
+ profile?: PaddockProceduralTrackProfile;
6
+ minLengthMeters?: number;
7
+ maxLengthMeters?: number;
8
+ startStraightMeters?: number;
9
+ includePitLane?: boolean;
10
+ length?: {
11
+ minMeters?: number;
12
+ maxMeters?: number;
13
+ };
14
+ startStraight?: {
15
+ gridMeters?: number;
16
+ exitMeters?: number;
17
+ blendMeters?: number;
18
+ lockExtraMeters?: number;
19
+ };
20
+ pitLane?: {
21
+ enabled?: boolean;
22
+ };
23
+ shape?: {
24
+ scale?: number;
25
+ cornerDensity?: number;
26
+ variation?: number;
27
+ };
28
+ validation?: {
29
+ minClearanceMultiplier?: number;
30
+ minShapeVariation?: number;
31
+ minNonAdjacentArcMeters?: number;
32
+ maxLocalTurnRadians?: number;
33
+ maxSampleHeadingDeltaRadians?: number;
34
+ };
35
+ attempts?: {
36
+ primary?: number;
37
+ fallback?: number;
38
+ };
39
+ }
40
+
41
+ export interface CustomField {
42
+ label: string;
43
+ value: string;
44
+ }
45
+
46
+ export type CustomFieldInput = CustomField[] | Record<string, string>;
47
+ export type PaddockThemeSelector = 'default' | 'active' | 'selectedTeam' | 'team' | `team:${string}` | string;
48
+
49
+ export interface PaddockPitCrewStats {
50
+ speed?: number;
51
+ consistency?: number;
52
+ reliability?: number;
53
+ }
54
+
55
+ export interface TeamData {
56
+ id?: string;
57
+ name?: string;
58
+ color?: string;
59
+ icon?: string;
60
+ theme?: PaddockThemeSelector;
61
+ pitCrew?: PaddockPitCrewStats;
62
+ pitCrewStats?: PaddockPitCrewStats;
63
+ }
64
+
65
+ export interface SimulatorDriver {
66
+ id: string;
67
+ name: string;
68
+ color: string;
69
+ link?: string;
70
+ icon?: string;
71
+ code?: string;
72
+ timingName?: string;
73
+ tire?: TireCompound;
74
+ raceData?: string[];
75
+ customFields?: CustomFieldInput;
76
+ team?: TeamData | null;
77
+ driverNumber?: number;
78
+ }
79
+
80
+ export interface DriverRatings {
81
+ pace: number;
82
+ racecraft: number;
83
+ aggression: number;
84
+ riskTolerance: number;
85
+ patience: number;
86
+ consistency: number;
87
+ }
88
+
89
+ export interface VehicleRatings {
90
+ power: number;
91
+ braking: number;
92
+ aero: number;
93
+ dragEfficiency: number;
94
+ mechanicalGrip: number;
95
+ weightControl: number;
96
+ tireCare: number;
97
+ }
98
+
99
+ export interface DriverBlueprint extends Partial<DriverRatings> {
100
+ customFields?: CustomFieldInput;
101
+ }
102
+
103
+ export interface VehicleBlueprint extends Partial<VehicleRatings> {
104
+ id?: string | null;
105
+ name?: string | null;
106
+ customFields?: CustomFieldInput;
107
+ }
108
+
109
+ export interface DriverConstructorArgs {
110
+ ratings: DriverRatings;
111
+ customFields: CustomField[];
112
+ pace: number;
113
+ racecraft: number;
114
+ consistency: number;
115
+ personality: {
116
+ aggression: number;
117
+ riskTolerance: number;
118
+ patience: number;
119
+ };
120
+ }
121
+
122
+ export interface VehicleConstructorArgs {
123
+ id: string | null;
124
+ name: string | null;
125
+ ratings: VehicleRatings;
126
+ customFields: CustomField[];
127
+ powerNewtons: number;
128
+ brakeNewtons: number;
129
+ downforceCoefficient: number;
130
+ dragCoefficient: number;
131
+ tireGrip: number;
132
+ mass: number;
133
+ tireCare: number;
134
+ }
135
+
136
+ export class DriverData {
137
+ constructor(input?: DriverBlueprint);
138
+ readonly pace: number;
139
+ readonly racecraft: number;
140
+ readonly aggression: number;
141
+ readonly riskTolerance: number;
142
+ readonly patience: number;
143
+ readonly consistency: number;
144
+ readonly customFields: CustomField[];
145
+ ratings(): DriverRatings;
146
+ toConstructorArgs(): DriverConstructorArgs;
147
+ }
148
+
149
+ export class VehicleData {
150
+ constructor(input?: VehicleBlueprint);
151
+ readonly id: string | null;
152
+ readonly name: string | null;
153
+ readonly power: number;
154
+ readonly braking: number;
155
+ readonly aero: number;
156
+ readonly dragEfficiency: number;
157
+ readonly mechanicalGrip: number;
158
+ readonly weightControl: number;
159
+ readonly tireCare: number;
160
+ readonly customFields: CustomField[];
161
+ ratings(): VehicleRatings;
162
+ toConstructorArgs(): VehicleConstructorArgs;
163
+ }
164
+
165
+ export interface ChampionshipEntryBlueprint {
166
+ driverId: string;
167
+ driverNumber?: number;
168
+ timingName?: string;
169
+ driver?: DriverData | DriverBlueprint;
170
+ vehicle?: VehicleData | VehicleBlueprint;
171
+ team?: TeamData;
172
+ }
173
+
174
+ export interface NormalizedSimulatorDriver extends SimulatorDriver {
175
+ code: string;
176
+ timingCode: string;
177
+ raceName: string;
178
+ driverNumber: number;
179
+ team: TeamData | null;
180
+ pace: number;
181
+ racecraft: number;
182
+ consistency: number;
183
+ personality: DriverConstructorArgs['personality'];
184
+ vehicle: VehicleConstructorArgs;
185
+ constructorArgs: {
186
+ driver: DriverConstructorArgs;
187
+ vehicle: VehicleConstructorArgs;
188
+ };
189
+ championship: {
190
+ id: string;
191
+ name: string;
192
+ season: number;
193
+ entryIndex: number;
194
+ vehicleId: string | null;
195
+ };
196
+ }
197
+
198
+ export const CHAMPIONSHIP_ENTRY_BLUEPRINTS: ChampionshipEntryBlueprint[];
199
+ export const DEMO_PROJECT_DRIVERS: SimulatorDriver[];
200
+
201
+ export function buildChampionshipDriverGrid(
202
+ drivers?: SimulatorDriver[],
203
+ entries?: ChampionshipEntryBlueprint[],
204
+ ): NormalizedSimulatorDriver[];
205
+ export function formatDriverNumber(driverNumber: number | string | null | undefined): string;
206
+ export function normalizeSimulatorDrivers(
207
+ drivers: SimulatorDriver[],
208
+ options?: { entries?: ChampionshipEntryBlueprint[]; caller?: string },
209
+ ): NormalizedSimulatorDriver[];
210
+ export function createProceduralTrack(seed?: number | string, options?: PaddockProceduralTrackOptions): unknown;
211
+ export function kphToSimSpeed(kph: number): number;
212
+ export function simSpeedToKph(simSpeed: number): number;
@@ -0,0 +1,14 @@
1
+ export {
2
+ CHAMPIONSHIP_ENTRY_BLUEPRINTS,
3
+ DriverData,
4
+ VehicleData,
5
+ buildChampionshipDriverGrid,
6
+ formatDriverNumber,
7
+ } from './championship.js';
8
+ export { DEMO_PROJECT_DRIVERS } from './demoDrivers.js';
9
+ export { normalizeSimulatorDrivers } from './normalizeDrivers.js';
10
+ export { createProceduralTrack } from '../simulation/track/trackModel.js';
11
+ export {
12
+ kphToSimSpeed,
13
+ simSpeedToKph,
14
+ } from '../simulation/units.js';
@@ -168,8 +168,7 @@ export function createPaddockDriverControllerLoop({
168
168
 
169
169
  function stop() {
170
170
  running = false;
171
- if (scheduled && typeof scheduled.cancel === 'function') scheduled.cancel();
172
- else if (typeof scheduled === 'number' && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(scheduled);
171
+ cancelScheduled(scheduled);
173
172
  scheduled = null;
174
173
  }
175
174
 
@@ -250,9 +249,24 @@ function now() {
250
249
  : Date.now();
251
250
  }
252
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
+
253
264
  function defaultScheduler() {
254
265
  if (typeof requestAnimationFrame === 'function') {
255
- return (callback) => requestAnimationFrame(callback);
266
+ return (callback) => {
267
+ const id = requestAnimationFrame(callback);
268
+ return { cancel: () => cancelAnimationFrame(id) };
269
+ };
256
270
  }
257
271
  return (callback) => {
258
272
  const id = setTimeout(callback, 0);
@@ -2,6 +2,9 @@ export function createEpisodeState() {
2
2
  return {
3
3
  step: 0,
4
4
  drivers: new Map(),
5
+ observationScratch: {},
6
+ metricScratch: {},
7
+ destroyedCarsById: new Map(),
5
8
  previousSnapshot: null,
6
9
  lastResult: null,
7
10
  };
@@ -30,7 +33,14 @@ export function resetDriverEpisodes(episodeState, driverIds = []) {
30
33
  }
31
34
 
32
35
  export function markDestroyedDriverEpisodes(episodeState, driverIds = [], snapshot = null) {
33
- const carsById = new Map((snapshot?.cars ?? []).map((car) => [car.id, car]));
36
+ const carsById = episodeState.destroyedCarsById ?? new Map();
37
+ episodeState.destroyedCarsById = carsById;
38
+ carsById.clear();
39
+ const cars = snapshot?.cars ?? [];
40
+ for (let index = 0; index < cars.length; index += 1) {
41
+ const car = cars[index];
42
+ carsById.set(car.id, car);
43
+ }
34
44
  driverIds.forEach((driverId) => {
35
45
  const car = carsById.get(driverId);
36
46
  if (!car?.destroyed && !car?.dnf && !car?.outOfRace) return;
@@ -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 {
@@ -478,7 +478,6 @@ export interface PaddockEnvironmentOptions {
478
478
  seed?: number;
479
479
  trackSeed?: number;
480
480
  trackGeneration?: PaddockProceduralTrackOptions;
481
- trackQueryIndex?: boolean;
482
481
  totalLaps?: number;
483
482
  frameSkip?: number;
484
483
  physicsMode?: PaddockPhysicsMode;
@@ -5,6 +5,21 @@ import { simUnitsToMeters } from '../simulation/units.js';
5
5
  const LEGAL_SURFACES = new Set(['track', 'kerb', 'pit-entry', 'pit-lane', 'pit-exit', 'pit-box']);
6
6
  const ILLEGAL_SURFACES = new Set(['grass', 'gravel', 'runoff', 'barrier']);
7
7
  const UNSTABLE_STATES = new Set(['spun', 'backwards', 'spin-risk', 'understeer', 'oversteer', 'destroyed']);
8
+ export const ENVIRONMENT_METRIC_FIELDS = Object.freeze([
9
+ 'progressDeltaMeters',
10
+ 'legalProgressDeltaMeters',
11
+ 'offTrack',
12
+ 'kerb',
13
+ 'fullyOutsideWhiteLine',
14
+ 'severeCut',
15
+ 'destroyed',
16
+ 'destroyReason',
17
+ 'under30kph',
18
+ 'spinOrBackwards',
19
+ 'completedLap',
20
+ 'lapTimeSeconds',
21
+ 'contactCount',
22
+ ]);
8
23
 
9
24
  export function isEnvironmentCarOffTrack(car) {
10
25
  if (!car) return false;
@@ -15,19 +30,32 @@ export function isEnvironmentContactEvent(event) {
15
30
  return ['collision', 'contact', 'car-contact'].includes(event?.type);
16
31
  }
17
32
 
18
- export function buildDriverMetrics({ snapshot, previousSnapshot = null, options, events = [] }) {
19
- const previousById = new Map((previousSnapshot?.cars ?? []).map((car) => [car.id, car]));
20
- const currentById = new Map(snapshot.cars.map((car) => [car.id, car]));
21
- const contactCounts = contactCountsByDriver(events);
33
+ export function buildDriverMetrics({ snapshot, previousSnapshot = null, options, events = [], scratch = null }) {
34
+ const previousById = carsByIdForMetrics(previousSnapshot, scratch, 'previousCarsById');
35
+ const currentById = carsByIdForMetrics(snapshot, scratch, 'currentCarsById');
36
+ const contactCounts = contactCountsByDriver(events, scratch);
22
37
  const metrics = {};
23
38
  options.controlledDrivers.forEach((driverId) => {
24
39
  const car = currentById.get(driverId);
25
40
  const previous = previousById.get(driverId);
26
- metrics[driverId] = car ? buildMetricForDriver(car, previous, snapshot, contactCounts.get(driverId) ?? 0) : emptyMetrics();
41
+ metrics[driverId] = car ? buildMetricForDriver(car, previous, snapshot, contactCounts?.get(driverId) ?? 0) : emptyMetrics();
27
42
  });
28
43
  return metrics;
29
44
  }
30
45
 
46
+ function carsByIdForMetrics(snapshot, scratch, key) {
47
+ if (!scratch) return new Map((snapshot?.cars ?? []).map((car) => [car.id, car]));
48
+ const carsById = scratch[key] ?? new Map();
49
+ scratch[key] = carsById;
50
+ carsById.clear();
51
+ const cars = snapshot?.cars ?? [];
52
+ for (let index = 0; index < cars.length; index += 1) {
53
+ const car = cars[index];
54
+ carsById.set(car.id, car);
55
+ }
56
+ return carsById;
57
+ }
58
+
31
59
  function buildMetricForDriver(car, previous, snapshot, contactCount) {
32
60
  const progressDeltaMeters = progressDelta(car, previous);
33
61
  const wheelState = classifyWheels(car);
@@ -140,20 +168,35 @@ function lastLapTimeSeconds(car) {
140
168
  return car.lapTelemetry?.lastLapTime ?? car.lapTelemetry?.bestLapTime ?? null;
141
169
  }
142
170
 
143
- function contactCountsByDriver(events) {
144
- const counts = new Map();
145
- if (!events.length) return counts;
171
+ function contactCountsByDriver(events, scratch = null) {
172
+ if (!events.length) return null;
173
+ const counts = scratch ? (scratch.contactCounts ?? new Map()) : new Map();
174
+ const eventDriverIds = scratch ? (scratch.eventDriverIds ?? []) : [];
175
+ if (scratch) {
176
+ scratch.contactCounts = counts;
177
+ scratch.eventDriverIds = eventDriverIds;
178
+ }
179
+ counts.clear();
146
180
  events.forEach((event) => {
147
181
  if (!isEnvironmentContactEvent(event)) return;
148
- const eventDriverIds = new Set([
149
- event.driverId,
150
- event.carId,
151
- event.otherCarId,
152
- ...(event.driverIds ?? []),
153
- ].filter(Boolean));
154
- eventDriverIds.forEach((driverId) => {
182
+ eventDriverIds.length = 0;
183
+ pushEventDriverId(eventDriverIds, event.driverId);
184
+ pushEventDriverId(eventDriverIds, event.carId);
185
+ pushEventDriverId(eventDriverIds, event.otherCarId);
186
+ const extraDriverIds = event.driverIds ?? [];
187
+ for (let index = 0; index < extraDriverIds.length; index += 1) {
188
+ pushEventDriverId(eventDriverIds, extraDriverIds[index]);
189
+ }
190
+ for (let index = 0; index < eventDriverIds.length; index += 1) {
191
+ const driverId = eventDriverIds[index];
155
192
  counts.set(driverId, (counts.get(driverId) ?? 0) + 1);
156
- });
193
+ }
157
194
  });
195
+ eventDriverIds.length = 0;
158
196
  return counts;
159
197
  }
198
+
199
+ function pushEventDriverId(target, driverId) {
200
+ if (!driverId || target.includes(driverId)) return;
201
+ target.push(driverId);
202
+ }
@@ -91,40 +91,36 @@ export function buildObservationVector(source, sensors, { includeSchema = true,
91
91
  );
92
92
  vector.push(sample.curvature ?? 0, (sample.headingDeltaRadians ?? 0) / Math.PI);
93
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
- }
94
+ if (typeof source.appendRayVectorValues === 'function') {
95
+ source.appendRayVectorValues(vector);
96
+ } else if (Array.isArray(source.rayVectorValues)) {
97
+ source.rayVectorValues.forEach((values, index) => {
98
+ pushRaySchema(schema, index, sensors);
99
+ vector.push(...values);
126
100
  });
127
- });
101
+ } else {
102
+ source.rays.forEach((ray, index) => {
103
+ pushRaySchema(schema, index, sensors);
104
+ vector.push(
105
+ ratio(ray.track.distanceMeters, ray.lengthMeters),
106
+ ray.track.hit ? 1 : 0,
107
+ ray.track.kind === 'exit' ? 1 : 0,
108
+ ray.track.kind === 'entry' ? 1 : 0,
109
+ ratio(ray.car.distanceMeters, ray.lengthMeters),
110
+ ray.car.hit ? 1 : 0,
111
+ ray.car.relativeSpeedKph / 200,
112
+ ray.car.targetType === 'replayGhost' ? 1 : 0,
113
+ );
114
+ MODEL_RAY_SURFACE_CHANNELS.forEach((channel) => {
115
+ if (sensors.rays.channels?.includes?.(channel)) {
116
+ vector.push(
117
+ ratio(ray[channel]?.distanceMeters ?? ray.lengthMeters, ray.lengthMeters),
118
+ ray[channel]?.hit ? 1 : 0,
119
+ );
120
+ }
121
+ });
122
+ });
123
+ }
128
124
  const nearbyLimit = sensors.nearbyCars.enabled ? (sensors.nearbyCars.maxCars ?? source.nearbyCars.length) : 0;
129
125
  const nearbyRadius = sensors.nearbyCars.radiusMeters ?? 150;
130
126
  for (let index = 0; index < nearbyLimit; index += 1) {
@@ -179,6 +175,27 @@ function pushSchema(schema, ...entries) {
179
175
  if (schema) schema.push(...entries);
180
176
  }
181
177
 
178
+ function pushRaySchema(schema, index, sensors) {
179
+ pushSchema(schema,
180
+ { name: `rays[${index}].track.distanceRatio`, scale: '0..1' },
181
+ { name: `rays[${index}].track.hit`, scale: 'boolean' },
182
+ { name: `rays[${index}].track.kindExit`, scale: 'boolean' },
183
+ { name: `rays[${index}].track.kindEntry`, scale: 'boolean' },
184
+ { name: `rays[${index}].car.distanceRatio`, scale: '0..1' },
185
+ { name: `rays[${index}].car.hit`, scale: 'boolean' },
186
+ { name: `rays[${index}].car.relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
187
+ { name: `rays[${index}].car.targetTypeReplayGhost`, scale: 'boolean' },
188
+ );
189
+ MODEL_RAY_SURFACE_CHANNELS.forEach((channel) => {
190
+ if (sensors.rays.channels?.includes?.(channel)) {
191
+ pushSchema(schema,
192
+ { name: `rays[${index}].${channel}.distanceRatio`, scale: '0..1' },
193
+ { name: `rays[${index}].${channel}.hit`, scale: 'boolean' },
194
+ );
195
+ }
196
+ });
197
+ }
198
+
182
199
  function normalizeLapProgress(source) {
183
200
  return ratio(source.self.lapProgressMeters, source.track.lengthMeters || 1);
184
201
  }