@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
@@ -1,8 +1,10 @@
1
1
  import { pointAt } from '../track/trackModel.js';
2
2
  import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
3
3
  import {
4
- affectsRaceOrder,
5
- } from '../participants/participantInteractions.js';
4
+ clearRunoffCenterState,
5
+ takeValidRunoffCenterState,
6
+ } from '../vehicle/runoffResponse.js';
7
+ import { affectsRaceOrder } from '../participants/participantInteractions.js';
6
8
  import {
7
9
  estimateGapAheadSeconds,
8
10
  recordTimingLineCrossings,
@@ -13,13 +15,14 @@ import {
13
15
  wholeLapGap,
14
16
  } from '../timing/raceTiming.js';
15
17
  import { progressDelta } from './raceDistance.js';
18
+ import { assignDrsReferenceCarsForSimulation } from './raceOrder.js';
16
19
  import { isRaceDnf } from './retirements.js';
17
20
  import { freezeVehicleMotion } from '../vehicle/vehicleKinematics.js';
18
21
 
19
- export function recalculateRaceStateForSimulation(sim, { updateDrs = true } = {}) {
22
+ export function refreshLocalRaceStateForSimulation(sim) {
20
23
  sim.cars.forEach((car) => {
21
- const previousRaceDistance = car.raceDistance;
22
24
  if (car.destroyed || car.outOfRace) {
25
+ clearRunoffCenterState(car);
23
26
  applyWheelSurfaceState(car, sim.track);
24
27
  freezeVehicleMotion(car, {
25
28
  clearManualControls: true,
@@ -33,52 +36,134 @@ export function recalculateRaceStateForSimulation(sim, { updateDrs = true } = {}
33
36
  car.drsZoneEnabled = false;
34
37
  return;
35
38
  }
39
+ const centerState = takeValidRunoffCenterState(car);
40
+ if (centerState) {
41
+ applyWheelSurfaceState(car, sim.track, { centerState, cacheAsAuto: true });
42
+ } else {
43
+ applyWheelSurfaceState(car, sim.track);
44
+ }
45
+ });
46
+ }
47
+
48
+ function resolveLeaderGapSeconds({
49
+ sim,
50
+ leader,
51
+ ahead,
52
+ car,
53
+ leaderGapLaps,
54
+ gapAheadSeconds,
55
+ currentTime,
56
+ track,
57
+ }) {
58
+ if (leaderGapLaps > 0) return Infinity;
59
+ if (!leader || leader === car) return 0;
60
+ if (!ahead) return Infinity;
61
+ if (ahead === leader) {
62
+ if (sim?.runtimeBenchmarkStats) {
63
+ sim.runtimeBenchmarkStats.leaderGapAccumulated = (sim.runtimeBenchmarkStats.leaderGapAccumulated ?? 0) + 1;
64
+ }
65
+ return gapAheadSeconds;
66
+ }
67
+ if (Number.isFinite(gapAheadSeconds) && Number.isFinite(ahead.leaderGapSeconds)) {
68
+ if (sim?.runtimeBenchmarkStats) {
69
+ sim.runtimeBenchmarkStats.leaderGapAccumulated = (sim.runtimeBenchmarkStats.leaderGapAccumulated ?? 0) + 1;
70
+ }
71
+ return ahead.leaderGapSeconds + gapAheadSeconds;
72
+ }
73
+ if (sim?.runtimeBenchmarkStats) {
74
+ sim.runtimeBenchmarkStats.leaderGapFallbackEstimates = (sim.runtimeBenchmarkStats.leaderGapFallbackEstimates ?? 0) + 1;
75
+ }
76
+ return estimateGapAheadSeconds(leader, car, currentTime, track);
77
+ }
78
+
79
+ function recordTimingStateForCar(car, currentTime, track, previousRaceDistanceForTiming, runtimeBenchmarkStats) {
80
+ recordTimingSample(car, currentTime);
81
+ recordTimingLineCrossings(car, previousRaceDistanceForTiming, currentTime, track);
82
+ car.previousRaceDistanceForTiming = car.raceDistance;
83
+ if (runtimeBenchmarkStats) {
84
+ runtimeBenchmarkStats.timingStateUpdates = (runtimeBenchmarkStats.timingStateUpdates ?? 0) + 1;
85
+ }
86
+ }
87
+
88
+ export function recalculateRaceStateForSimulation(sim, { updateDrs = true, refreshSurfaces = true } = {}) {
89
+ let sectorPerformanceDirty = false;
90
+ sim.cars.forEach((car) => {
91
+ const previousRaceDistance = car.raceDistance;
92
+ const previousRaceDistanceForTiming = car.previousRaceDistanceForTiming;
93
+ if (car.destroyed || car.outOfRace) {
94
+ if (refreshSurfaces) applyWheelSurfaceState(car, sim.track);
95
+ freezeVehicleMotion(car, {
96
+ clearManualControls: true,
97
+ physicsMode: sim.physicsMode,
98
+ stabilityState: car.destroyed ? 'destroyed' : car.stabilityState,
99
+ });
100
+ car.canAttack = false;
101
+ car.drsEligible = false;
102
+ car.drsActive = false;
103
+ car.drsZoneId = null;
104
+ car.drsZoneEnabled = false;
105
+ recordTimingStateForCar(car, sim.time, sim.track, previousRaceDistanceForTiming, sim.runtimeBenchmarkStats);
106
+ return;
107
+ }
36
108
  if (car.gridLocked) {
37
109
  const gridPoint = pointAt(sim.track, car.gridDistance);
38
- applyWheelSurfaceState(car, sim.track);
110
+ if (refreshSurfaces) applyWheelSurfaceState(car, sim.track);
39
111
  car.progress = gridPoint.distance;
40
112
  car.raceDistance = car.gridDistance;
41
113
  car.lap = 1;
42
114
  resetLapTelemetry(car, sim.time, sim.track, sim.totalLaps);
115
+ sectorPerformanceDirty = true;
116
+ recordTimingStateForCar(car, sim.time, sim.track, previousRaceDistanceForTiming, sim.runtimeBenchmarkStats);
43
117
  return;
44
118
  }
45
119
 
46
- applyWheelSurfaceState(car, sim.track);
120
+ if (refreshSurfaces) applyWheelSurfaceState(car, sim.track);
47
121
  const previousProgress = car.progress ?? car.trackState.distance;
48
122
  const delta = progressDelta(car.trackState.distance, previousProgress, sim.track.length);
49
123
  car.raceDistance = (car.raceDistance ?? previousProgress) + delta;
50
124
  car.progress = car.trackState.distance;
51
125
  car.lap = sim.computeLap(car.raceDistance);
52
- updateLapTelemetry(car, previousRaceDistance, sim.time, sim.track, sim.totalLaps);
126
+ sectorPerformanceDirty = updateLapTelemetry(car, previousRaceDistance, sim.time, sim.track, sim.totalLaps) || sectorPerformanceDirty;
127
+ recordTimingStateForCar(car, sim.time, sim.track, previousRaceDistanceForTiming, sim.runtimeBenchmarkStats);
53
128
  });
54
129
 
55
- updateSectorPerformance(sim.cars);
56
- sim.cars.forEach((car) => {
57
- recordTimingSample(car, sim.time);
58
- recordTimingLineCrossings(car, car.previousRaceDistanceForTiming, sim.time, sim.track);
59
- car.previousRaceDistanceForTiming = car.raceDistance;
60
- });
130
+ if (sectorPerformanceDirty) {
131
+ sim.runtimeBenchmarkStats && (sim.runtimeBenchmarkStats.sectorPerformanceDirtyCommits = (sim.runtimeBenchmarkStats.sectorPerformanceDirtyCommits ?? 0) + 1);
132
+ }
133
+ updateSectorPerformance(sim.cars, sim.runtimeBenchmarkStats);
61
134
 
62
135
  const ordered = sim.orderedCars();
63
- const orderedIds = new Set(ordered.map((car) => car.id));
64
- sim.cars.forEach((car) => {
65
- if (orderedIds.has(car.id) || affectsRaceOrder(car)) return;
66
- car.rank = null;
67
- car.classifiedRank = null;
68
- car.gapAhead = Infinity;
69
- car.gapAheadLaps = 0;
70
- car.intervalAheadLaps = 0;
71
- car.leaderGapLaps = 0;
72
- car.gapAheadSeconds = Infinity;
73
- car.intervalAheadSeconds = Infinity;
74
- car.leaderGapSeconds = Infinity;
75
- car.drsEligible = false;
76
- car.drsActive = false;
77
- car.drsZoneId = null;
78
- car.drsZoneEnabled = false;
79
- car.canAttack = false;
80
- });
136
+ const drsReferenceByIndex = updateDrs ? assignDrsReferenceCarsForSimulation(sim, ordered) : null;
137
+ if (ordered.length !== sim.cars.length) {
138
+ if (sim?.runtimeBenchmarkStats) {
139
+ sim.runtimeBenchmarkStats.excludedCarResetScans = (sim.runtimeBenchmarkStats.excludedCarResetScans ?? 0) + 1;
140
+ }
141
+ sim.cars.forEach((car) => {
142
+ if (affectsRaceOrder(car)) return;
143
+ car.rank = null;
144
+ car.classifiedRank = null;
145
+ car.gapAhead = Infinity;
146
+ car.gapAheadLaps = 0;
147
+ car.intervalAheadLaps = 0;
148
+ car.leaderGapLaps = 0;
149
+ car.gapAheadSeconds = Infinity;
150
+ car.intervalAheadSeconds = Infinity;
151
+ car.leaderGapSeconds = Infinity;
152
+ car.drsEligible = false;
153
+ car.drsActive = false;
154
+ car.drsZoneId = null;
155
+ car.drsZoneEnabled = false;
156
+ car.canAttack = false;
157
+ });
158
+ } else if (sim?.runtimeBenchmarkStats) {
159
+ sim.runtimeBenchmarkStats.excludedCarResetSkips = (sim.runtimeBenchmarkStats.excludedCarResetSkips ?? 0) + 1;
160
+ }
81
161
  const leader = ordered[0];
162
+ let liveRaceOrderCount = ordered.length;
163
+ while (liveRaceOrderCount > 0 && isRaceDnf(ordered[liveRaceOrderCount - 1])) {
164
+ liveRaceOrderCount -= 1;
165
+ }
166
+ const aggressionFieldDepth = Math.max(1, liveRaceOrderCount - 1);
82
167
  ordered.forEach((car, index) => {
83
168
  if (isRaceDnf(car)) {
84
169
  car.rank = index + 1;
@@ -98,7 +183,7 @@ export function recalculateRaceStateForSimulation(sim, { updateDrs = true } = {}
98
183
  return;
99
184
  }
100
185
  const ahead = ordered[index - 1];
101
- const drsReference = sim.getDrsReferenceCar(car);
186
+ const drsReference = drsReferenceByIndex?.[car.index] ?? null;
102
187
  const gap = ahead ? ahead.raceDistance - car.raceDistance : Infinity;
103
188
  const intervalAheadLaps = ahead ? wholeLapGap(ahead.raceDistance, car.raceDistance, sim.track.length) : 0;
104
189
  const leaderGapLaps = leader ? wholeLapGap(leader.raceDistance, car.raceDistance, sim.track.length) : 0;
@@ -112,11 +197,18 @@ export function recalculateRaceStateForSimulation(sim, { updateDrs = true } = {}
112
197
  ? estimateGapAheadSeconds(ahead, car, sim.time, sim.track)
113
198
  : Infinity;
114
199
  car.intervalAheadSeconds = car.gapAheadSeconds;
115
- car.leaderGapSeconds = leaderGapLaps > 0
116
- ? Infinity
117
- : (leader && leader !== car ? estimateGapAheadSeconds(leader, car, sim.time, sim.track) : 0);
200
+ car.leaderGapSeconds = resolveLeaderGapSeconds({
201
+ sim,
202
+ leader,
203
+ ahead,
204
+ car,
205
+ leaderGapLaps,
206
+ gapAheadSeconds: car.gapAheadSeconds,
207
+ currentTime: sim.time,
208
+ track: sim.track,
209
+ });
118
210
  car.canAttack = !sim.safetyCar.deployed && !car.finished && !activePitStop;
119
- car.aggression = sim.computeAggression(car, index);
211
+ car.aggression = sim.computeAggression(car, index, aggressionFieldDepth);
120
212
  if (activePitStop) {
121
213
  car.drsEligible = false;
122
214
  car.drsActive = false;
@@ -124,6 +216,6 @@ export function recalculateRaceStateForSimulation(sim, { updateDrs = true } = {}
124
216
  car.drsZoneEnabled = false;
125
217
  } else if (updateDrs) sim.updateDrsLatch(car, drsReference, Boolean(drsReference));
126
218
  });
127
- sim.evaluateRaceFinish();
219
+ sim.evaluateRaceFinish(ordered);
128
220
  return ordered;
129
221
  }
@@ -1,5 +1,6 @@
1
1
  import { buildTrackModel, createProceduralTrack, TRACK } from '../track/trackModel.js';
2
2
  import { attachTrackQueryIndex, createTrackQueryIndex, forkTrackQueryIndex } from '../track/trackQueryIndex.js';
3
+ import { attachTrackSnapshotJsonSerializer } from '../track/trackSnapshotJson.js';
3
4
  import { createMulberry32 } from '../simMath.js';
4
5
  import { createCar } from '../vehicle/vehicleState.js';
5
6
  import { normalizePhysicsMode } from '../vehicle/vehiclePhysics.js';
@@ -78,6 +79,7 @@ export function initializeRaceSimulation(simulation, {
78
79
  tireCompounds: simulation.rules.modules?.tireStrategy?.compounds,
79
80
  PIT_INTENT_NONE,
80
81
  });
82
+ attachTrackSnapshotJsonSerializer(simulation.track);
81
83
  simulation.recalculateRaceState({ updateDrs: false });
82
84
  simulation.cars.forEach((car) => resetTimingHistory(car, simulation.time));
83
85
  simulation.cars.forEach((car) => resetTimingLineCrossings(car, simulation.time));
@@ -6,6 +6,13 @@ import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
6
6
  import { applyRedFlagHoldForSimulation } from './redFlag.js';
7
7
  import { freezeRetiredCar, updateStalledDnfForSimulation } from './stalledDnf.js';
8
8
 
9
+ function measureRuntimePhase(simulation, name, run) {
10
+ const profiler = simulation.runtimeProfiler;
11
+ return typeof profiler?.measure === 'function'
12
+ ? profiler.measure(name, run)
13
+ : run();
14
+ }
15
+
9
16
  export function runRaceStep(simulation, dt) {
10
17
  const delta = clamp(dt, 0, 1 / 20);
11
18
  if (!Number.isFinite(delta) || delta <= 0) return;
@@ -14,22 +21,22 @@ export function runRaceStep(simulation, dt) {
14
21
  simulation.events = [];
15
22
  updateReplayGhosts(simulation.replayGhosts, simulation.time);
16
23
  simulation.updateStartSequence();
17
- const orderedCars = simulation.recalculateRaceState({ updateDrs: false }) ?? simulation.orderedCars();
18
24
 
19
25
  if (simulation.raceControl.mode === 'pre-start' && simulation.cars.every((car) => car.gridLocked)) {
20
26
  simulation.holdGridCars();
21
- simulation.recalculateRaceState({ updateDrs: false });
27
+ measureRuntimePhase(simulation, 'broadRaceCommit', () => simulation.recalculateRaceState({ updateDrs: false }));
22
28
  return;
23
29
  }
24
30
 
25
31
  if (simulation.raceControl.redFlag) {
26
32
  applyRedFlagHoldForSimulation(simulation);
27
- simulation.recalculateRaceState({ updateDrs: false });
33
+ measureRuntimePhase(simulation, 'broadRaceCommit', () => simulation.recalculateRaceState({ updateDrs: false }));
28
34
  return;
29
35
  }
30
36
 
31
37
  simulation.updateSafetyCar(delta);
32
38
 
39
+ const orderedCars = simulation.orderedCars();
33
40
  const raceContext = simulation.driverRaceContext(orderedCars);
34
41
  const orderedIndexById = new Map(orderedCars.map((car, index) => [car.id, index]));
35
42
  const driveCars = orderedCars.length === simulation.cars.length
@@ -60,21 +67,26 @@ export function runRaceStep(simulation, dt) {
60
67
  brake: controls.brake ?? 0,
61
68
  };
62
69
  if (simulation.physicsMode === 'advanced') {
63
- applyWheelSurfaceState(car, simulation.track);
70
+ measureRuntimePhase(simulation, 'prePhysicsWheelSurface', () => {
71
+ applyWheelSurfaceState(car, simulation.track);
72
+ });
64
73
  }
65
- integrateVehiclePhysics(car, controls, delta, {
66
- physicsMode: simulation.physicsMode,
67
- tireDegradationEnabled: simulation.rules.modules?.tireDegradation?.enabled !== false,
74
+ measureRuntimePhase(simulation, 'physicsIntegration', () => {
75
+ integrateVehiclePhysics(car, controls, delta, {
76
+ physicsMode: simulation.physicsMode,
77
+ tireDegradationEnabled: simulation.rules.modules?.tireDegradation?.enabled !== false,
78
+ });
79
+ });
80
+ measureRuntimePhase(simulation, 'runoffResponse', () => {
81
+ simulation.applyRunoffResponse(car);
68
82
  });
69
- simulation.applyRunoffResponse(car);
70
83
  car.contactCooldown = Math.max(0, car.contactCooldown - delta);
71
84
  });
72
85
 
73
- simulation.resolveCollisions();
74
- simulation.recalculateRaceState();
75
- if (updateStalledDnfForSimulation(simulation, delta)) {
76
- simulation.recalculateRaceState();
77
- }
86
+ measureRuntimePhase(simulation, 'collisions', () => simulation.resolveCollisions());
87
+ measureRuntimePhase(simulation, 'localSurfaceRefresh', () => simulation.refreshLocalRaceState());
88
+ updateStalledDnfForSimulation(simulation, delta);
89
+ measureRuntimePhase(simulation, 'broadRaceCommit', () => simulation.recalculateRaceState({ refreshSurfaces: false }));
78
90
  simulation.reviewTrackLimits();
79
91
  simulation.reviewPitLaneSpeeding();
80
92
  }
@@ -21,7 +21,7 @@ import {
21
21
  } from './vehicle/contactResolution.js';
22
22
  import { applyRunoffResponseForSimulation } from './vehicle/runoffResponse.js';
23
23
  import { resetLapTelemetry, resetTimingHistory, resetTimingLineCrossings } from './timing/raceTiming.js';
24
- import { updateDrsLatch as updateDrsLatchState } from './timing/drsTiming.js';
24
+ import { updateDrsLatchForSimulation as updateDrsLatchState } from './timing/drsTiming.js';
25
25
  import {
26
26
  PIT_INTENT_NONE,
27
27
  setPitIntentForSimulation,
@@ -87,7 +87,7 @@ import {
87
87
  finishDistanceForRace,
88
88
  progressDelta,
89
89
  } from './race/raceDistance.js';
90
- import { recalculateRaceStateForSimulation } from './race/raceProgress.js';
90
+ import { recalculateRaceStateForSimulation, refreshLocalRaceStateForSimulation } from './race/raceProgress.js';
91
91
  import { evaluateRaceFinishForSimulation } from './race/raceFinish.js';
92
92
  import { applyGridDropForSimulation } from './race/gridPenalties.js';
93
93
  import { initializeRaceSimulation, normalizePitIntentForRace } from './race/raceSetup.js';
@@ -98,6 +98,7 @@ import {
98
98
  snapshotRace,
99
99
  snapshotRaceObservation,
100
100
  snapshotRaceRender,
101
+ snapshotRaceRenderInto,
101
102
  snapshotRaceTraining,
102
103
  } from './snapshots/raceSnapshots.js';
103
104
  import {
@@ -370,8 +371,8 @@ export class F1RaceSimulation {
370
371
  applyGridDropForSimulation(this, driverId, positions);
371
372
  }
372
373
 
373
- reviewCollision(first, second, collision) {
374
- reviewCollisionForSimulation(this, first, second, collision);
374
+ reviewCollision(first, second, collision, options) {
375
+ reviewCollisionForSimulation(this, first, second, collision, options);
375
376
  }
376
377
 
377
378
  reviewTireRequirement(car) {
@@ -400,6 +401,10 @@ export class F1RaceSimulation {
400
401
  return snapshotRaceRender(this);
401
402
  }
402
403
 
404
+ snapshotRenderInto(target = {}) {
405
+ return snapshotRaceRenderInto(this, target);
406
+ }
407
+
403
408
  snapshotObservation() {
404
409
  return snapshotRaceObservation(this);
405
410
  }
@@ -420,8 +425,8 @@ export class F1RaceSimulation {
420
425
  return driverRaceContextForSimulation(this, orderedCars);
421
426
  }
422
427
 
423
- computeAggression(car, orderIndex = Math.max(0, (car.rank ?? 1) - 1)) {
424
- return computeAggressionForSimulation(this, car, orderIndex);
428
+ computeAggression(car, orderIndex = Math.max(0, (car.rank ?? 1) - 1), fieldDepth = null) {
429
+ return computeAggressionForSimulation(this, car, orderIndex, fieldDepth);
425
430
  }
426
431
 
427
432
  updateStartSequence() {
@@ -448,12 +453,16 @@ export class F1RaceSimulation {
448
453
  applyRunoffResponseForSimulation(this, car);
449
454
  }
450
455
 
451
- recalculateRaceState({ updateDrs = true } = {}) {
452
- return recalculateRaceStateForSimulation(this, { updateDrs });
456
+ recalculateRaceState({ updateDrs = true, refreshSurfaces = true } = {}) {
457
+ return recalculateRaceStateForSimulation(this, { updateDrs, refreshSurfaces });
458
+ }
459
+
460
+ refreshLocalRaceState() {
461
+ refreshLocalRaceStateForSimulation(this);
453
462
  }
454
463
 
455
- evaluateRaceFinish() {
456
- evaluateRaceFinishForSimulation(this);
464
+ evaluateRaceFinish(orderedCars = undefined) {
465
+ evaluateRaceFinishForSimulation(this, orderedCars);
457
466
  }
458
467
 
459
468
  applyOutstandingServicePenalties() {
@@ -481,13 +490,7 @@ export class F1RaceSimulation {
481
490
  }
482
491
 
483
492
  updateDrsLatch(car, ahead, hasReference = Boolean(ahead)) {
484
- updateDrsLatchState(car, ahead, {
485
- hasReference,
486
- safetyCarDeployed: this.safetyCar.deployed,
487
- time: this.time,
488
- track: this.track,
489
- rules: this.rules,
490
- });
493
+ updateDrsLatchState(this, car, ahead, hasReference);
491
494
  }
492
495
 
493
496
  resolveCollisions() {
@@ -1,7 +1,21 @@
1
1
  import { simSpeedToKph } from '../units.js';
2
2
 
3
3
  export function calculateCollisionPenalties({ first, second, collision = null, rule }) {
4
- if (!rule) return [];
4
+ const penalties = [];
5
+ emitCollisionPenalties({
6
+ first,
7
+ second,
8
+ collision,
9
+ rule,
10
+ emit: (penalty) => {
11
+ penalties.push(penalty);
12
+ },
13
+ });
14
+ return penalties;
15
+ }
16
+
17
+ export function emitCollisionPenalties({ first, second, collision = null, rule, emit, emitContext = null }) {
18
+ if (!rule || typeof emit !== 'function') return 0;
5
19
  const severity = collision?.depth ?? 0;
6
20
  const severityThreshold = (rule.minimumSeverity ?? 0) + (rule.relaxedSeverityMargin ?? 0) * (1 - rule.strictness);
7
21
  const impactSpeed = collision?.impactSpeed ?? 0;
@@ -12,41 +26,44 @@ export function calculateCollisionPenalties({ first, second, collision = null, r
12
26
  ? collision.sharedFaultDriverIds
13
27
  : [];
14
28
 
15
- if (severity < severityThreshold) return [];
16
- if (impactSpeed < impactSpeedThreshold) return [];
29
+ if (severity < severityThreshold) return 0;
30
+ if (impactSpeed < impactSpeedThreshold) return 0;
17
31
 
32
+ let emitted = 0;
18
33
  if (sharedFaultDriverIds.length > 0) {
19
- return sharedFaultDriverIds
20
- .map((driverId) => {
21
- const car = first.id === driverId ? first : second.id === driverId ? second : null;
22
- if (!car) return null;
23
- const other = first.id === driverId ? second : first;
24
- return buildCollisionPenalty(car, other, rule, {
25
- severity,
26
- severityThreshold,
27
- impactSpeed,
28
- impactSpeedThreshold,
29
- aheadDriverId: null,
30
- atFaultDriverId: driverId,
31
- sharedFault: true,
32
- reason: 'Unclear collision responsibility',
33
- });
34
- })
35
- .filter(Boolean);
34
+ for (let index = 0; index < sharedFaultDriverIds.length; index += 1) {
35
+ const driverId = sharedFaultDriverIds[index];
36
+ const car = first.id === driverId ? first : second.id === driverId ? second : null;
37
+ if (!car) continue;
38
+ const other = first.id === driverId ? second : first;
39
+ emit(buildCollisionPenalty(car, other, rule, {
40
+ severity,
41
+ severityThreshold,
42
+ impactSpeed,
43
+ impactSpeedThreshold,
44
+ aheadDriverId: null,
45
+ atFaultDriverId: driverId,
46
+ sharedFault: true,
47
+ reason: 'Unclear collision responsibility',
48
+ }), emitContext);
49
+ emitted += 1;
50
+ }
51
+ return emitted;
36
52
  }
37
53
 
38
- if (!atFaultDriverId || !aheadDriverId) return [];
54
+ if (!atFaultDriverId || !aheadDriverId) return 0;
39
55
 
40
56
  const atFault = first.id === atFaultDriverId ? first : second;
41
57
  const other = first.id === atFaultDriverId ? second : first;
42
- return [buildCollisionPenalty(atFault, other, rule, {
58
+ emit(buildCollisionPenalty(atFault, other, rule, {
43
59
  severity,
44
60
  severityThreshold,
45
61
  impactSpeed,
46
62
  impactSpeedThreshold,
47
63
  aheadDriverId,
48
64
  atFaultDriverId,
49
- })];
65
+ }), emitContext);
66
+ return 1;
50
67
  }
51
68
 
52
69
  function buildCollisionPenalty(car, other, rule, context) {