@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
@@ -6,7 +6,7 @@ import {
6
6
  distanceToNextLimiterSegment,
7
7
  nearestDistanceOnRoute,
8
8
  routeLimiterActiveAt,
9
- sampleRoute,
9
+ sampleRouteInto,
10
10
  } from './pitRouting.js';
11
11
  import { syncPitCarKinematics } from './pitKinematics.js';
12
12
  import { completePitPenaltyService } from './pitPenaltyService.js';
@@ -26,6 +26,11 @@ import {
26
26
  pointDistance,
27
27
  } from './pitServiceConstants.js';
28
28
 
29
+ function samplePitStopRoute(stop, route, distanceAlong) {
30
+ const target = stop.routeSample ?? (stop.routeSample = { x: 0, y: 0, heading: 0, limiterActive: false });
31
+ return sampleRouteInto(target, route, distanceAlong);
32
+ }
33
+
29
34
  export function applyPitRoutePosition(sim, car, delta) {
30
35
  const stop = car.pitStop;
31
36
  if (!stop?.route) return false;
@@ -33,7 +38,7 @@ export function applyPitRoutePosition(sim, car, delta) {
33
38
  const route = stop.route;
34
39
  if (stop.phase === 'queue-release') {
35
40
  const nextProgress = Math.min(route.length, (stop.routeProgress ?? 0) + PIT_QUEUE_RELEASE_SPEED * delta);
36
- const point = sampleRoute(route, nextProgress);
41
+ const point = samplePitStopRoute(stop, route, nextProgress);
37
42
  if (!point) return false;
38
43
  const previousHeading = car.heading;
39
44
  car.previousX = car.x;
@@ -63,7 +68,7 @@ export function applyPitRoutePosition(sim, car, delta) {
63
68
  return route.length - stop.routeProgress <= PIT_QUEUE_RELEASE_FINISH_DISTANCE;
64
69
  }
65
70
  stop.routeProgress = clamp(nearestDistanceOnRoute(route, car, stop.routeProgress ?? 0), 0, route.length);
66
- const routeEnd = route.points.at(-1);
71
+ const routeEnd = route.points[route.points.length - 1];
67
72
  const limiterActive = routeLimiterActiveAt(route, stop.routeProgress);
68
73
  let targetSpeed = limiterActive
69
74
  ? Math.min(speedLimit, VEHICLE_LIMITS.maxSpeed)
@@ -93,7 +98,7 @@ export function applyPitRoutePosition(sim, car, delta) {
93
98
  }
94
99
 
95
100
  const nextProgress = Math.min(route.length, stop.routeProgress + Math.max(0, targetSpeed) * delta);
96
- const point = sampleRoute(route, nextProgress);
101
+ const point = samplePitStopRoute(stop, route, nextProgress);
97
102
  if (!point) return false;
98
103
  const previousSpeed = car.speed;
99
104
  const previousHeading = car.heading;
@@ -128,18 +128,17 @@ export function pointDistance(first, second) {
128
128
  }
129
129
 
130
130
  export function createRoute(points) {
131
- const routePoints = points
132
- .filter(Boolean)
133
- .map((point) => routePoint(point))
134
- .filter(Boolean)
135
- .reduce((deduped, point) => {
136
- if (!sameRoutePoint(deduped.at(-1), point)) {
137
- deduped.push(point);
138
- } else if (point.limiterActive) {
139
- deduped.at(-1).limiterActive = true;
140
- }
141
- return deduped;
142
- }, []);
131
+ const routePoints = [];
132
+ for (let index = 0; index < points.length; index += 1) {
133
+ const point = routePoint(points[index]);
134
+ if (!point) continue;
135
+ const previous = routePoints.length > 0 ? routePoints[routePoints.length - 1] : null;
136
+ if (!sameRoutePoint(previous, point)) {
137
+ routePoints.push(point);
138
+ } else if (point.limiterActive) {
139
+ previous.limiterActive = true;
140
+ }
141
+ }
143
142
  const segments = [];
144
143
  let totalLength = 0;
145
144
 
@@ -176,6 +175,12 @@ export function createRoute(points) {
176
175
  };
177
176
  }
178
177
 
178
+ function recordRouteBenchmarkStat(route, key, amount = 1) {
179
+ const stats = route?.runtimeBenchmarkStats;
180
+ if (!stats) return;
181
+ stats[key] = (stats[key] ?? 0) + amount;
182
+ }
183
+
179
184
  function findRouteSegment(route, distanceAlong, endPadding = 0) {
180
185
  if (!route?.segments?.length) return null;
181
186
  const clampedDistance = clamp(distanceAlong, 0, route.length);
@@ -205,35 +210,62 @@ function findRouteSegment(route, distanceAlong, endPadding = 0) {
205
210
  clampedDistance < segment.startDistance - 0.001 ||
206
211
  clampedDistance > segment.endDistance - endPadding
207
212
  ) {
208
- segment = segments.find((candidate) => (
209
- clampedDistance >= candidate.startDistance - 0.001 &&
210
- clampedDistance <= candidate.endDistance - endPadding
211
- )) ?? segments.at(-1);
212
- index = segments.indexOf(segment);
213
+ recordRouteBenchmarkStat(route, 'segmentFallbackScans');
214
+ segment = segments[segments.length - 1];
215
+ index = segments.length - 1;
216
+ for (let candidateIndex = 0; candidateIndex < segments.length; candidateIndex += 1) {
217
+ const candidate = segments[candidateIndex];
218
+ if (
219
+ clampedDistance >= candidate.startDistance - 0.001 &&
220
+ clampedDistance <= candidate.endDistance - endPadding
221
+ ) {
222
+ segment = candidate;
223
+ index = candidateIndex;
224
+ break;
225
+ }
226
+ }
213
227
  }
214
228
 
215
229
  route.segmentCursor = Math.max(0, index);
216
230
  return segment;
217
231
  }
218
232
 
219
- export function sampleRoute(route, distanceAlong) {
220
- if (!route?.segments?.length) return route?.points?.[0] ?? null;
233
+ function writeRouteSample(target, route, distanceAlong, statKey) {
234
+ if (!route?.segments?.length) {
235
+ const point = route?.points?.[0] ?? null;
236
+ if (!point || !target) return point;
237
+ target.x = point.x;
238
+ target.y = point.y;
239
+ target.heading = point.heading;
240
+ target.limiterActive = Boolean(point.limiterActive);
241
+ return target;
242
+ }
243
+ const output = target ?? {};
221
244
  const clampedDistance = clamp(distanceAlong, 0, route.length);
222
- const segment = findRouteSegment(route, clampedDistance) ?? route.segments.at(-1);
245
+ const segment = findRouteSegment(route, clampedDistance) ?? route.segments[route.segments.length - 1];
223
246
  const amount = clamp((clampedDistance - segment.startDistance) / segment.length, 0, 1);
224
247
 
225
- return {
226
- x: segment.start.x + (segment.end.x - segment.start.x) * amount,
227
- y: segment.start.y + (segment.end.y - segment.start.y) * amount,
228
- heading: segment.heading,
229
- limiterActive: segment.limiterActive,
230
- };
248
+ output.x = segment.start.x + (segment.end.x - segment.start.x) * amount;
249
+ output.y = segment.start.y + (segment.end.y - segment.start.y) * amount;
250
+ output.heading = segment.heading;
251
+ output.limiterActive = segment.limiterActive;
252
+ recordRouteBenchmarkStat(route, statKey);
253
+ return output;
254
+ }
255
+
256
+ export function sampleRouteInto(target, route, distanceAlong) {
257
+ return writeRouteSample(target, route, distanceAlong, 'sampleRouteIntoCalls');
258
+ }
259
+
260
+ export function sampleRoute(route, distanceAlong) {
261
+ if (!route?.segments?.length) return route?.points?.[0] ?? null;
262
+ return writeRouteSample({}, route, distanceAlong, 'sampleRouteAllocations');
231
263
  }
232
264
 
233
265
  export function routeLimiterActiveAt(route, distanceAlong) {
234
266
  if (!route?.segments?.length) return false;
235
267
  const clampedDistance = clamp(distanceAlong, 0, route.length);
236
- const segment = findRouteSegment(route, clampedDistance, 0.001) ?? route.segments.at(-1);
268
+ const segment = findRouteSegment(route, clampedDistance, 0.001) ?? route.segments[route.segments.length - 1];
237
269
  return Boolean(segment?.limiterActive);
238
270
  }
239
271
 
@@ -251,7 +283,7 @@ function easeInOut(value) {
251
283
  return amount * amount * (3 - 2 * amount);
252
284
  }
253
285
 
254
- function projectPositionToSegment(position, segment) {
286
+ function projectPositionToSegmentInto(target, position, segment) {
255
287
  const dx = segment.end.x - segment.start.x;
256
288
  const dy = segment.end.y - segment.start.y;
257
289
  const lengthSquared = dx * dx + dy * dy;
@@ -260,26 +292,29 @@ function projectPositionToSegment(position, segment) {
260
292
  : 0;
261
293
  const x = segment.start.x + dx * amount;
262
294
  const y = segment.start.y + dy * amount;
263
- const distanceSquared = (position.x - x) ** 2 + (position.y - y) ** 2;
264
-
265
- return {
266
- distanceAlong: segment.startDistance + segment.length * amount,
267
- distanceSquared,
268
- };
295
+ target.distanceAlong = segment.startDistance + segment.length * amount;
296
+ target.distanceSquared = (position.x - x) ** 2 + (position.y - y) ** 2;
297
+ return target;
269
298
  }
270
299
 
271
300
  export function nearestDistanceOnRoute(route, position, previousDistance = 0) {
272
301
  if (!route?.segments?.length) return 0;
273
- let best = null;
302
+ let bestDistanceAlong = previousDistance;
303
+ let bestDistanceSquared = Infinity;
274
304
  const backtrackAllowance = metersToSimUnits(90);
305
+ const projection = route._projectionScratch ?? (route._projectionScratch = { distanceAlong: 0, distanceSquared: 0 });
306
+
307
+ for (let index = 0; index < route.segments.length; index += 1) {
308
+ const segment = route.segments[index];
309
+ if (segment.endDistance < previousDistance - backtrackAllowance) continue;
310
+ projectPositionToSegmentInto(projection, position, segment);
311
+ if (projection.distanceSquared < bestDistanceSquared) {
312
+ bestDistanceSquared = projection.distanceSquared;
313
+ bestDistanceAlong = projection.distanceAlong;
314
+ }
315
+ }
275
316
 
276
- route.segments.forEach((segment) => {
277
- if (segment.endDistance < previousDistance - backtrackAllowance) return;
278
- const projected = projectPositionToSegment(position, segment);
279
- if (!best || projected.distanceSquared < best.distanceSquared) best = projected;
280
- });
281
-
282
- return clamp(best?.distanceAlong ?? previousDistance, 0, route.length);
317
+ return clamp(bestDistanceAlong, 0, route.length);
283
318
  }
284
319
 
285
320
  export function createPitApproachPoints(track, car, pitLane, entryRaceDistance) {
@@ -103,6 +103,7 @@ export function initializePitStops({ cars, pitLane, pitStops, totalLaps, trackLe
103
103
  serviceProfile: null,
104
104
  queueingForService: false,
105
105
  route: null,
106
+ routeSample: null,
106
107
  routeProgress: 0,
107
108
  routeStartRaceDistance: null,
108
109
  routeEndRaceDistance: null,
@@ -51,13 +51,17 @@ export function completePitService(sim, car) {
51
51
 
52
52
  const driveLaneOffset = pitDriveLaneOffset(pitLane);
53
53
  const boxReleaseDistance = Math.min(pitLane.mainLane.length, box.distanceAlongLane + PIT_BOX_APPROACH_DISTANCE);
54
- const route = createRoute([
54
+ const routePoints = [
55
55
  routePoint(box.center, pitLane.mainLane.heading, { limiterActive: true }),
56
56
  routePoint(box.laneTarget, pitLane.mainLane.heading, { limiterActive: true }),
57
57
  routePoint(pitMainLanePointAt(pitLane, boxReleaseDistance, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
58
58
  routePoint(pitMainLanePointAt(pitLane, pitLane.mainLane.length, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
59
- ...(pitLane.exit.roadCenterline ?? []).map((point) => routePoint(point)),
60
- ]);
59
+ ];
60
+ const exitCenterline = pitLane.exit.roadCenterline ?? [];
61
+ for (let index = 0; index < exitCenterline.length; index += 1) {
62
+ routePoints.push(routePoint(exitCenterline[index]));
63
+ }
64
+ const route = createRoute(routePoints);
61
65
  stop.status = 'exiting';
62
66
  stop.phase = 'exit';
63
67
  stop.route = route;
@@ -1,13 +1,22 @@
1
1
  import { isRaceDnf } from './retirements.js';
2
2
 
3
- export function evaluateRaceFinishForSimulation(sim) {
3
+ export function evaluateRaceFinishForSimulation(sim, orderedCars = sim.orderedCars()) {
4
4
  if (sim.raceControl.finished) return;
5
5
  if (sim.raceControl.mode === 'pre-start') return;
6
6
 
7
- const ordered = sim.orderedCars();
8
- const newlyFinished = ordered.filter((car) => !isRaceDnf(car) && !car.finished && car.raceDistance >= sim.finishDistance);
7
+ const ordered = orderedCars;
8
+ if (!ordered.length) return;
9
+
10
+ let allFinishedOrDnf = true;
11
+ let newlyFinishedCount = 0;
12
+ for (let index = 0; index < ordered.length; index += 1) {
13
+ const car = ordered[index];
14
+ if (isRaceDnf(car) || car.finished) continue;
15
+ if (car.raceDistance < sim.finishDistance) {
16
+ allFinishedOrDnf = false;
17
+ continue;
18
+ }
9
19
 
10
- newlyFinished.forEach((car) => {
11
20
  car.finished = true;
12
21
  car.finishTime = sim.time;
13
22
  car.finishRank = sim.raceControl.finishOrder.length + 1;
@@ -27,9 +36,17 @@ export function evaluateRaceFinishForSimulation(sim) {
27
36
  winnerId: sim.raceControl.winnerId,
28
37
  });
29
38
  sim.reviewTireRequirement(car);
30
- });
39
+ newlyFinishedCount += 1;
40
+ }
31
41
 
32
- if (!ordered.length || !ordered.every((car) => car.finished || isRaceDnf(car))) return;
42
+ if (!allFinishedOrDnf) {
43
+ if (!newlyFinishedCount && sim.runtimeBenchmarkStats) {
44
+ sim.runtimeBenchmarkStats.finishEvalNoFinishFastPathCalls = (
45
+ sim.runtimeBenchmarkStats.finishEvalNoFinishFastPathCalls ?? 0
46
+ ) + 1;
47
+ }
48
+ return;
49
+ }
33
50
 
34
51
  sim.applyOutstandingServicePenalties();
35
52
  const classification = sim.buildClassificationFromFinishOrder();
@@ -41,8 +58,8 @@ export function evaluateRaceFinishForSimulation(sim) {
41
58
  sim.raceControl.classification = classification;
42
59
  sim.raceControl.frozenOrder = classification.map((entry) => entry.id);
43
60
  sim.safetyCar.deployed = true;
44
- const leader = sim.cars.find((car) => car.id === sim.raceControl.winnerId) ?? ordered[0];
45
- const safetyCarProgress = (leader?.raceDistance ?? 0) + sim.rules.safetyCarLeadDistance;
61
+ const safetyCarLeader = sim.cars.find((car) => car.id === sim.raceControl.winnerId) ?? ordered[0];
62
+ const safetyCarProgress = (safetyCarLeader?.raceDistance ?? 0) + sim.rules.safetyCarLeadDistance;
46
63
  if (sim.safetyCar.progress < safetyCarProgress) {
47
64
  sim.moveSafetyCarTo(safetyCarProgress);
48
65
  }
@@ -1,45 +1,194 @@
1
1
  import { clamp } from '../simMath.js';
2
2
  import { affectsRaceOrder } from '../participants/participantInteractions.js';
3
3
  import { distanceForward } from './raceDistance.js';
4
- import { isRaceDnf, raceDnfCars } from './retirements.js';
4
+ import { compareDnfOrder, isRaceDnf } from './retirements.js';
5
5
 
6
6
  function canAppearInRaceOrder(car) {
7
7
  return car?.interaction?.affectsRaceOrder !== false;
8
8
  }
9
9
 
10
- export function orderedCarsForSimulation(sim) {
11
- const raceCars = raceOrderCarsForSimulation(sim);
12
- const dnfCars = raceDnfCars(sim.cars.filter(canAppearInRaceOrder));
13
- const sortLive = (cars) => [...cars].sort((a, b) => {
10
+ function partitionRaceOrderParticipants(cars) {
11
+ const live = [];
12
+ const dnf = [];
13
+ for (let index = 0; index < cars.length; index += 1) {
14
+ const car = cars[index];
15
+ if (!canAppearInRaceOrder(car)) continue;
16
+ if (isRaceDnf(car)) dnf.push(car);
17
+ else live.push(car);
18
+ }
19
+ dnf.sort(compareDnfOrder);
20
+ return { live, dnf };
21
+ }
22
+
23
+ function sortLiveRaceOrderInPlace(cars) {
24
+ cars.sort((a, b) => {
14
25
  const delta = b.raceDistance - a.raceDistance;
15
26
  return delta === 0 ? a.index - b.index : delta;
16
27
  });
17
- const byId = new Map(sim.cars.map((car) => [car.id, car]));
28
+ return cars;
29
+ }
30
+
31
+ function appendCars(target, source) {
32
+ for (let index = 0; index < source.length; index += 1) {
33
+ target.push(source[index]);
34
+ }
35
+ return target;
36
+ }
37
+
38
+ function collectLiveRaceOrderedCars(orderedCars, scratch) {
39
+ const physicalCars = scratch?.physicalCars ?? [];
40
+ physicalCars.length = 0;
41
+ for (let index = 0; index < orderedCars.length; index += 1) {
42
+ const car = orderedCars[index];
43
+ if (!affectsRaceOrder(car) || isRaceDnf(car)) continue;
44
+ physicalCars.push(car);
45
+ }
46
+ if (scratch) scratch.physicalCars = physicalCars;
47
+ return physicalCars;
48
+ }
49
+
50
+ function sortPhysicalCarsInTrackOrder(sim, physicalCars) {
51
+ physicalCars.sort((a, b) => {
52
+ const progressDelta = (b.progress ?? b.raceDistance ?? 0) - (a.progress ?? a.raceDistance ?? 0);
53
+ if (progressDelta !== 0) return progressDelta;
54
+ const raceDistanceDelta = (b.raceDistance ?? 0) - (a.raceDistance ?? 0);
55
+ return raceDistanceDelta === 0 ? a.index - b.index : raceDistanceDelta;
56
+ });
57
+ return physicalCars;
58
+ }
59
+
60
+ function canUseRaceOrderAsPhysicalOrder(sim, physicalCars) {
61
+ if (physicalCars.length < 2) return true;
62
+ const leaderDistance = physicalCars[0]?.raceDistance;
63
+ const tailDistance = physicalCars[physicalCars.length - 1]?.raceDistance;
64
+ return Number.isFinite(leaderDistance) &&
65
+ Number.isFinite(tailDistance) &&
66
+ leaderDistance - tailDistance < sim.track.length - 1e-6;
67
+ }
68
+
69
+ function buildFullFieldDrsReferenceArrayForSimulation(sim, orderedCars, references) {
70
+ if (orderedCars.length !== sim.cars.length || !orderedCars.length) return false;
71
+ let wrappedEligibleCar = null;
72
+ for (let index = orderedCars.length - 1; index >= 0; index -= 1) {
73
+ const car = orderedCars[index];
74
+ if (!affectsRaceOrder(car) || isRaceDnf(car)) return false;
75
+ if (!car.finished && !sim.isCarInActivePitStop(car) && wrappedEligibleCar == null) {
76
+ wrappedEligibleCar = car;
77
+ }
78
+ }
79
+ if (!canUseRaceOrderAsPhysicalOrder(sim, orderedCars)) return false;
80
+
81
+ const trackLength = sim.track.length;
82
+ let previousEligibleCar = wrappedEligibleCar;
83
+ for (let index = 0; index < orderedCars.length; index += 1) {
84
+ const car = orderedCars[index];
85
+ const candidate = previousEligibleCar === car ? null : previousEligibleCar;
86
+ if (candidate) {
87
+ const delta = distanceForward(
88
+ car.progress ?? car.raceDistance ?? 0,
89
+ candidate.progress ?? candidate.raceDistance ?? 0,
90
+ trackLength,
91
+ );
92
+ references[car.index] = delta > 0 && delta <= trackLength / 2 ? candidate : null;
93
+ } else {
94
+ references[car.index] = null;
95
+ }
96
+ if (!car.finished && !sim.isCarInActivePitStop(car)) previousEligibleCar = car;
97
+ }
98
+ return true;
99
+ }
100
+
101
+ function buildDrsReferenceArrayForSimulation(sim, orderedCars = orderedCarsForSimulation(sim)) {
102
+ const scratch = sim._drsReferenceScratch ??= {};
103
+ const references = scratch.references ?? [];
104
+ references.length = sim.cars.length;
105
+ if (buildFullFieldDrsReferenceArrayForSimulation(sim, orderedCars, references)) {
106
+ scratch.references = references;
107
+ if (sim?.runtimeBenchmarkStats) {
108
+ sim.runtimeBenchmarkStats.drsReferenceScratchBuilds = (sim.runtimeBenchmarkStats.drsReferenceScratchBuilds ?? 0) + 1;
109
+ sim.runtimeBenchmarkStats.drsReferenceFullFieldFastPathCalls = (sim.runtimeBenchmarkStats.drsReferenceFullFieldFastPathCalls ?? 0) + 1;
110
+ sim.runtimeBenchmarkStats.drsReferenceOrderedFastPathCalls = (sim.runtimeBenchmarkStats.drsReferenceOrderedFastPathCalls ?? 0) + 1;
111
+ }
112
+ return references;
113
+ }
114
+ const physicalCars = collectLiveRaceOrderedCars(orderedCars, scratch);
115
+ for (let index = 0; index < references.length; index += 1) {
116
+ references[index] = null;
117
+ }
118
+ if (!physicalCars.length) return references;
119
+ if (canUseRaceOrderAsPhysicalOrder(sim, physicalCars)) {
120
+ if (sim?.runtimeBenchmarkStats) {
121
+ sim.runtimeBenchmarkStats.drsReferenceOrderedFastPathCalls = (sim.runtimeBenchmarkStats.drsReferenceOrderedFastPathCalls ?? 0) + 1;
122
+ }
123
+ } else {
124
+ sortPhysicalCarsInTrackOrder(sim, physicalCars);
125
+ }
126
+
127
+ const trackLength = sim.track.length;
128
+ let wrappedEligibleCar = null;
129
+ for (let index = physicalCars.length - 1; index >= 0; index -= 1) {
130
+ const car = physicalCars[index];
131
+ if (!car.finished && !sim.isCarInActivePitStop(car)) {
132
+ wrappedEligibleCar = car;
133
+ break;
134
+ }
135
+ }
136
+
137
+ let previousEligibleCar = wrappedEligibleCar;
138
+ for (let index = 0; index < physicalCars.length; index += 1) {
139
+ const car = physicalCars[index];
140
+ const candidate = previousEligibleCar === car ? null : previousEligibleCar;
141
+ if (candidate) {
142
+ const delta = distanceForward(
143
+ car.progress ?? car.raceDistance ?? 0,
144
+ candidate.progress ?? candidate.raceDistance ?? 0,
145
+ trackLength,
146
+ );
147
+ references[car.index] = delta > 0 && delta <= trackLength / 2 ? candidate : null;
148
+ } else {
149
+ references[car.index] = null;
150
+ }
151
+ if (!car.finished && !sim.isCarInActivePitStop(car)) previousEligibleCar = car;
152
+ }
153
+ scratch.references = references;
154
+ if (sim?.runtimeBenchmarkStats) {
155
+ sim.runtimeBenchmarkStats.drsReferenceScratchBuilds = (sim.runtimeBenchmarkStats.drsReferenceScratchBuilds ?? 0) + 1;
156
+ }
157
+ return references;
158
+ }
159
+
160
+ export function orderedCarsForSimulation(sim) {
161
+ const participants = partitionRaceOrderParticipants(sim.cars);
162
+ const raceCars = participants.live;
163
+ const dnfCars = participants.dnf;
18
164
 
19
165
  if (sim.raceControl.finished && sim.raceControl.classification?.length) {
166
+ const byId = new Map(sim.cars.map((car) => [car.id, car]));
20
167
  const classified = sim.raceControl.classification.map((entry) => byId.get(entry.id)).filter(Boolean);
21
168
  const classifiedIds = new Set(classified.map((car) => car.id));
22
169
  return [
23
170
  ...classified,
24
- ...sortLive(raceCars.filter((car) => !classifiedIds.has(car.id))),
171
+ ...sortLiveRaceOrderInPlace(raceCars.filter((car) => !classifiedIds.has(car.id))),
25
172
  ...dnfCars.filter((car) => !classifiedIds.has(car.id)),
26
173
  ];
27
174
  }
28
175
 
29
176
  if (sim.raceControl.finishOrder?.length) {
177
+ const byId = new Map(sim.cars.map((car) => [car.id, car]));
30
178
  const finished = sim.raceControl.finishOrder.map((id) => byId.get(id)).filter(Boolean);
31
179
  const finishedIds = new Set(finished.map((car) => car.id));
32
- const missedFinished = sim.cars
33
- .filter((car) => canAppearInRaceOrder(car) && !isRaceDnf(car) && car.finished && !finishedIds.has(car.id))
180
+ const missedFinished = raceCars
181
+ .filter((car) => car.finished && !finishedIds.has(car.id))
34
182
  .sort((a, b) => {
35
183
  const delta = (a.finishRank ?? Infinity) - (b.finishRank ?? Infinity);
36
184
  return delta === 0 ? a.index - b.index : delta;
37
185
  });
38
- const running = sortLive(raceCars.filter((car) => !finishedIds.has(car.id) && !car.finished && !isRaceDnf(car)));
186
+ const running = sortLiveRaceOrderInPlace(raceCars.filter((car) => !finishedIds.has(car.id) && !car.finished));
39
187
  return [...finished, ...missedFinished, ...running, ...dnfCars.filter((car) => !finishedIds.has(car.id))];
40
188
  }
41
189
 
42
190
  if (sim.safetyCar.deployed && sim.raceControl.frozenOrder?.length) {
191
+ const byId = new Map(sim.cars.map((car) => [car.id, car]));
43
192
  const frozen = sim.raceControl.frozenOrder.map((id) => byId.get(id)).filter(Boolean);
44
193
  const activeFrozen = frozen.filter((car) => !isRaceDnf(car));
45
194
  const activeFrozenIds = new Set(activeFrozen.map((car) => car.id));
@@ -49,9 +198,11 @@ export function orderedCarsForSimulation(sim) {
49
198
  ];
50
199
  }
51
200
 
52
- return [...sortLive(raceCars.filter((car) => !isRaceDnf(car))), ...dnfCars];
201
+ sortLiveRaceOrderInPlace(raceCars);
202
+ return appendCars(raceCars, dnfCars);
53
203
  }
54
204
 
205
+
55
206
  export function raceOrderCarsForSimulation(sim) {
56
207
  return sim.cars.filter((car) => affectsRaceOrder(car) && !isRaceDnf(car));
57
208
  }
@@ -68,14 +219,36 @@ export function driverRaceContextForSimulation(sim, orderedCars = orderedCarsFor
68
219
  };
69
220
  }
70
221
 
71
- export function computeAggressionForSimulation(sim, car, orderIndex = Math.max(0, (car.rank ?? 1) - 1)) {
222
+ export function buildDrsReferenceByCarId(sim, orderedCars = orderedCarsForSimulation(sim)) {
223
+ const referencesByIndex = buildDrsReferenceArrayForSimulation(sim, orderedCars);
224
+ const references = new Map();
225
+ for (let index = 0; index < orderedCars.length; index += 1) {
226
+ const car = orderedCars[index];
227
+ if (!affectsRaceOrder(car) || isRaceDnf(car)) continue;
228
+ references.set(car.id, referencesByIndex[car.index] ?? null);
229
+ }
230
+ return references;
231
+ }
232
+
233
+ export function assignDrsReferenceCarsForSimulation(sim, orderedCars = orderedCarsForSimulation(sim)) {
234
+ return buildDrsReferenceArrayForSimulation(sim, orderedCars);
235
+ }
236
+
237
+ export function computeAggressionForSimulation(
238
+ sim,
239
+ car,
240
+ orderIndex = Math.max(0, (car.rank ?? 1) - 1),
241
+ fieldDepth = null,
242
+ ) {
72
243
  const personality = car.personality ?? { baseAggression: 0.5, riskTolerance: 0.5, patience: 0.5 };
73
244
  if (sim.safetyCar.deployed || car.canAttack === false) {
74
245
  return clamp(personality.baseAggression * 0.62, 0.08, 0.62);
75
246
  }
76
247
 
77
- const fieldDepth = Math.max(1, raceOrderCarsForSimulation(sim).length - 1);
78
- const positionPressure = clamp(orderIndex / fieldDepth, 0, 1);
248
+ const resolvedFieldDepth = Number.isFinite(fieldDepth)
249
+ ? Math.max(1, fieldDepth)
250
+ : Math.max(1, raceOrderCarsForSimulation(sim).length - 1);
251
+ const positionPressure = clamp(orderIndex / resolvedFieldDepth, 0, 1);
79
252
  const gapPressure = Number.isFinite(car.gapAhead) ? clamp((230 - car.gapAhead) / 230, 0, 1) : 0;
80
253
  const tireConfidence = clamp(((car.tireEnergy ?? 100) - 42) / 58, 0, 1);
81
254
  const patienceDamping = (1 - gapPressure) * personality.patience * 0.08;
@@ -92,22 +265,6 @@ export function computeAggressionForSimulation(sim, car, orderIndex = Math.max(0
92
265
  }
93
266
 
94
267
  export function getDrsReferenceCarForSimulation(sim, car) {
95
- let closest = null;
96
- sim.cars.forEach((candidate) => {
97
- if (
98
- candidate === car ||
99
- !affectsRaceOrder(candidate) ||
100
- isRaceDnf(candidate) ||
101
- candidate.finished ||
102
- sim.isCarInActivePitStop(candidate)
103
- ) return;
104
- const delta = distanceForward(
105
- car.progress ?? car.raceDistance ?? 0,
106
- candidate.progress ?? candidate.raceDistance ?? 0,
107
- sim.track.length,
108
- );
109
- if (delta <= 0 || delta > sim.track.length / 2) return;
110
- if (!closest || delta < closest.delta) closest = { car: candidate, delta };
111
- });
112
- return closest?.car ?? null;
268
+ if (!car) return null;
269
+ return buildDrsReferenceArrayForSimulation(sim)[car.index] ?? null;
113
270
  }