@inventure71/paddockjs 1.0.0 → 2.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 (190) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/INSTALL_AND_UPDATE.md +4 -3
  3. package/README.md +194 -12
  4. package/docs/architecture.md +119 -44
  5. package/docs/concepts.md +4 -2
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +299 -17
  8. package/docs/index.md +1 -0
  9. package/docs/learnings.md +22 -0
  10. package/docs/rules.md +37 -10
  11. package/docs/system_specs.md +23 -10
  12. package/docs/training.md +272 -30
  13. package/examples/python/README.md +62 -0
  14. package/examples/python/base_policy_server.py +191 -0
  15. package/examples/python/requirements.txt +2 -0
  16. package/package.json +12 -4
  17. package/src/api/PaddockSimulatorController.js +4 -0
  18. package/src/app/BrowserExpertAdapter.js +367 -9
  19. package/src/app/F1SimulatorApp.js +305 -1603
  20. package/src/app/banners/bannerFormatters.js +55 -0
  21. package/src/app/banners/raceDataBannerController.js +141 -0
  22. package/src/app/banners/stewardMessageController.js +99 -0
  23. package/src/app/camera/cameraConstants.js +21 -0
  24. package/src/app/camera/cameraController.js +265 -0
  25. package/src/app/camera/cameraGeometry.js +176 -0
  26. package/src/app/domBindings.js +4 -0
  27. package/src/app/readouts/carOverviewRenderer.js +109 -0
  28. package/src/app/readouts/controlStateRenderer.js +37 -0
  29. package/src/app/readouts/raceStatusRenderer.js +133 -0
  30. package/src/app/readouts/readoutFormatters.js +58 -0
  31. package/src/app/readouts/telemetryRenderer.js +113 -0
  32. package/src/app/readouts/timingTowerRenderer.js +177 -0
  33. package/src/app/rendering/appAssets.js +38 -0
  34. package/src/app/rendering/carRenderer.js +286 -0
  35. package/src/app/rendering/displayUtils.js +22 -0
  36. package/src/app/rendering/drsTrailRenderer.js +90 -0
  37. package/src/app/rendering/expertSensorRenderer.js +126 -0
  38. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  39. package/src/app/rendering/replayGhostRenderer.js +124 -0
  40. package/src/app/rendering/trackRenderer.js +25 -0
  41. package/src/app/runtime/frameLoop.js +66 -0
  42. package/src/app/runtime/runtimeTiming.js +34 -0
  43. package/src/app/runtime/runtimeVisibility.js +45 -0
  44. package/src/config/defaultOptions.js +20 -0
  45. package/src/environment/actions.js +13 -5
  46. package/src/environment/controllerLoop.js +246 -0
  47. package/src/environment/episode.js +86 -2
  48. package/src/environment/evaluation.js +54 -32
  49. package/src/environment/index.d.ts +361 -9
  50. package/src/environment/index.js +1 -0
  51. package/src/environment/metrics.js +159 -0
  52. package/src/environment/observationOptions.js +22 -1
  53. package/src/environment/observations.js +325 -40
  54. package/src/environment/options.js +37 -5
  55. package/src/environment/runtime.js +222 -21
  56. package/src/environment/scenarios.js +37 -8
  57. package/src/environment/sensors/bodySenses.js +23 -0
  58. package/src/environment/sensors/boundarySenses.js +17 -0
  59. package/src/environment/sensors/carRays.js +75 -0
  60. package/src/environment/sensors/contactSenses.js +32 -0
  61. package/src/environment/sensors/index.js +157 -0
  62. package/src/environment/sensors/indexedRayBands.js +103 -0
  63. package/src/environment/sensors/nearbyCars.js +52 -0
  64. package/src/environment/sensors/opponentRadar.js +65 -0
  65. package/src/environment/sensors/rayConfig.js +110 -0
  66. package/src/environment/sensors/rayDefaults.js +6 -0
  67. package/src/environment/sensors/rayGeometry.js +60 -0
  68. package/src/environment/sensors/rayGuards.js +19 -0
  69. package/src/environment/sensors/sensorTargets.js +60 -0
  70. package/src/environment/sensors/surfaceRays.js +296 -0
  71. package/src/environment/sensors/trackRays.js +261 -0
  72. package/src/environment/sensors.js +1 -367
  73. package/src/environment/specs.js +100 -5
  74. package/src/environment/workerProtocol.js +13 -1
  75. package/src/index.d.ts +169 -6
  76. package/src/index.js +5 -0
  77. package/src/rendering/proceduralTrackAsset.js +20 -823
  78. package/src/rendering/renderSnapshot.js +38 -12
  79. package/src/rendering/track/finishLineRenderer.js +34 -0
  80. package/src/rendering/track/gridRenderer.js +47 -0
  81. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  82. package/src/rendering/track/pitLaneRenderer.js +248 -0
  83. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  84. package/src/rendering/track/trackRenderConstants.js +38 -0
  85. package/src/rendering/track/trackRenderGeometry.js +94 -0
  86. package/src/simulation/collisionGeometry.js +1 -1
  87. package/src/simulation/driver/driverControlConstants.js +28 -0
  88. package/src/simulation/driver/driverController.js +47 -0
  89. package/src/simulation/driver/driverInput.js +28 -0
  90. package/src/simulation/driver/driverMath.js +6 -0
  91. package/src/simulation/driver/driverPersonality.js +25 -0
  92. package/src/simulation/driver/edgeRecovery.js +48 -0
  93. package/src/simulation/driver/racingControls.js +399 -0
  94. package/src/simulation/driver/racingLinePlan.js +166 -0
  95. package/src/simulation/driver/recoveryDynamics.js +38 -0
  96. package/src/simulation/driver/rejoinControls.js +213 -0
  97. package/src/simulation/driver/safetyCarControls.js +35 -0
  98. package/src/simulation/driver/trafficScan.js +51 -0
  99. package/src/simulation/driverController.js +4 -619
  100. package/src/simulation/participants/participantInteractions.js +120 -0
  101. package/src/simulation/pit/pitFlow.js +234 -0
  102. package/src/simulation/pit/pitIntent.js +92 -0
  103. package/src/simulation/pit/pitOccupancy.js +47 -0
  104. package/src/simulation/pit/pitPenaltyService.js +71 -0
  105. package/src/simulation/pit/pitQueue.js +55 -0
  106. package/src/simulation/pit/pitRouteMovement.js +160 -0
  107. package/src/simulation/pit/pitRouting.js +319 -0
  108. package/src/simulation/pit/pitService.js +41 -0
  109. package/src/simulation/pit/pitServiceConstants.js +21 -0
  110. package/src/simulation/pit/pitServiceProfile.js +45 -0
  111. package/src/simulation/pit/pitSnapshots.js +61 -0
  112. package/src/simulation/pit/pitState.js +113 -0
  113. package/src/simulation/pit/pitTireService.js +92 -0
  114. package/src/simulation/race/classification.js +104 -0
  115. package/src/simulation/race/gridPenalties.js +37 -0
  116. package/src/simulation/race/raceControlState.js +38 -0
  117. package/src/simulation/race/raceDistance.js +32 -0
  118. package/src/simulation/race/raceFinish.js +65 -0
  119. package/src/simulation/race/raceLifecycle.js +71 -0
  120. package/src/simulation/race/raceOrder.js +113 -0
  121. package/src/simulation/race/raceProgress.js +124 -0
  122. package/src/simulation/race/raceSetup.js +93 -0
  123. package/src/simulation/race/raceStep.js +80 -0
  124. package/src/simulation/race/redFlag.js +15 -0
  125. package/src/simulation/race/retirements.js +44 -0
  126. package/src/simulation/race/safetyCar.js +86 -0
  127. package/src/simulation/race/stalledDnf.js +86 -0
  128. package/src/simulation/raceSimulation.js +230 -2817
  129. package/src/simulation/replay/replayGhosts.js +128 -0
  130. package/src/simulation/rules/moduleConfig.js +72 -0
  131. package/src/simulation/rules/penaltyConfig.js +99 -0
  132. package/src/simulation/rules/penaltyEvents.js +18 -0
  133. package/src/simulation/rules/penaltyStats.js +26 -0
  134. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  135. package/src/simulation/rules/ruleDefaults.js +85 -0
  136. package/src/simulation/rules/rulesReview.js +141 -0
  137. package/src/simulation/rules/rulesetPresets.js +47 -0
  138. package/src/simulation/rules/trackLimitsSteward.js +1 -1
  139. package/src/simulation/rulesConfig.js +5 -323
  140. package/src/simulation/snapshots/raceSnapshots.js +192 -0
  141. package/src/simulation/timing/drsTiming.js +87 -0
  142. package/src/simulation/timing/gapEstimation.js +38 -0
  143. package/src/simulation/timing/lapTelemetry.js +182 -0
  144. package/src/simulation/timing/raceTiming.js +15 -0
  145. package/src/simulation/timing/sectorPerformance.js +60 -0
  146. package/src/simulation/timing/timingConstants.js +5 -0
  147. package/src/simulation/timing/timingHistory.js +46 -0
  148. package/src/simulation/timing/timingLines.js +79 -0
  149. package/src/simulation/track/centerlineSpline.js +59 -0
  150. package/src/simulation/track/drsZones.js +67 -0
  151. package/src/simulation/track/pitLaneAccess.js +103 -0
  152. package/src/simulation/track/pitLaneLayout.js +395 -0
  153. package/src/simulation/track/pitLaneState.js +160 -0
  154. package/src/simulation/track/proceduralCenterline.js +237 -0
  155. package/src/simulation/track/sampleGeometry.js +49 -0
  156. package/src/simulation/track/spatialQueries.js +154 -0
  157. package/src/simulation/track/startStraight.js +141 -0
  158. package/src/simulation/track/trackConstants.js +109 -0
  159. package/src/simulation/track/trackGenerationOptions.js +283 -0
  160. package/src/simulation/track/trackMath.js +241 -0
  161. package/src/simulation/track/trackModel.js +240 -0
  162. package/src/simulation/track/trackQueryIndex.js +945 -0
  163. package/src/simulation/track/trackSectors.js +19 -0
  164. package/src/simulation/track/trackStatePolicy.js +24 -0
  165. package/src/simulation/track/trackValidation.js +98 -0
  166. package/src/simulation/trackModel.js +1 -1706
  167. package/src/simulation/vehicle/contactResolution.js +116 -0
  168. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  169. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  170. package/src/simulation/vehicle/runoffResponse.js +88 -0
  171. package/src/simulation/vehicle/surfacePriority.js +28 -0
  172. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  173. package/src/simulation/vehicle/vehiclePhysics.js +363 -0
  174. package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
  175. package/src/simulation/vehicle/vehicleState.js +186 -0
  176. package/src/simulation/vehicle/wheelSurface.js +150 -0
  177. package/src/simulation/vehicleGeometry.js +1 -247
  178. package/src/simulation/vehiclePhysics.js +1 -136
  179. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  180. package/src/simulation/wheelSurface.js +1 -338
  181. package/src/styles.css +42 -0
  182. package/src/ui/cameraControlsTemplate.js +20 -0
  183. package/src/ui/carOverviewTemplate.js +33 -0
  184. package/src/ui/componentTemplates.js +14 -360
  185. package/src/ui/raceCanvasTemplate.js +69 -0
  186. package/src/ui/raceControlsTemplate.js +36 -0
  187. package/src/ui/shellTemplate.js +5 -4
  188. package/src/ui/telemetryDrawerTemplate.js +55 -0
  189. package/src/ui/telemetryTemplates.js +136 -0
  190. package/src/ui/timingTowerTemplate.js +34 -0
