@inventure71/paddockjs 3.0.0 → 4.1.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 (97) hide show
  1. package/CHANGELOG.md +152 -1
  2. package/INSTALL_AND_UPDATE.md +21 -4
  3. package/README.md +103 -551
  4. package/docs/architecture.md +28 -18
  5. package/docs/component_inventory.md +5 -4
  6. package/docs/composable-layouts.md +79 -0
  7. package/docs/custom_model_controller.md +4 -4
  8. package/docs/data-helpers.md +50 -0
  9. package/docs/data_contract.md +13 -7
  10. package/docs/getting-started.md +105 -0
  11. package/docs/headless-environment.md +68 -0
  12. package/docs/index.md +17 -4
  13. package/docs/rules.md +6 -2
  14. package/docs/system_specs.md +60 -7
  15. package/docs/theming.md +64 -0
  16. package/docs/training.md +20 -14
  17. package/docs/troubleshooting.md +76 -0
  18. package/docs/upgrading-hosts-to-3.md +42 -0
  19. package/examples/python/README.md +6 -0
  20. package/examples/python/base_policy_server.py +19 -0
  21. package/package.json +21 -5
  22. package/src/api/PaddockSimulatorController.js +78 -22
  23. package/src/api/runtimeTheme.js +59 -0
  24. package/src/app/F1SimulatorApp.js +24 -3
  25. package/src/app/readouts/timingTowerRenderer.js +203 -42
  26. package/src/app/runtime/frameLoop.js +5 -1
  27. package/src/config/defaultOptions.js +8 -1
  28. package/src/config/themeOptions.js +5 -0
  29. package/src/data/index.d.ts +212 -0
  30. package/src/data/index.js +14 -0
  31. package/src/environment/episode.js +11 -1
  32. package/src/environment/metrics.js +59 -16
  33. package/src/environment/observationVector.js +50 -33
  34. package/src/environment/observations.js +89 -20
  35. package/src/environment/runtime.js +41 -0
  36. package/src/environment/sensors/carRays.js +48 -37
  37. package/src/environment/sensors/index.js +501 -97
  38. package/src/environment/sensors/indexedRayBands.js +134 -46
  39. package/src/environment/sensors/rayBandTrace.js +1171 -0
  40. package/src/environment/sensors/rayChannels.js +108 -0
  41. package/src/environment/sensors/rayConfig.js +71 -25
  42. package/src/environment/sensors/rayGeometry.js +23 -17
  43. package/src/environment/sensors/sensorTargets.js +67 -1
  44. package/src/environment/sensors/surfaceRays.js +130 -89
  45. package/src/environment/sensors/trackRays.js +115 -1
  46. package/src/index.d.ts +43 -0
  47. package/src/index.js +27 -2
  48. package/src/placeholder/index.d.ts +11 -0
  49. package/src/placeholder/index.js +61 -0
  50. package/src/placeholder.css +72 -0
  51. package/src/simulation/collisionGeometry.js +260 -96
  52. package/src/simulation/pit/pitFlow.js +21 -9
  53. package/src/simulation/pit/pitQueue.js +5 -3
  54. package/src/simulation/pit/pitRouteMovement.js +9 -4
  55. package/src/simulation/pit/pitRouting.js +77 -42
  56. package/src/simulation/pit/pitState.js +1 -0
  57. package/src/simulation/pit/pitTireService.js +7 -3
  58. package/src/simulation/race/raceFinish.js +25 -8
  59. package/src/simulation/race/raceOrder.js +189 -32
  60. package/src/simulation/race/raceProgress.js +129 -37
  61. package/src/simulation/race/raceSetup.js +2 -0
  62. package/src/simulation/race/raceStep.js +25 -13
  63. package/src/simulation/raceSimulation.js +20 -17
  64. package/src/simulation/rules/collisionSteward.js +40 -23
  65. package/src/simulation/rules/rulesReview.js +77 -56
  66. package/src/simulation/snapshots/raceSnapshots.js +37 -0
  67. package/src/simulation/timing/drsTiming.js +143 -30
  68. package/src/simulation/timing/gapEstimation.js +25 -3
  69. package/src/simulation/timing/lapTelemetry.js +115 -36
  70. package/src/simulation/timing/raceTiming.js +5 -1
  71. package/src/simulation/timing/sectorPerformance.js +104 -26
  72. package/src/simulation/timing/timingHistory.js +229 -14
  73. package/src/simulation/timing/timingLines.js +65 -7
  74. package/src/simulation/track/pitLaneLayout.js +23 -0
  75. package/src/simulation/track/pitLaneState.js +218 -19
  76. package/src/simulation/track/spatialQueries.js +67 -11
  77. package/src/simulation/track/trackMath.js +91 -24
  78. package/src/simulation/track/trackModel.js +2 -0
  79. package/src/simulation/track/trackQueryIndex.js +1334 -100
  80. package/src/simulation/track/trackQueryPitIndex.js +138 -14
  81. package/src/simulation/track/trackQueryScratch.js +7 -0
  82. package/src/simulation/track/trackQueryStats.js +25 -0
  83. package/src/simulation/track/trackSnapshotJson.js +139 -0
  84. package/src/simulation/track/trackStatePolicy.js +249 -1
  85. package/src/simulation/vehicle/contactResolution.js +96 -59
  86. package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
  87. package/src/simulation/vehicle/pitWheelSurface.js +81 -28
  88. package/src/simulation/vehicle/runoffResponse.js +54 -29
  89. package/src/simulation/vehicle/vehicleGeometry.js +245 -51
  90. package/src/simulation/vehicle/vehiclePhysics.js +42 -27
  91. package/src/simulation/vehicle/vehicleSnapshots.js +23 -0
  92. package/src/simulation/vehicle/vehicleState.js +6 -0
  93. package/src/simulation/vehicle/wheelSurface.js +807 -92
  94. package/src/styles.css +497 -497
  95. package/src/ui/raceControlsTemplate.js +1 -0
  96. package/src/ui/telemetryDrawerTemplate.js +1 -0
  97. package/src/ui/telemetryTemplates.js +1 -0
