@inventure71/paddockjs 0.3.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 (202) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/INSTALL_AND_UPDATE.md +5 -2
  3. package/README.md +328 -13
  4. package/docs/architecture.md +150 -41
  5. package/docs/concepts.md +22 -3
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +474 -34
  8. package/docs/data_model.md +1 -1
  9. package/docs/index.md +1 -0
  10. package/docs/learnings.md +23 -1
  11. package/docs/rules.md +169 -21
  12. package/docs/system_specs.md +56 -20
  13. package/docs/training.md +369 -25
  14. package/examples/python/README.md +62 -0
  15. package/examples/python/base_policy_server.py +191 -0
  16. package/examples/python/requirements.txt +2 -0
  17. package/examples/train-basic-policy.mjs +5 -6
  18. package/examples/trainingData.mjs +133 -0
  19. package/package.json +15 -4
  20. package/src/api/PaddockSimulatorController.js +36 -2
  21. package/src/app/BrowserExpertAdapter.js +367 -9
  22. package/src/app/F1SimulatorApp.js +615 -940
  23. package/src/app/banners/bannerFormatters.js +55 -0
  24. package/src/app/banners/raceDataBannerController.js +141 -0
  25. package/src/app/banners/stewardMessageController.js +99 -0
  26. package/src/app/camera/cameraConstants.js +21 -0
  27. package/src/app/camera/cameraController.js +265 -0
  28. package/src/app/camera/cameraGeometry.js +176 -0
  29. package/src/app/domBindings.js +14 -1
  30. package/src/app/readouts/carOverviewRenderer.js +109 -0
  31. package/src/app/readouts/controlStateRenderer.js +37 -0
  32. package/src/app/readouts/raceStatusRenderer.js +133 -0
  33. package/src/app/readouts/readoutFormatters.js +58 -0
  34. package/src/app/readouts/telemetryRenderer.js +113 -0
  35. package/src/app/readouts/timingTowerRenderer.js +177 -0
  36. package/src/app/rendering/appAssets.js +38 -0
  37. package/src/app/rendering/carRenderer.js +286 -0
  38. package/src/app/rendering/displayUtils.js +22 -0
  39. package/src/app/rendering/drsTrailRenderer.js +90 -0
  40. package/src/app/rendering/expertSensorRenderer.js +126 -0
  41. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  42. package/src/app/rendering/replayGhostRenderer.js +124 -0
  43. package/src/app/rendering/trackRenderer.js +25 -0
  44. package/src/app/runtime/frameLoop.js +66 -0
  45. package/src/app/runtime/runtimeTiming.js +34 -0
  46. package/src/app/runtime/runtimeVisibility.js +45 -0
  47. package/src/config/defaultOptions.js +24 -4
  48. package/src/environment/actions.js +38 -7
  49. package/src/environment/controllerLoop.js +246 -0
  50. package/src/environment/episode.js +86 -2
  51. package/src/environment/evaluation.js +206 -0
  52. package/src/environment/index.d.ts +638 -6
  53. package/src/environment/index.js +14 -0
  54. package/src/environment/metrics.js +159 -0
  55. package/src/environment/observationOptions.js +33 -0
  56. package/src/environment/observations.js +500 -43
  57. package/src/environment/options.js +41 -2
  58. package/src/environment/recorder.js +28 -0
  59. package/src/environment/runtime.js +260 -24
  60. package/src/environment/scenarios.js +249 -0
  61. package/src/environment/sensors/bodySenses.js +23 -0
  62. package/src/environment/sensors/boundarySenses.js +17 -0
  63. package/src/environment/sensors/carRays.js +75 -0
  64. package/src/environment/sensors/contactSenses.js +32 -0
  65. package/src/environment/sensors/index.js +157 -0
  66. package/src/environment/sensors/indexedRayBands.js +103 -0
  67. package/src/environment/sensors/nearbyCars.js +52 -0
  68. package/src/environment/sensors/opponentRadar.js +65 -0
  69. package/src/environment/sensors/rayConfig.js +110 -0
  70. package/src/environment/sensors/rayDefaults.js +6 -0
  71. package/src/environment/sensors/rayGeometry.js +60 -0
  72. package/src/environment/sensors/rayGuards.js +19 -0
  73. package/src/environment/sensors/sensorTargets.js +60 -0
  74. package/src/environment/sensors/surfaceRays.js +296 -0
  75. package/src/environment/sensors/trackRays.js +261 -0
  76. package/src/environment/sensors.js +1 -257
  77. package/src/environment/specs.js +188 -9
  78. package/src/environment/workerProtocol.js +64 -0
  79. package/src/index.d.ts +533 -7
  80. package/src/index.js +26 -0
  81. package/src/rendering/proceduralTrackAsset.js +20 -510
  82. package/src/rendering/renderSnapshot.js +38 -12
  83. package/src/rendering/track/finishLineRenderer.js +34 -0
  84. package/src/rendering/track/gridRenderer.js +47 -0
  85. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  86. package/src/rendering/track/pitLaneRenderer.js +248 -0
  87. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  88. package/src/rendering/track/trackRenderConstants.js +38 -0
  89. package/src/rendering/track/trackRenderGeometry.js +94 -0
  90. package/src/simulation/collisionGeometry.js +214 -0
  91. package/src/simulation/driver/driverControlConstants.js +28 -0
  92. package/src/simulation/driver/driverController.js +47 -0
  93. package/src/simulation/driver/driverInput.js +28 -0
  94. package/src/simulation/driver/driverMath.js +6 -0
  95. package/src/simulation/driver/driverPersonality.js +25 -0
  96. package/src/simulation/driver/edgeRecovery.js +48 -0
  97. package/src/simulation/driver/racingControls.js +399 -0
  98. package/src/simulation/driver/racingLinePlan.js +166 -0
  99. package/src/simulation/driver/recoveryDynamics.js +38 -0
  100. package/src/simulation/driver/rejoinControls.js +213 -0
  101. package/src/simulation/driver/safetyCarControls.js +35 -0
  102. package/src/simulation/driver/trafficScan.js +51 -0
  103. package/src/simulation/driverController.js +4 -253
  104. package/src/simulation/participants/participantInteractions.js +120 -0
  105. package/src/simulation/pit/pitFlow.js +234 -0
  106. package/src/simulation/pit/pitIntent.js +92 -0
  107. package/src/simulation/pit/pitOccupancy.js +47 -0
  108. package/src/simulation/pit/pitPenaltyService.js +71 -0
  109. package/src/simulation/pit/pitQueue.js +55 -0
  110. package/src/simulation/pit/pitRouteMovement.js +160 -0
  111. package/src/simulation/pit/pitRouting.js +319 -0
  112. package/src/simulation/pit/pitService.js +41 -0
  113. package/src/simulation/pit/pitServiceConstants.js +21 -0
  114. package/src/simulation/pit/pitServiceProfile.js +45 -0
  115. package/src/simulation/pit/pitSnapshots.js +61 -0
  116. package/src/simulation/pit/pitState.js +113 -0
  117. package/src/simulation/pit/pitTireService.js +92 -0
  118. package/src/simulation/race/classification.js +104 -0
  119. package/src/simulation/race/gridPenalties.js +37 -0
  120. package/src/simulation/race/raceControlState.js +38 -0
  121. package/src/simulation/race/raceDistance.js +32 -0
  122. package/src/simulation/race/raceFinish.js +65 -0
  123. package/src/simulation/race/raceLifecycle.js +71 -0
  124. package/src/simulation/race/raceOrder.js +113 -0
  125. package/src/simulation/race/raceProgress.js +124 -0
  126. package/src/simulation/race/raceSetup.js +93 -0
  127. package/src/simulation/race/raceStep.js +80 -0
  128. package/src/simulation/race/redFlag.js +15 -0
  129. package/src/simulation/race/retirements.js +44 -0
  130. package/src/simulation/race/safetyCar.js +86 -0
  131. package/src/simulation/race/stalledDnf.js +86 -0
  132. package/src/simulation/raceSimulation.js +386 -1012
  133. package/src/simulation/replay/replayGhosts.js +128 -0
  134. package/src/simulation/rules/collisionSteward.js +69 -0
  135. package/src/simulation/rules/moduleConfig.js +72 -0
  136. package/src/simulation/rules/penaltyConfig.js +99 -0
  137. package/src/simulation/rules/penaltyEvents.js +18 -0
  138. package/src/simulation/rules/penaltyLedger.js +153 -0
  139. package/src/simulation/rules/penaltyStats.js +26 -0
  140. package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
  141. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  142. package/src/simulation/rules/ruleDefaults.js +85 -0
  143. package/src/simulation/rules/rulesReview.js +141 -0
  144. package/src/simulation/rules/rulesetPresets.js +47 -0
  145. package/src/simulation/rules/tireRequirementSteward.js +29 -0
  146. package/src/simulation/rules/trackLimitsSteward.js +85 -0
  147. package/src/simulation/rulesConfig.js +34 -0
  148. package/src/simulation/snapshots/raceSnapshots.js +192 -0
  149. package/src/simulation/timing/drsTiming.js +87 -0
  150. package/src/simulation/timing/gapEstimation.js +38 -0
  151. package/src/simulation/timing/lapTelemetry.js +182 -0
  152. package/src/simulation/timing/raceTiming.js +15 -0
  153. package/src/simulation/timing/sectorPerformance.js +60 -0
  154. package/src/simulation/timing/timingConstants.js +5 -0
  155. package/src/simulation/timing/timingHistory.js +46 -0
  156. package/src/simulation/timing/timingLines.js +79 -0
  157. package/src/simulation/track/centerlineSpline.js +59 -0
  158. package/src/simulation/track/drsZones.js +67 -0
  159. package/src/simulation/track/pitLaneAccess.js +103 -0
  160. package/src/simulation/track/pitLaneLayout.js +395 -0
  161. package/src/simulation/track/pitLaneState.js +160 -0
  162. package/src/simulation/track/proceduralCenterline.js +237 -0
  163. package/src/simulation/track/sampleGeometry.js +49 -0
  164. package/src/simulation/track/spatialQueries.js +154 -0
  165. package/src/simulation/track/startStraight.js +141 -0
  166. package/src/simulation/track/trackConstants.js +109 -0
  167. package/src/simulation/track/trackGenerationOptions.js +283 -0
  168. package/src/simulation/track/trackMath.js +241 -0
  169. package/src/simulation/track/trackModel.js +240 -0
  170. package/src/simulation/track/trackQueryIndex.js +945 -0
  171. package/src/simulation/track/trackSectors.js +19 -0
  172. package/src/simulation/track/trackStatePolicy.js +24 -0
  173. package/src/simulation/track/trackValidation.js +98 -0
  174. package/src/simulation/trackModel.js +1 -628
  175. package/src/simulation/units.js +6 -4
  176. package/src/simulation/vehicle/contactResolution.js +116 -0
  177. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  178. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  179. package/src/simulation/vehicle/runoffResponse.js +88 -0
  180. package/src/simulation/vehicle/surfacePriority.js +28 -0
  181. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  182. package/src/simulation/vehicle/vehiclePhysics.js +363 -0
  183. package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
  184. package/src/simulation/vehicle/vehicleState.js +186 -0
  185. package/src/simulation/vehicle/wheelSurface.js +150 -0
  186. package/src/simulation/vehicleGeometry.js +1 -0
  187. package/src/simulation/vehiclePhysics.js +1 -104
  188. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  189. package/src/simulation/wheelSurface.js +1 -0
  190. package/src/styles.css +342 -42
  191. package/src/ui/bannerTemplates.js +69 -0
  192. package/src/ui/cameraControlsTemplate.js +20 -0
  193. package/src/ui/carOverviewTemplate.js +33 -0
  194. package/src/ui/componentTemplates.js +20 -426
  195. package/src/ui/raceCanvasTemplate.js +69 -0
  196. package/src/ui/raceControlStatusBanner.js +39 -0
  197. package/src/ui/raceControlsTemplate.js +36 -0
  198. package/src/ui/shellTemplate.js +6 -5
  199. package/src/ui/telemetryDrawerTemplate.js +55 -0
  200. package/src/ui/telemetryTemplates.js +136 -0
  201. package/src/ui/templateUtils.js +23 -0
  202. package/src/ui/timingTowerTemplate.js +34 -0