@@ -0,0 +1,113 @@
1
+ import { metersToSimUnits } from '../units.js';
2
+ import { firstDifferentCompound } from './pitIntent.js';
3
+
4
+ const PIT_ENTRY_APPROACH_DISTANCE = metersToSimUnits(250);
5
+
6
+ export function normalizePitCrewStats(value) {
7
+ const source = value && typeof value === 'object' ? value : {};
8
+ const rating = (entry) => {
9
+ const numeric = Number(entry);
10
+ return Number.isFinite(numeric) ? Math.max(0, Math.min(1, numeric)) : 0.5;
11
+ };
12
+ return {
13
+ speed: rating(source.speed ?? source.pace),
14
+ consistency: rating(source.consistency),
15
+ reliability: rating(source.reliability),
16
+ };
17
+ }
18
+
19
+ export function assignPitLaneTeams({ cars, pitLane }) {
20
+ if (!pitLane?.enabled || !Array.isArray(pitLane.boxes)) return;
21
+ const boxesPerTeam = pitLane.boxesPerTeam ?? 2;
22
+ const teamCount = pitLane.teamCount ?? Math.ceil(pitLane.boxes.length / boxesPerTeam);
23
+
24
+ pitLane.teams = Array.from({ length: teamCount }, (_, teamIndex) => {
25
+ const primary = cars[teamIndex * boxesPerTeam] ?? cars[teamIndex] ?? null;
26
+ const secondary = cars[teamIndex * boxesPerTeam + 1] ?? null;
27
+ const team = primary?.team ?? secondary?.team ?? null;
28
+ const id = team?.id ?? `team-${teamIndex + 1}`;
29
+ const name = team?.name ?? (primary ? `${primary.name} Team` : `Team ${teamIndex + 1}`);
30
+ const color = team?.color ?? primary?.color ?? secondary?.color ?? '#f8fafc';
31
+ const pitCrew = normalizePitCrewStats(
32
+ team?.pitCrew ?? team?.pitCrewStats ?? primary?.team?.pitCrew ?? secondary?.team?.pitCrew,
33
+ );
34
+ const boxIds = pitLane.boxes
35
+ .filter((box) => box.teamIndex === teamIndex)
36
+ .map((box) => box.id);
37
+
38
+ pitLane.boxes
39
+ .filter((box) => box.teamIndex === teamIndex)
40
+ .forEach((box) => {
41
+ box.teamId = id;
42
+ box.teamName = name;
43
+ box.teamColor = color;
44
+ });
45
+ const serviceArea = pitLane.serviceAreas?.[teamIndex] ?? null;
46
+ if (serviceArea) {
47
+ serviceArea.teamId = id;
48
+ serviceArea.teamName = name;
49
+ serviceArea.teamColor = color;
50
+ serviceArea.pitCrew = pitCrew;
51
+ }
52
+
53
+ return {
54
+ id,
55
+ name,
56
+ color,
57
+ index: teamIndex,
58
+ boxIds,
59
+ serviceAreaId: serviceArea?.id ?? null,
60
+ pitCrew,
61
+ };
62
+ });
63
+ }
64
+
65
+ export function initializePitStops({ cars, pitLane, pitStops, totalLaps, trackLength, tireCompounds, PIT_INTENT_NONE }) {
66
+ if (!pitStops?.enabled || !pitLane?.enabled || !Array.isArray(pitLane.boxes) || totalLaps < 2) return;
67
+ const boxesPerTeam = pitLane.boxesPerTeam ?? 2;
68
+ const maxConcurrentPitLaneCars = Math.max(1, Math.floor(pitStops.maxConcurrentPitLaneCars ?? 3));
69
+ const pitWindowLapCount = Math.max(1, Math.min(
70
+ totalLaps - 1,
71
+ Math.ceil(cars.length / maxConcurrentPitLaneCars),
72
+ ));
73
+
74
+ cars.forEach((car, index) => {
75
+ const stopLapBase = trackLength * (1 + (index % pitWindowLapCount));
76
+ const trainPosition = Math.floor(index / pitWindowLapCount);
77
+ const teamIndex = Math.floor(index / boxesPerTeam);
78
+ const teamBoxIndex = index % boxesPerTeam;
79
+ const garageBoxIndex = Math.min(pitLane.boxes.length - 1, teamIndex * boxesPerTeam + teamBoxIndex);
80
+ const garageBox = pitLane.boxes[garageBoxIndex] ?? pitLane.boxes[index % pitLane.boxes.length];
81
+ const serviceArea = pitLane.serviceAreas?.[teamIndex] ??
82
+ pitLane.serviceAreas?.[index % pitLane.serviceAreas.length] ??
83
+ garageBox;
84
+ car.pitStop = {
85
+ status: 'pending',
86
+ phase: null,
87
+ boxIndex: serviceArea.index,
88
+ boxId: serviceArea.id,
89
+ garageBoxIndex,
90
+ garageBoxId: garageBox.id,
91
+ teamId: serviceArea.teamId ?? garageBox.teamId ?? null,
92
+ teamColor: serviceArea.teamColor ?? garageBox.teamColor ?? null,
93
+ stopsCompleted: 0,
94
+ entryRaceDistance: stopLapBase + pitLane.entry.distanceFromStart,
95
+ plannedRaceDistance: stopLapBase + pitLane.entry.distanceFromStart -
96
+ PIT_ENTRY_APPROACH_DISTANCE,
97
+ trainPosition,
98
+ lapBase: stopLapBase,
99
+ serviceRemaining: 0,
100
+ penaltyServiceRemaining: 0,
101
+ penaltyServiceTotal: 0,
102
+ servingPenaltyIds: [],
103
+ serviceProfile: null,
104
+ queueingForService: false,
105
+ route: null,
106
+ routeProgress: 0,
107
+ routeStartRaceDistance: null,
108
+ routeEndRaceDistance: null,
109
+ targetTire: firstDifferentCompound(car.tire, tireCompounds),
110
+ intent: PIT_INTENT_NONE,
111
+ };
112
+ });
113
+ }
@@ -0,0 +1,92 @@
1
+ import { kphToSimSpeed } from '../units.js';
2
+ import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
3
+ import {
4
+ createRoute,
5
+ pitDriveLaneOffset,
6
+ pitMainLanePointAt,
7
+ routePoint,
8
+ } from './pitRouting.js';
9
+ import { getPitBoxRaceDistance, getPitStopBox } from './pitOccupancy.js';
10
+ import { calculatePitServiceProfile } from './pitServiceProfile.js';
11
+ import { PIT_BOX_APPROACH_DISTANCE, PIT_EXIT_RELEASE_SPEED_KPH } from './pitServiceConstants.js';
12
+
13
+ export function beginTireService(sim, car) {
14
+ const stop = car.pitStop;
15
+ if (!stop) return;
16
+ const serviceProfile = calculatePitServiceProfile(sim, car);
17
+ stop.phase = 'service';
18
+ stop.penaltyServiceRemaining = 0;
19
+ stop.serviceProfile = serviceProfile;
20
+ stop.serviceRemaining = serviceProfile.seconds;
21
+ sim.events.unshift({
22
+ type: 'pit-stop-start',
23
+ at: sim.time,
24
+ carId: car.id,
25
+ boxId: stop.boxId,
26
+ teamId: stop.teamId ?? null,
27
+ targetTire: stop.targetTire,
28
+ serviceSeconds: serviceProfile.seconds,
29
+ serviceIssue: serviceProfile.issue,
30
+ });
31
+ }
32
+
33
+ export function completePitService(sim, car) {
34
+ const stop = car.pitStop;
35
+ const box = getPitStopBox(sim, stop);
36
+ const pitLane = sim.track.pitLane;
37
+ if (!stop || !box || !pitLane?.enabled) return;
38
+
39
+ car.tire = stop.targetTire ?? car.tire;
40
+ car.tireEnergy = 100;
41
+ if (car.tire && !car.usedTireCompounds.includes(car.tire)) car.usedTireCompounds.push(car.tire);
42
+ sim.events.unshift({
43
+ type: 'pit-stop-complete',
44
+ at: sim.time,
45
+ carId: car.id,
46
+ boxId: box.id,
47
+ teamId: box.teamId ?? null,
48
+ tire: car.tire,
49
+ });
50
+
51
+ const driveLaneOffset = pitDriveLaneOffset(pitLane);
52
+ const boxReleaseDistance = Math.min(pitLane.mainLane.length, box.distanceAlongLane + PIT_BOX_APPROACH_DISTANCE);
53
+ const route = createRoute([
54
+ routePoint(box.center, pitLane.mainLane.heading, { limiterActive: true }),
55
+ routePoint(box.laneTarget, pitLane.mainLane.heading, { limiterActive: true }),
56
+ routePoint(pitMainLanePointAt(pitLane, boxReleaseDistance, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
57
+ routePoint(pitMainLanePointAt(pitLane, pitLane.mainLane.length, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
58
+ ...(pitLane.exit.roadCenterline ?? []).map((point) => routePoint(point)),
59
+ ]);
60
+ stop.status = 'exiting';
61
+ stop.phase = 'exit';
62
+ stop.route = route;
63
+ stop.routeProgress = 0;
64
+ stop.routeStartRaceDistance = getPitBoxRaceDistance(sim, stop, box);
65
+ stop.routeEndRaceDistance = stop.lapBase + pitLane.exit.distanceFromStart;
66
+ }
67
+
68
+ export function finishPitExit(sim, car) {
69
+ const stop = car.pitStop;
70
+ if (!stop) return;
71
+ stop.status = 'completed';
72
+ stop.phase = null;
73
+ stop.route = null;
74
+ stop.routeProgress = 0;
75
+ stop.serviceRemaining = 0;
76
+ stop.queueingForService = false;
77
+ stop.stopsCompleted = (stop.stopsCompleted ?? 0) + 1;
78
+ stop.intent = 0;
79
+ car.speed = Math.max(car.speed, kphToSimSpeed(PIT_EXIT_RELEASE_SPEED_KPH));
80
+ car.throttle = 0.55;
81
+ car.brake = 0;
82
+ applyWheelSurfaceState(car, sim.track);
83
+ car.progress = car.trackState.distance;
84
+ car.raceDistance = Math.max(car.raceDistance ?? 0, stop.routeEndRaceDistance ?? car.raceDistance ?? 0);
85
+ sim.events.unshift({
86
+ type: 'pit-exit',
87
+ at: sim.time,
88
+ carId: car.id,
89
+ boxId: stop.boxId,
90
+ teamId: stop.teamId ?? null,
91
+ });
92
+ }
@@ -0,0 +1,104 @@
1
+ import { clamp } from '../simMath.js';
2
+ import { simUnitsToMeters } from '../units.js';
3
+ import { isRaceDnf, raceDnfCars } from './retirements.js';
4
+
5
+ function getPenaltyStats(statsByDriver, driverId) {
6
+ return statsByDriver.get(driverId) ?? {
7
+ seconds: 0,
8
+ positionDrop: 0,
9
+ disqualified: false,
10
+ };
11
+ }
12
+
13
+ export function applyOutstandingServicePenalties(sim, applyUnservedServicePenalty) {
14
+ sim.penalties.forEach((penalty) => {
15
+ const beforeStatus = penalty.status;
16
+ applyUnservedServicePenalty(penalty, sim.time);
17
+ if (beforeStatus !== penalty.status && penalty.unserved) {
18
+ sim.events.unshift({
19
+ type: 'penalty-applied',
20
+ at: sim.time,
21
+ penaltyId: penalty.id,
22
+ driverId: penalty.driverId,
23
+ serviceType: penalty.serviceType,
24
+ penaltySeconds: penalty.penaltySeconds,
25
+ });
26
+ }
27
+ });
28
+ }
29
+
30
+ export function buildClassificationFromFinishOrder(sim) {
31
+ const byId = new Map(sim.cars.map((car) => [car.id, car]));
32
+ const penaltyStats = sim.getPenaltyStatsByDriver();
33
+ const finishOrderIds = new Set(sim.raceControl.finishOrder);
34
+ const orderedByAdjustedTime = sim.raceControl.finishOrder
35
+ .map((id, finishOrderIndex) => ({ car: byId.get(id), finishOrderIndex }))
36
+ .filter((entry) => Boolean(entry.car))
37
+ .sort((left, right) => {
38
+ const leftTime = (left.car.finishTime ?? Infinity) + getPenaltyStats(penaltyStats, left.car.id).seconds;
39
+ const rightTime = (right.car.finishTime ?? Infinity) + getPenaltyStats(penaltyStats, right.car.id).seconds;
40
+ return leftTime === rightTime ? left.finishOrderIndex - right.finishOrderIndex : leftTime - rightTime;
41
+ })
42
+ .map((entry) => entry.car);
43
+ const finishedOrder = applyClassificationConsequences(sim, orderedByAdjustedTime, penaltyStats);
44
+ const dnfOrder = raceDnfCars(sim.cars.filter((car) => (
45
+ !finishOrderIds.has(car.id) && car.interaction?.affectsRaceOrder !== false
46
+ )));
47
+ return buildClassification(sim, [...finishedOrder, ...dnfOrder], penaltyStats);
48
+ }
49
+
50
+ export function applyClassificationConsequences(sim, ordered, penaltyStats = sim.getPenaltyStatsByDriver()) {
51
+ const classified = ordered.filter((car) => !getPenaltyStats(penaltyStats, car.id).disqualified);
52
+ ordered.forEach((car) => {
53
+ const stats = getPenaltyStats(penaltyStats, car.id);
54
+ if (stats.disqualified) return;
55
+ const drop = stats.positionDrop;
56
+ if (drop <= 0) return;
57
+ const currentIndex = classified.findIndex((entry) => entry.id === car.id);
58
+ if (currentIndex < 0) return;
59
+ const [entry] = classified.splice(currentIndex, 1);
60
+ classified.splice(Math.min(classified.length, currentIndex + drop), 0, entry);
61
+ });
62
+ return [
63
+ ...classified,
64
+ ...ordered.filter((car) => getPenaltyStats(penaltyStats, car.id).disqualified),
65
+ ];
66
+ }
67
+
68
+ export function buildClassification(sim, ordered = sim.orderedCars(), penaltyStats = sim.getPenaltyStatsByDriver()) {
69
+ const leaderDistance = ordered[0]?.raceDistance ?? 0;
70
+ return ordered.map((car, index) => {
71
+ const dnf = isRaceDnf(car);
72
+ const finishTime = dnf ? null : (car.finishTime ?? (car.raceDistance >= sim.finishDistance ? sim.time : null));
73
+ const stats = getPenaltyStats(penaltyStats, car.id);
74
+ const penaltySeconds = stats.seconds;
75
+ const positionDrop = stats.positionDrop;
76
+ const disqualified = stats.disqualified;
77
+ return {
78
+ id: car.id,
79
+ code: car.code,
80
+ timingCode: car.timingCode,
81
+ name: car.name,
82
+ rank: index + 1,
83
+ raceDistance: car.raceDistance,
84
+ distanceMeters: simUnitsToMeters(car.raceDistance),
85
+ lap: sim.computeLap(car.raceDistance),
86
+ lapsCompleted: clamp(Math.floor(Math.max(0, car.raceDistance) / sim.track.length), 0, sim.totalLaps),
87
+ gapMeters: simUnitsToMeters(Math.max(0, leaderDistance - car.raceDistance)),
88
+ gapSeconds: index === 0 ? 0 : car.leaderGapSeconds,
89
+ intervalSeconds: index === 0 ? 0 : car.intervalAheadSeconds,
90
+ gapLaps: index === 0 ? 0 : car.leaderGapLaps,
91
+ intervalLaps: index === 0 ? 0 : car.intervalAheadLaps,
92
+ finished: !dnf && Boolean(car.finished || car.raceDistance >= sim.finishDistance),
93
+ finishTime,
94
+ dnf,
95
+ dnfReason: car.dnfReason ?? car.destroyReason ?? null,
96
+ dnfAt: car.dnfAt ?? car.destroyedAt ?? null,
97
+ dnfOrder: car.dnfOrder ?? null,
98
+ penaltySeconds,
99
+ adjustedFinishTime: finishTime == null ? null : finishTime + penaltySeconds,
100
+ positionDrop,
101
+ disqualified,
102
+ };
103
+ });
104
+ }
@@ -0,0 +1,37 @@
1
+ import { pointAt, offsetTrackPoint } from '../track/trackModel.js';
2
+ import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
3
+ import { getStartGridSlot } from '../vehicle/vehicleState.js';
4
+
5
+ export function applyGridDropForSimulation(simulation, driverId, positions) {
6
+ const drop = Math.max(0, Math.floor(Number(positions) || 0));
7
+ if (drop <= 0) return;
8
+
9
+ const ordered = [...simulation.cars].sort((left, right) => {
10
+ const delta = right.gridDistance - left.gridDistance;
11
+ return delta === 0 ? left.index - right.index : delta;
12
+ });
13
+ const currentIndex = ordered.findIndex((car) => car.id === driverId);
14
+ if (currentIndex < 0) return;
15
+ const [car] = ordered.splice(currentIndex, 1);
16
+ ordered.splice(Math.min(ordered.length, currentIndex + drop), 0, car);
17
+
18
+ ordered.forEach((entry, index) => {
19
+ const { gridDistance, gridOffset } = getStartGridSlot(index, { standingStart: true });
20
+ const gridPoint = pointAt(simulation.track, gridDistance);
21
+ const position = offsetTrackPoint(gridPoint, gridOffset);
22
+ entry.gridDistance = gridDistance;
23
+ entry.gridOffset = gridOffset;
24
+ entry.rank = index + 1;
25
+ if (entry.gridLocked) {
26
+ entry.x = position.x;
27
+ entry.y = position.y;
28
+ entry.previousX = position.x;
29
+ entry.previousY = position.y;
30
+ entry.heading = gridPoint.heading;
31
+ entry.previousHeading = gridPoint.heading;
32
+ entry.progress = gridPoint.distance;
33
+ entry.raceDistance = gridDistance;
34
+ applyWheelSurfaceState(entry, simulation.track);
35
+ }
36
+ });
37
+ }
@@ -0,0 +1,38 @@
1
+ import { pointAt } from '../track/trackModel.js';
2
+
3
+ export function createRaceControlState(rules, startLightsOutAt) {
4
+ return {
5
+ mode: rules.standingStart === false ? 'green' : 'pre-start',
6
+ frozenOrder: null,
7
+ redFlag: false,
8
+ pitLaneOpen: true,
9
+ finished: false,
10
+ finishedAt: null,
11
+ winnerId: null,
12
+ classification: [],
13
+ finishOrder: [],
14
+ nextDnfOrder: 1,
15
+ start: {
16
+ lightCount: rules.startLightCount,
17
+ lightsLit: 0,
18
+ lightsOutAt: startLightsOutAt,
19
+ released: rules.standingStart === false,
20
+ releasedAt: rules.standingStart === false ? 0 : null,
21
+ },
22
+ };
23
+ }
24
+
25
+ export function createSafetyCarState(track, rules) {
26
+ const safetyCarStart = pointAt(track, rules.safetyCarLeadDistance);
27
+ return {
28
+ deployed: false,
29
+ progress: rules.safetyCarLeadDistance,
30
+ speed: rules.safetyCarSpeed,
31
+ previousX: safetyCarStart.x,
32
+ previousY: safetyCarStart.y,
33
+ previousHeading: safetyCarStart.heading,
34
+ x: safetyCarStart.x,
35
+ y: safetyCarStart.y,
36
+ heading: safetyCarStart.heading,
37
+ };
38
+ }
@@ -0,0 +1,32 @@
1
+ import { clamp, wrapDistance } from '../simMath.js';
2
+
3
+ const MIN_TOTAL_LAPS = 1;
4
+
5
+ export function normalizeTotalLaps(value, { minimum = MIN_TOTAL_LAPS } = {}) {
6
+ const numeric = Number(value);
7
+ if (!Number.isFinite(numeric)) return minimum;
8
+ return Math.max(minimum, Math.floor(numeric));
9
+ }
10
+
11
+ export function wrapProgress(value, length) {
12
+ return wrapDistance(value, length);
13
+ }
14
+
15
+ export function distanceForward(from, to, length) {
16
+ return wrapProgress(to - from, length);
17
+ }
18
+
19
+ export function progressDelta(a, b, trackLength) {
20
+ let delta = a - b;
21
+ if (delta < -trackLength / 2) delta += trackLength;
22
+ if (delta > trackLength / 2) delta -= trackLength;
23
+ return delta;
24
+ }
25
+
26
+ export function computeLapForDistance(raceDistance, trackLength, totalLaps) {
27
+ return clamp(Math.floor(Math.max(0, raceDistance) / trackLength) + 1, 1, totalLaps);
28
+ }
29
+
30
+ export function finishDistanceForRace(trackLength, totalLaps) {
31
+ return trackLength * totalLaps;
32
+ }
@@ -0,0 +1,65 @@
1
+ import { isRaceDnf } from './retirements.js';
2
+
3
+ export function evaluateRaceFinishForSimulation(sim) {
4
+ if (sim.raceControl.finished) return;
5
+ if (sim.raceControl.mode === 'pre-start') return;
6
+
7
+ const ordered = sim.orderedCars();
8
+ const newlyFinished = ordered.filter((car) => !isRaceDnf(car) && !car.finished && car.raceDistance >= sim.finishDistance);
9
+
10
+ newlyFinished.forEach((car) => {
11
+ car.finished = true;
12
+ car.finishTime = sim.time;
13
+ car.finishRank = sim.raceControl.finishOrder.length + 1;
14
+ car.classifiedRank = car.finishRank;
15
+ sim.raceControl.finishOrder.push(car.id);
16
+ if (!sim.raceControl.winnerId) sim.raceControl.winnerId = car.id;
17
+ car.drsActive = false;
18
+ car.drsEligible = false;
19
+ car.drsZoneId = null;
20
+ car.drsZoneEnabled = false;
21
+ car.canAttack = false;
22
+ sim.events.unshift({
23
+ type: 'car-finish',
24
+ at: sim.time,
25
+ carId: car.id,
26
+ rank: car.finishRank,
27
+ winnerId: sim.raceControl.winnerId,
28
+ });
29
+ sim.reviewTireRequirement(car);
30
+ });
31
+
32
+ if (!ordered.length || !ordered.every((car) => car.finished || isRaceDnf(car))) return;
33
+
34
+ sim.applyOutstandingServicePenalties();
35
+ const classification = sim.buildClassificationFromFinishOrder();
36
+ const winningClassification = classification.find((entry) => !entry.dnf && entry.finished);
37
+ sim.raceControl.winnerId = winningClassification?.id ?? null;
38
+ sim.raceControl.mode = 'safety-car';
39
+ sim.raceControl.finished = true;
40
+ sim.raceControl.finishedAt = sim.time;
41
+ sim.raceControl.classification = classification;
42
+ sim.raceControl.frozenOrder = classification.map((entry) => entry.id);
43
+ 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;
46
+ if (sim.safetyCar.progress < safetyCarProgress) {
47
+ sim.moveSafetyCarTo(safetyCarProgress);
48
+ }
49
+ sim.cars.forEach((car) => {
50
+ const classified = classification.find((entry) => entry.id === car.id);
51
+ car.classifiedRank = classified?.rank ?? null;
52
+ car.desiredOffset = 0;
53
+ car.drsActive = false;
54
+ car.drsEligible = false;
55
+ car.drsZoneId = null;
56
+ car.drsZoneEnabled = false;
57
+ car.canAttack = false;
58
+ });
59
+ sim.events.unshift({
60
+ type: 'race-finish',
61
+ at: sim.time,
62
+ winnerId: sim.raceControl.winnerId,
63
+ classification: classification.map((entry) => ({ id: entry.id, rank: entry.rank })),
64
+ });
65
+ }
@@ -0,0 +1,71 @@
1
+ import { offsetTrackPoint, pointAt } from '../track/trackModel.js';
2
+ import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
3
+ import { clamp } from '../simMath.js';
4
+ import { nearestTrackStateForCar } from '../track/trackStatePolicy.js';
5
+
6
+ export function setPitLaneOpenState(sim, open) {
7
+ const next = Boolean(open);
8
+ if (next === Boolean(sim.raceControl.pitLaneOpen)) return;
9
+ sim.raceControl.pitLaneOpen = next;
10
+ sim.events.unshift({ type: next ? 'pit-lane-open' : 'pit-lane-closed', at: sim.time });
11
+ }
12
+
13
+ export function updateStartSequence(sim) {
14
+ const start = sim.raceControl.start;
15
+ if (sim.raceControl.mode !== 'pre-start' || !start || start.released) return;
16
+
17
+ if (sim.time >= start.lightsOutAt) {
18
+ releaseRaceStart(sim);
19
+ sim.events.unshift({ type: 'start-lights-out', at: sim.time });
20
+ return;
21
+ }
22
+
23
+ start.lightsLit = clamp(
24
+ Math.floor((sim.time + Number.EPSILON) / sim.rules.startLightInterval),
25
+ 0,
26
+ start.lightCount,
27
+ );
28
+ }
29
+
30
+ export function releaseRaceStart(sim) {
31
+ const start = sim.raceControl.start;
32
+ sim.raceControl.mode = 'green';
33
+ start.lightsLit = 0;
34
+ start.released = true;
35
+ start.releasedAt = sim.time;
36
+ sim.cars.forEach((car) => {
37
+ const wasGridLocked = car.gridLocked;
38
+ car.gridLocked = false;
39
+ const state = nearestTrackStateForCar(sim.track, car, car, car.gridDistance);
40
+ car.progress = state.distance;
41
+ applyWheelSurfaceState(car, sim.track);
42
+ if (wasGridLocked) car.raceDistance = car.gridDistance;
43
+ if (wasGridLocked) car.desiredOffset = 0;
44
+ car.previousX = car.x;
45
+ car.previousY = car.y;
46
+ car.previousHeading = car.heading;
47
+ });
48
+ }
49
+
50
+ export function holdGridCars(sim) {
51
+ sim.cars.forEach((car) => {
52
+ if (!car.gridLocked) return;
53
+ const gridPoint = pointAt(sim.track, car.gridDistance);
54
+ const position = offsetTrackPoint(gridPoint, car.gridOffset);
55
+ car.previousX = position.x;
56
+ car.previousY = position.y;
57
+ car.previousHeading = gridPoint.heading;
58
+ car.x = position.x;
59
+ car.y = position.y;
60
+ car.heading = gridPoint.heading;
61
+ car.speed = 0;
62
+ car.throttle = 0;
63
+ car.brake = 1;
64
+ car.steeringAngle = 0;
65
+ car.yawRate = 0;
66
+ car.turnRadius = Infinity;
67
+ car.progress = gridPoint.distance;
68
+ car.raceDistance = car.gridDistance;
69
+ applyWheelSurfaceState(car, sim.track);
70
+ });
71
+ }
@@ -0,0 +1,113 @@
1
+ import { clamp } from '../simMath.js';
2
+ import { affectsRaceOrder } from '../participants/participantInteractions.js';
3
+ import { distanceForward } from './raceDistance.js';
4
+ import { isRaceDnf, raceDnfCars } from './retirements.js';
5
+
6
+ function canAppearInRaceOrder(car) {
7
+ return car?.interaction?.affectsRaceOrder !== false;
8
+ }
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) => {
14
+ const delta = b.raceDistance - a.raceDistance;
15
+ return delta === 0 ? a.index - b.index : delta;
16
+ });
17
+ const byId = new Map(sim.cars.map((car) => [car.id, car]));
18
+
19
+ if (sim.raceControl.finished && sim.raceControl.classification?.length) {
20
+ const classified = sim.raceControl.classification.map((entry) => byId.get(entry.id)).filter(Boolean);
21
+ const classifiedIds = new Set(classified.map((car) => car.id));
22
+ return [
23
+ ...classified,
24
+ ...sortLive(raceCars.filter((car) => !classifiedIds.has(car.id))),
25
+ ...dnfCars.filter((car) => !classifiedIds.has(car.id)),
26
+ ];
27
+ }
28
+
29
+ if (sim.raceControl.finishOrder?.length) {
30
+ const finished = sim.raceControl.finishOrder.map((id) => byId.get(id)).filter(Boolean);
31
+ 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))
34
+ .sort((a, b) => {
35
+ const delta = (a.finishRank ?? Infinity) - (b.finishRank ?? Infinity);
36
+ return delta === 0 ? a.index - b.index : delta;
37
+ });
38
+ const running = sortLive(raceCars.filter((car) => !finishedIds.has(car.id) && !car.finished && !isRaceDnf(car)));
39
+ return [...finished, ...missedFinished, ...running, ...dnfCars.filter((car) => !finishedIds.has(car.id))];
40
+ }
41
+
42
+ if (sim.safetyCar.deployed && sim.raceControl.frozenOrder?.length) {
43
+ const frozen = sim.raceControl.frozenOrder.map((id) => byId.get(id)).filter(Boolean);
44
+ const activeFrozen = frozen.filter((car) => !isRaceDnf(car));
45
+ const activeFrozenIds = new Set(activeFrozen.map((car) => car.id));
46
+ return [
47
+ ...activeFrozen,
48
+ ...dnfCars.filter((car) => !activeFrozenIds.has(car.id)),
49
+ ];
50
+ }
51
+
52
+ return [...sortLive(raceCars.filter((car) => !isRaceDnf(car))), ...dnfCars];
53
+ }
54
+
55
+ export function raceOrderCarsForSimulation(sim) {
56
+ return sim.cars.filter((car) => affectsRaceOrder(car) && !isRaceDnf(car));
57
+ }
58
+
59
+ export function driverRaceContextForSimulation(sim, orderedCars = orderedCarsForSimulation(sim)) {
60
+ const raceCars = raceOrderCarsForSimulation(sim);
61
+ return {
62
+ physicsMode: sim.physicsMode,
63
+ track: sim.track,
64
+ cars: raceCars,
65
+ orderedCars,
66
+ safetyCar: sim.safetyCar,
67
+ rules: sim.rules,
68
+ };
69
+ }
70
+
71
+ export function computeAggressionForSimulation(sim, car, orderIndex = Math.max(0, (car.rank ?? 1) - 1)) {
72
+ const personality = car.personality ?? { baseAggression: 0.5, riskTolerance: 0.5, patience: 0.5 };
73
+ if (sim.safetyCar.deployed || car.canAttack === false) {
74
+ return clamp(personality.baseAggression * 0.62, 0.08, 0.62);
75
+ }
76
+
77
+ const fieldDepth = Math.max(1, raceOrderCarsForSimulation(sim).length - 1);
78
+ const positionPressure = clamp(orderIndex / fieldDepth, 0, 1);
79
+ const gapPressure = Number.isFinite(car.gapAhead) ? clamp((230 - car.gapAhead) / 230, 0, 1) : 0;
80
+ const tireConfidence = clamp(((car.tireEnergy ?? 100) - 42) / 58, 0, 1);
81
+ const patienceDamping = (1 - gapPressure) * personality.patience * 0.08;
82
+
83
+ return clamp(
84
+ personality.baseAggression
85
+ + positionPressure * 0.26
86
+ + gapPressure * (0.1 + personality.riskTolerance * 0.08)
87
+ - (1 - tireConfidence) * 0.1
88
+ - patienceDamping,
89
+ 0.08,
90
+ 1,
91
+ );
92
+ }
93
+
94
+ 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;
113
+ }