@@ -125,6 +125,195 @@ export function syncTimingGapModeControls({ readouts, buttons, timingGapMode })
125
125
  });
126
126
  }
127
127
 
128
+ function timingGapForCar(car, raceMode, timingGapMode) {
129
+ if (isDnfCar(car)) return 'DNF';
130
+ if (isWavedFlagCar(car) && raceMode !== 'finished') return 'WAVED';
131
+ if (raceMode === 'finished') return car.rank === 1 ? 'Winner' : 'FIN';
132
+ if (raceMode === 'safety-car' && car.rank > 1) return 'SC';
133
+ if (raceMode === 'pre-start') return car.rank === 1 ? 'Pole' : 'Grid';
134
+ if (car.rank > 1) return formatRaceGap(car, timingGapMode);
135
+ return 'Leader';
136
+ }
137
+
138
+ function timingRowModel({ car, driver, raceMode, selectedId, timingGapMode, penalty }) {
139
+ const dnf = isDnfCar(car);
140
+ const tire = car.tire ?? driver?.tire ?? 'M';
141
+ const timingCode = car.timingCode ?? driver?.timingCode ?? car.code;
142
+ const team = car.team ?? driver?.team ?? null;
143
+ const icon = team?.icon ?? car.icon ?? driver?.icon ?? timingCode;
144
+ const driverColor = formatCssColor(car.color);
145
+ const iconColor = formatCssColor(team?.color ?? car.color, driverColor);
146
+ const penaltyLabel = penalty ? formatPenaltyBadgeLabel(penalty) : '';
147
+
148
+ return {
149
+ id: car.id,
150
+ rank: car.rank,
151
+ name: car.name,
152
+ selected: car.id === selectedId,
153
+ dnf,
154
+ gap: timingGapForCar(car, raceMode, timingGapMode),
155
+ tire,
156
+ tireClass: getTireClass(tire),
157
+ timingCode,
158
+ icon,
159
+ driverColor,
160
+ iconColor,
161
+ penaltyLabel,
162
+ };
163
+ }
164
+
165
+ function timingRowKey(row) {
166
+ return [
167
+ row.id,
168
+ row.rank,
169
+ row.name,
170
+ row.selected ? 1 : 0,
171
+ row.dnf ? 1 : 0,
172
+ row.gap,
173
+ row.tire,
174
+ row.tireClass,
175
+ row.timingCode,
176
+ row.icon,
177
+ row.driverColor,
178
+ row.iconColor,
179
+ row.penaltyLabel,
180
+ ].join('\u001f');
181
+ }
182
+
183
+ function timingRowsKey(rows) {
184
+ return rows.map(timingRowKey).join('\u001e');
185
+ }
186
+
187
+ function renderTimingRowMarkup(row) {
188
+ const penaltyBadge = row.penaltyLabel
189
+ ? `<span class="timing-penalty-badge" aria-label="${escapeHtml(row.penaltyLabel)}" title="${escapeHtml(row.penaltyLabel)}">!</span>`
190
+ : '';
191
+
192
+ return `
193
+ <li>
194
+ <button class="timing-row ${row.selected ? 'is-selected' : ''} ${row.dnf ? 'is-dnf' : ''}" type="button"
195
+ data-driver-id="${escapeHtml(row.id)}" aria-label="Select ${escapeHtml(row.name)}"
196
+ style="--driver-color: ${escapeHtml(row.driverColor)}">
197
+ <span class="timing-position">${row.rank}</span>
198
+ <span class="timing-icon timing-team-icon" aria-hidden="true" style="--team-color: ${escapeHtml(row.iconColor)}">${escapeHtml(row.icon)}</span>
199
+ <span class="timing-name" title="${escapeHtml(row.name)}"><span>${escapeHtml(row.timingCode)}</span>${penaltyBadge}</span>
200
+ <span class="timing-gap">${escapeHtml(row.gap)}</span>
201
+ <span class="timing-tire timing-tire--${row.tireClass}">${escapeHtml(row.tire)}</span>
202
+ </button>
203
+ </li>
204
+ `;
205
+ }
206
+
207
+ function canPatchTimingRows(timingList) {
208
+ return Boolean(
209
+ timingList?.ownerDocument?.createElement &&
210
+ typeof timingList.appendChild === 'function' &&
211
+ timingList.children,
212
+ );
213
+ }
214
+
215
+ function createTimingRowView(document) {
216
+ const item = document.createElement('li');
217
+ const button = document.createElement('button');
218
+ const position = document.createElement('span');
219
+ const icon = document.createElement('span');
220
+ const name = document.createElement('span');
221
+ const code = document.createElement('span');
222
+ const gap = document.createElement('span');
223
+ const tire = document.createElement('span');
224
+
225
+ item.appendChild(button);
226
+ button.appendChild(position);
227
+ button.appendChild(icon);
228
+ button.appendChild(name);
229
+ name.appendChild(code);
230
+ button.appendChild(gap);
231
+ button.appendChild(tire);
232
+
233
+ return {
234
+ item,
235
+ button,
236
+ position,
237
+ icon,
238
+ name,
239
+ code,
240
+ gap,
241
+ tire,
242
+ penaltyBadge: null,
243
+ };
244
+ }
245
+
246
+ function setStyleProperty(element, name, value) {
247
+ if (typeof element.style?.setProperty === 'function') {
248
+ element.style.setProperty(name, value);
249
+ } else {
250
+ element.setAttribute?.('style', `${name}: ${value}`);
251
+ }
252
+ }
253
+
254
+ function updateTimingRowView(view, row) {
255
+ view.button.className = `timing-row ${row.selected ? 'is-selected' : ''} ${row.dnf ? 'is-dnf' : ''}`;
256
+ view.button.setAttribute('type', 'button');
257
+ view.button.setAttribute('data-driver-id', row.id);
258
+ view.button.setAttribute('aria-label', `Select ${row.name}`);
259
+ setStyleProperty(view.button, '--driver-color', row.driverColor);
260
+
261
+ view.position.className = 'timing-position';
262
+ view.position.textContent = String(row.rank);
263
+
264
+ view.icon.className = 'timing-icon timing-team-icon';
265
+ view.icon.setAttribute('aria-hidden', 'true');
266
+ setStyleProperty(view.icon, '--team-color', row.iconColor);
267
+ view.icon.textContent = String(row.icon ?? '');
268
+
269
+ view.name.className = 'timing-name';
270
+ view.name.setAttribute('title', row.name);
271
+ view.code.textContent = String(row.timingCode ?? '');
272
+
273
+ if (row.penaltyLabel) {
274
+ if (!view.penaltyBadge) {
275
+ view.penaltyBadge = view.name.ownerDocument.createElement('span');
276
+ view.name.appendChild(view.penaltyBadge);
277
+ }
278
+ view.penaltyBadge.className = 'timing-penalty-badge';
279
+ view.penaltyBadge.setAttribute('aria-label', row.penaltyLabel);
280
+ view.penaltyBadge.setAttribute('title', row.penaltyLabel);
281
+ view.penaltyBadge.textContent = '!';
282
+ } else if (view.penaltyBadge) {
283
+ view.penaltyBadge.remove();
284
+ view.penaltyBadge = null;
285
+ }
286
+
287
+ view.gap.className = 'timing-gap';
288
+ view.gap.textContent = String(row.gap);
289
+
290
+ view.tire.className = `timing-tire timing-tire--${row.tireClass}`;
291
+ view.tire.textContent = String(row.tire ?? '');
292
+ }
293
+
294
+ function patchTimingRows(timingList, rows) {
295
+ const rowViews = timingList.__paddockTimingRowViews ?? new Map();
296
+ timingList.__paddockTimingRowViews = rowViews;
297
+ const seen = new Set();
298
+
299
+ rows.forEach((row) => {
300
+ let view = rowViews.get(row.id);
301
+ if (!view) {
302
+ view = createTimingRowView(timingList.ownerDocument);
303
+ rowViews.set(row.id, view);
304
+ }
305
+ seen.add(row.id);
306
+ updateTimingRowView(view, row);
307
+ timingList.appendChild(view.item);
308
+ });
309
+
310
+ rowViews.forEach((view, id) => {
311
+ if (seen.has(id)) return;
312
+ view.item.remove();
313
+ rowViews.delete(id);
314
+ });
315
+ }
316
+
128
317
  export function renderTimingTower({
129
318
  timingList,
130
319
  cars,
@@ -138,48 +327,20 @@ export function renderTimingTower({
138
327
  }) {
139
328
  if (!timingList) return lastTimingMarkup;
140
329
  const penaltyByDriver = timingPenaltyBadgesEnabled ? getPenaltyByDriver(penalties) : new Map();
141
- const timingMarkup = cars.map((car) => {
142
- const driver = driverById.get(car.id);
143
- const dnf = isDnfCar(car);
144
- let gap = 'Leader';
145
- if (dnf) {
146
- gap = 'DNF';
147
- } else if (isWavedFlagCar(car) && raceMode !== 'finished') {
148
- gap = 'WAVED';
149
- } else if (raceMode === 'finished') {
150
- gap = car.rank === 1 ? 'Winner' : 'FIN';
151
- } else if (raceMode === 'safety-car' && car.rank > 1) {
152
- gap = 'SC';
153
- } else if (raceMode === 'pre-start') {
154
- gap = car.rank === 1 ? 'Pole' : 'Grid';
155
- } else if (car.rank > 1) {
156
- gap = formatRaceGap(car, timingGapMode);
157
- }
158
- const tire = car.tire ?? driver?.tire ?? 'M';
159
- const timingCode = car.timingCode ?? driver?.timingCode ?? car.code;
160
- const team = car.team ?? driver?.team ?? null;
161
- const icon = team?.icon ?? car.icon ?? driver?.icon ?? timingCode;
162
- const driverColor = formatCssColor(car.color);
163
- const iconColor = formatCssColor(team?.color ?? car.color, driverColor);
164
- const penalty = penaltyByDriver.get(car.id);
165
- const penaltyBadge = penalty
166
- ? `<span class="timing-penalty-badge" aria-label="${escapeHtml(formatPenaltyBadgeLabel(penalty))}" title="${escapeHtml(formatPenaltyBadgeLabel(penalty))}">!</span>`
167
- : '';
168
-
169
- return `
170
- <li>
171
- <button class="timing-row ${car.id === selectedId ? 'is-selected' : ''} ${dnf ? 'is-dnf' : ''}" type="button"
172
- data-driver-id="${escapeHtml(car.id)}" aria-label="Select ${escapeHtml(car.name)}"
173
- style="--driver-color: ${escapeHtml(driverColor)}">
174
- <span class="timing-position">${car.rank}</span>
175
- <span class="timing-icon timing-team-icon" aria-hidden="true" style="--team-color: ${escapeHtml(iconColor)}">${escapeHtml(icon)}</span>
176
- <span class="timing-name" title="${escapeHtml(car.name)}"><span>${escapeHtml(timingCode)}</span>${penaltyBadge}</span>
177
- <span class="timing-gap">${escapeHtml(gap)}</span>
178
- <span class="timing-tire timing-tire--${getTireClass(tire)}">${escapeHtml(tire)}</span>
179
- </button>
180
- </li>
181
- `;
182
- }).join('');
330
+ const rows = cars.map((car) => timingRowModel({
331
+ car,
332
+ driver: driverById.get(car.id),
333
+ raceMode,
334
+ selectedId,
335
+ timingGapMode,
336
+ penalty: penaltyByDriver.get(car.id),
337
+ }));
338
+ if (canPatchTimingRows(timingList)) {
339
+ patchTimingRows(timingList, rows);
340
+ return timingRowsKey(rows);
341
+ }
342
+
343
+ const timingMarkup = rows.map(renderTimingRowMarkup).join('');
183
344
  if (timingMarkup !== lastTimingMarkup || timingList.innerHTML !== timingMarkup) {
184
345
  timingList.innerHTML = timingMarkup;
185
346
  return timingMarkup;
@@ -48,7 +48,11 @@ export function runFrameLoopTick(app) {
48
48
  const shouldUpdateDom = now - app.lastDomUpdateTime >= domUpdateIntervalForSpeed(app.simulationSpeed);
49
49
  const fullSnapshot = stepEvents.length > 0 || shouldUpdateDom ? app.sim.snapshot() : null;
50
50
  if (stepEvents.length > 0) app.emitRaceEvents(stepEvents, fullSnapshot);
51
- const renderSource = fullSnapshot ?? app.sim.snapshotRender?.() ?? app.sim.snapshot();
51
+ const renderSource = fullSnapshot ??
52
+ app.sim.snapshotRenderInto?.(app.renderSourceSnapshotBuffer ?? {}) ??
53
+ app.sim.snapshotRender?.() ??
54
+ app.sim.snapshot();
55
+ if (!fullSnapshot && app.sim.snapshotRenderInto) app.renderSourceSnapshotBuffer = renderSource;
52
56
  const renderSnapshot = interpolateRenderSnapshotInto(
53
57
  app.renderSnapshotBuffer ?? {},
54
58
  renderSource,
@@ -119,7 +119,14 @@ export const PADDOCK_SIMULATOR_PRESETS = {
119
119
  },
120
120
  };
121
121
 
122
- export { PADDOCK_THEME_CSS_VARIABLES, applyPaddockThemeCssVariables } from './themeOptions.js';
122
+ export {
123
+ DEFAULT_PADDOCK_THEME,
124
+ PADDOCK_THEME_CSS_VARIABLES,
125
+ PADDOCK_THEME_TOKEN_KEYS,
126
+ applyPaddockTheme,
127
+ applyPaddockThemeCssVariables,
128
+ resolvePaddockTheme,
129
+ } from './themeOptions.js';
123
130
 
124
131
  const SUPPORTED_CAMERA_MODES = new Set(['overview', 'leader', 'selected', 'driver', 'show-all', 'pit']);
125
132
 
@@ -34,6 +34,7 @@ export const THEME_SIZE_TOKEN_KEYS = [
34
34
  ];
35
35
 
36
36
  const THEME_TOKEN_KEYS = [...THEME_COLOR_TOKEN_KEYS, ...THEME_SIZE_TOKEN_KEYS];
37
+ export const PADDOCK_THEME_TOKEN_KEYS = THEME_TOKEN_KEYS;
37
38
  const THEME_TOKEN_SET = new Set(THEME_TOKEN_KEYS);
38
39
  const COLOR_TOKEN_SET = new Set(THEME_COLOR_TOKEN_KEYS);
39
40
  const SIZE_TOKEN_SET = new Set(THEME_SIZE_TOKEN_KEYS);
@@ -248,6 +249,7 @@ export const DEFAULT_PADDOCK_THEME_INPUT = {
248
249
  mode: 'dark',
249
250
  use: 'default',
250
251
  };
252
+ export const DEFAULT_PADDOCK_THEME = DEFAULT_PADDOCK_THEME_INPUT;
251
253
 
252
254
  const DEFAULT_THEME_PACKAGE = {
253
255
  tokens: {
@@ -346,6 +348,9 @@ export function applyPaddockThemeCssVariables(root, theme = DEFAULT_PADDOCK_THEM
346
348
  });
347
349
  }
348
350
 
351
+ export const resolvePaddockTheme = normalizePaddockTheme;
352
+ export const applyPaddockTheme = applyPaddockThemeCssVariables;
353
+
349
354
  export function normalizeCssColorToken(value, fallback = null) {
350
355
  if (typeof value !== 'string') return fallback;
351
356
  const candidate = value.trim();
@@ -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';
@@ -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;
@@ -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
+ }