@@ -0,0 +1,21 @@
1
+ import { kphToSimSpeed, metersToSimUnits } from '../units.js';
2
+ import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
3
+
4
+ export const PIT_ROUTE_FINISH_DISTANCE = metersToSimUnits(8.5);
5
+ export const PIT_QUEUE_RELEASE_FINISH_DISTANCE = metersToSimUnits(4);
6
+ export const PIT_QUEUE_CAPTURE_DISTANCE = metersToSimUnits(2.5);
7
+ export const PIT_ENTRY_BOX_CAPTURE_DISTANCE = metersToSimUnits(20);
8
+ export const PIT_BOX_STOP_SPEED = kphToSimSpeed(35);
9
+ export const PIT_QUEUE_CAPTURE_SPEED = kphToSimSpeed(140);
10
+ export const PIT_QUEUE_RELEASE_SPEED = kphToSimSpeed(30);
11
+ export const PIT_EXIT_RELEASE_SPEED_KPH = 95;
12
+ export const PIT_BOX_APPROACH_DISTANCE = metersToSimUnits(34);
13
+ export const PIT_LIMITER_BRAKE_DISTANCE = metersToSimUnits(295);
14
+ export const PIT_LIMITER_APPROACH_SPEED_SLOPE = 0.045;
15
+ export const PIT_ENTRY_CONNECTOR_OVERSPEED_KPH = 75;
16
+ export const PIT_SERVICE_CLEAR_DISTANCE = VEHICLE_LIMITS.carLength * 0.9;
17
+
18
+ export function pointDistance(first, second) {
19
+ if (!first || !second) return Infinity;
20
+ return Math.hypot(first.x - second.x, first.y - second.y);
21
+ }
@@ -0,0 +1,45 @@
1
+ import { clamp } from '../simMath.js';
2
+ import { normalizePitCrewStats } from './pitState.js';
3
+ import { getPitStopBox } from './pitOccupancy.js';
4
+
5
+ export function calculatePitServiceProfile(sim, car) {
6
+ const pitStops = sim.rules.modules?.pitStops ?? {};
7
+ const variability = pitStops.variability ?? {};
8
+ const baseSeconds = Math.max(0, Number(pitStops.defaultStopSeconds) || 2.8);
9
+ const box = getPitStopBox(sim, car?.pitStop);
10
+ const team = sim.track.pitLane?.teams?.find((entry) => entry.id === car?.pitStop?.teamId);
11
+ const pitCrew = normalizePitCrewStats(
12
+ box?.pitCrew ?? team?.pitCrew ?? car?.team?.pitCrew ?? car?.team?.pitCrewStats,
13
+ );
14
+ const profile = {
15
+ baseSeconds,
16
+ seconds: baseSeconds,
17
+ perfect: Boolean(variability.perfect),
18
+ variabilityEnabled: Boolean(variability.enabled),
19
+ teamId: car?.pitStop?.teamId ?? box?.teamId ?? null,
20
+ pitCrew,
21
+ speedDeltaSeconds: 0,
22
+ consistencyDeltaSeconds: 0,
23
+ issueDelaySeconds: 0,
24
+ issue: null,
25
+ };
26
+
27
+ if (!variability.enabled || variability.perfect) return profile;
28
+
29
+ const speedImpact = Math.max(0, Number(variability.speedImpactSeconds) || 0);
30
+ const jitterImpact = Math.max(0, Number(variability.consistencyJitterSeconds) || 0);
31
+ const issueChance = clamp(Number(variability.issueChance) || 0, 0, 1);
32
+ const issueMaxDelay = Math.max(0, Number(variability.issueMaxDelaySeconds) || 0);
33
+ profile.speedDeltaSeconds = (0.5 - pitCrew.speed) * speedImpact;
34
+ profile.consistencyDeltaSeconds = (sim.random() - 0.5) * (1 - pitCrew.consistency) * jitterImpact;
35
+ const effectiveIssueChance = issueChance * (1 - pitCrew.reliability);
36
+ if (sim.random() < effectiveIssueChance) {
37
+ profile.issueDelaySeconds = 0.35 + sim.random() * issueMaxDelay * (1 - pitCrew.reliability);
38
+ profile.issue = 'slow-stop';
39
+ }
40
+ profile.seconds = Math.max(
41
+ Math.min(1.6, baseSeconds),
42
+ baseSeconds + profile.speedDeltaSeconds + profile.consistencyDeltaSeconds + profile.issueDelaySeconds,
43
+ );
44
+ return profile;
45
+ }
@@ -0,0 +1,61 @@
1
+ import { finiteOrNull } from '../vehicle/vehicleSnapshots.js';
2
+
3
+ export function pitLaneStatusSnapshot(raceControl, pitLane, pitStops) {
4
+ const enabled = Boolean(pitLane?.enabled && pitStops?.enabled);
5
+ const redFlag = Boolean(raceControl.redFlag);
6
+ const open = enabled && Boolean(raceControl.pitLaneOpen) && !redFlag;
7
+ return {
8
+ enabled,
9
+ open,
10
+ reason: !enabled ? 'unavailable' : redFlag ? 'red-flag' : open ? 'open' : 'closed',
11
+ color: open ? 'green' : redFlag ? 'yellow' : 'red',
12
+ light: open ? '#22c55e' : redFlag ? '#facc15' : '#ef4444',
13
+ };
14
+ }
15
+
16
+ export function serializePitStop(pitStop, normalizePitIntent, PIT_INTENT_NONE) {
17
+ if (!pitStop) return null;
18
+ return {
19
+ status: pitStop.status,
20
+ intent: normalizePitIntent(pitStop.intent) ?? PIT_INTENT_NONE,
21
+ phase: pitStop.phase ?? null,
22
+ boxIndex: pitStop.boxIndex,
23
+ boxId: pitStop.boxId,
24
+ garageBoxIndex: pitStop.garageBoxIndex ?? null,
25
+ garageBoxId: pitStop.garageBoxId ?? null,
26
+ teamId: pitStop.teamId ?? null,
27
+ teamColor: pitStop.teamColor ?? null,
28
+ stopsCompleted: pitStop.stopsCompleted ?? 0,
29
+ queueingForService: Boolean(pitStop.queueingForService),
30
+ plannedRaceDistance: finiteOrNull(pitStop.plannedRaceDistance),
31
+ entryRaceDistance: finiteOrNull(pitStop.entryRaceDistance),
32
+ serviceRemainingSeconds: finiteOrNull(pitStop.serviceRemaining),
33
+ penaltyServiceRemainingSeconds: finiteOrNull(pitStop.penaltyServiceRemaining),
34
+ penaltyServiceTotalSeconds: finiteOrNull(pitStop.penaltyServiceTotal),
35
+ servingPenaltyIds: [...(pitStop.servingPenaltyIds ?? [])],
36
+ targetTire: pitStop.targetTire ?? null,
37
+ serviceProfile: pitStop.serviceProfile ? { ...pitStop.serviceProfile } : null,
38
+ };
39
+ }
40
+
41
+ export function serializeRenderPitStop(pitStop) {
42
+ if (!pitStop) return null;
43
+ return {
44
+ phase: pitStop.phase ?? null,
45
+ serviceRemainingSeconds: finiteOrNull(pitStop.serviceRemaining),
46
+ penaltyServiceRemainingSeconds: finiteOrNull(pitStop.penaltyServiceRemaining),
47
+ };
48
+ }
49
+
50
+ export function serializeObservationPitStop(pitStop, normalizePitIntent, PIT_INTENT_NONE) {
51
+ if (!pitStop) return null;
52
+ return {
53
+ status: pitStop.status,
54
+ intent: normalizePitIntent(pitStop.intent) ?? PIT_INTENT_NONE,
55
+ phase: pitStop.phase ?? null,
56
+ targetTire: pitStop.targetTire ?? null,
57
+ serviceRemainingSeconds: finiteOrNull(pitStop.serviceRemaining),
58
+ penaltyServiceRemainingSeconds: finiteOrNull(pitStop.penaltyServiceRemaining),
59
+ stopsCompleted: pitStop.stopsCompleted ?? 0,
60
+ };
61
+ }
@@ -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
+ }