@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,85 @@
1
+ import { kphToSimSpeed, metersToSimUnits } from '../units.js';
2
+
3
+ export const DEFAULT_RULES = {
4
+ drsDetectionSeconds: 1,
5
+ safetyCarSpeed: kphToSimSpeed(166),
6
+ safetyCarLeadDistance: metersToSimUnits(55),
7
+ safetyCarGap: metersToSimUnits(22),
8
+ collisionRestitution: 0.18,
9
+ standingStart: true,
10
+ startLightCount: 5,
11
+ startLightInterval: 0.72,
12
+ startLightsOutHold: 0.78,
13
+ };
14
+
15
+ export const DEFAULT_MODULES = {
16
+ pitStops: {
17
+ enabled: false,
18
+ pitLaneSpeedLimitKph: 80,
19
+ defaultStopSeconds: 2.8,
20
+ variability: {
21
+ enabled: false,
22
+ perfect: false,
23
+ speedImpactSeconds: 1.2,
24
+ consistencyJitterSeconds: 1.4,
25
+ issueChance: 0.12,
26
+ issueMaxDelaySeconds: 4,
27
+ },
28
+ maxConcurrentPitLaneCars: 3,
29
+ minimumPitLaneGapMeters: 20,
30
+ doubleStacking: false,
31
+ tirePitRequestThresholdPercent: 50,
32
+ tirePitCommitThresholdPercent: 30,
33
+ },
34
+ tireStrategy: {
35
+ enabled: false,
36
+ compounds: ['S', 'M', 'H'],
37
+ mandatoryDistinctDryCompounds: null,
38
+ },
39
+ tireDegradation: {
40
+ enabled: true,
41
+ },
42
+ stalledDnf: {
43
+ enabled: false,
44
+ maxStoppedSeconds: 12,
45
+ speedThresholdKph: 5,
46
+ },
47
+ penalties: {
48
+ enabled: false,
49
+ stewardStrictness: 1,
50
+ trackLimits: {
51
+ strictness: 0,
52
+ warningsBeforePenalty: 3,
53
+ timePenaltySeconds: 5,
54
+ relaxedMarginMeters: 3,
55
+ },
56
+ collision: {
57
+ strictness: 0,
58
+ timePenaltySeconds: 5,
59
+ minimumSeverity: 2,
60
+ relaxedSeverityMargin: 6,
61
+ minimumImpactSpeedKph: 20,
62
+ relaxedImpactSpeedKph: 20,
63
+ },
64
+ tireRequirement: {
65
+ strictness: 0,
66
+ timePenaltySeconds: 10,
67
+ },
68
+ pitLaneSpeeding: {
69
+ strictness: 0,
70
+ speedLimitKph: 80,
71
+ marginKph: 0.5,
72
+ relaxedMarginKph: 5,
73
+ timePenaltySeconds: 5,
74
+ },
75
+ },
76
+ weather: {
77
+ enabled: false,
78
+ },
79
+ reliability: {
80
+ enabled: false,
81
+ },
82
+ fuelLoad: {
83
+ enabled: false,
84
+ },
85
+ };
@@ -0,0 +1,141 @@
1
+ import { calculateCollisionPenalties } from './collisionSteward.js';
2
+ import { calculateTireRequirementPenalty } from './tireRequirementSteward.js';
3
+ import { calculateTrackLimitReview } from './trackLimitsSteward.js';
4
+ import { calculatePitLaneSpeedingReview } from './pitLaneSpeedingSteward.js';
5
+ import { getPenaltyRule } from '../rulesConfig.js';
6
+ import { simSpeedToKph } from '../units.js';
7
+ import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
8
+
9
+ function forwardVector(car) {
10
+ return { x: Math.cos(car.heading), y: Math.sin(car.heading) };
11
+ }
12
+
13
+ function velocityVector(car) {
14
+ const forward = forwardVector(car);
15
+ return { x: forward.x * car.speed, y: forward.y * car.speed };
16
+ }
17
+
18
+ function dot(a, b) {
19
+ return a.x * b.x + a.y * b.y;
20
+ }
21
+
22
+ function normalizeVector(vector) {
23
+ const length = Math.hypot(vector.x, vector.y) || 1;
24
+ return { x: vector.x / length, y: vector.y / length };
25
+ }
26
+
27
+ function progressDelta(a, b, trackLength) {
28
+ let delta = a - b;
29
+ if (delta < -trackLength / 2) delta += trackLength;
30
+ if (delta > trackLength / 2) delta -= trackLength;
31
+ return delta;
32
+ }
33
+
34
+ function createCollisionStewardContext(first, second, collision) {
35
+ const distanceDelta = collision.trackLength
36
+ ? progressDelta(second.progress ?? second.raceDistance ?? 0, first.progress ?? first.raceDistance ?? 0, collision.trackLength)
37
+ : (second.raceDistance ?? 0) - (first.raceDistance ?? 0);
38
+ const sideBySideTolerance = VEHICLE_LIMITS.carLength * 0.18;
39
+ if (Math.abs(distanceDelta) <= sideBySideTolerance) {
40
+ const firstVelocity = velocityVector(first);
41
+ const secondVelocity = velocityVector(second);
42
+ const relativeVelocity = {
43
+ x: firstVelocity.x - secondVelocity.x,
44
+ y: firstVelocity.y - secondVelocity.y,
45
+ };
46
+ return {
47
+ ...collision,
48
+ impactSpeed: Math.hypot(relativeVelocity.x, relativeVelocity.y),
49
+ aheadDriverId: null,
50
+ atFaultDriverId: null,
51
+ sharedFault: true,
52
+ sharedFaultDriverIds: [first.id, second.id],
53
+ };
54
+ }
55
+
56
+ const firstBehind = distanceDelta > 0;
57
+ const behind = firstBehind ? first : second;
58
+ const ahead = firstBehind ? second : first;
59
+ const directionBehindToAhead = normalizeVector({
60
+ x: ahead.x - behind.x,
61
+ y: ahead.y - behind.y,
62
+ });
63
+ const behindVelocity = velocityVector(behind);
64
+ const aheadVelocity = velocityVector(ahead);
65
+ const relativeVelocity = {
66
+ x: behindVelocity.x - aheadVelocity.x,
67
+ y: behindVelocity.y - aheadVelocity.y,
68
+ };
69
+
70
+ return {
71
+ ...collision,
72
+ impactSpeed: Math.max(0, dot(relativeVelocity, directionBehindToAhead)),
73
+ aheadDriverId: ahead.id,
74
+ atFaultDriverId: behind.id,
75
+ };
76
+ }
77
+
78
+ function isLegallyInsidePitLaneForTrackLimits(car) {
79
+ if (!car.trackState?.inPitLane) return false;
80
+ const wheels = car.wheelStates ?? [];
81
+ return wheels.length > 0 && wheels.some((wheel) => wheel.inPitLane);
82
+ }
83
+
84
+ export function reviewCollisionForSimulation(sim, first, second, collision) {
85
+ const rule = getPenaltyRule(sim.rules, 'collision');
86
+ calculateCollisionPenalties({
87
+ first,
88
+ second,
89
+ collision: createCollisionStewardContext(first, second, collision),
90
+ rule,
91
+ }).forEach((penalty) => sim.recordPenalty(penalty));
92
+ }
93
+
94
+ export function reviewTireRequirementForSimulation(sim, car) {
95
+ const penalty = calculateTireRequirementPenalty({
96
+ car,
97
+ tireStrategy: sim.rules.modules?.tireStrategy,
98
+ rule: getPenaltyRule(sim.rules, 'tireRequirement'),
99
+ });
100
+ if (penalty) sim.recordPenalty(penalty);
101
+ }
102
+
103
+ export function reviewTrackLimitsForSimulation(sim) {
104
+ const rule = getPenaltyRule(sim.rules, 'trackLimits');
105
+ sim.cars.forEach((car) => {
106
+ if (isLegallyInsidePitLaneForTrackLimits(car)) {
107
+ const current = sim.stewardState.trackLimits[car.id];
108
+ sim.stewardState.trackLimits[car.id] = { ...(current ?? { violations: 0 }), active: false };
109
+ return;
110
+ }
111
+
112
+ const currentState = sim.stewardState.trackLimits[car.id];
113
+ const review = calculateTrackLimitReview({
114
+ car,
115
+ rule,
116
+ track: sim.track,
117
+ stewardState: currentState,
118
+ });
119
+ sim.stewardState.trackLimits[car.id] = review.nextState;
120
+ if (review.event) sim.events.unshift({ ...review.event, at: sim.time });
121
+ if (review.penalty) sim.recordPenalty(review.penalty);
122
+ });
123
+ }
124
+
125
+ export function reviewPitLaneSpeedingForSimulation(sim) {
126
+ const rule = getPenaltyRule(sim.rules, 'pitLaneSpeeding');
127
+ sim.cars.forEach((car) => {
128
+ const currentState = sim.stewardState.pitLaneSpeeding[car.id];
129
+ const review = calculatePitLaneSpeedingReview({
130
+ car: {
131
+ ...car,
132
+ speedKph: simSpeedToKph(car.speed),
133
+ },
134
+ rule,
135
+ stewardState: currentState,
136
+ });
137
+ sim.stewardState.pitLaneSpeeding[car.id] = review.nextState;
138
+ if (review.event) sim.events.unshift({ ...review.event, at: sim.time });
139
+ if (review.penalty) sim.recordPenalty(review.penalty);
140
+ });
141
+ }
@@ -0,0 +1,47 @@
1
+ import { DEFAULT_MODULES } from './ruleDefaults.js';
2
+
3
+ export const RULESET_MODULES = {
4
+ paddock: DEFAULT_MODULES,
5
+ custom: DEFAULT_MODULES,
6
+ grandPrix2025: {
7
+ ...DEFAULT_MODULES,
8
+ pitStops: {
9
+ ...DEFAULT_MODULES.pitStops,
10
+ enabled: true,
11
+ pitLaneSpeedLimitKph: 80,
12
+ },
13
+ tireStrategy: {
14
+ ...DEFAULT_MODULES.tireStrategy,
15
+ enabled: true,
16
+ mandatoryDistinctDryCompounds: 2,
17
+ },
18
+ penalties: {
19
+ ...DEFAULT_MODULES.penalties,
20
+ enabled: true,
21
+ stewardStrictness: 0.85,
22
+ trackLimits: {
23
+ ...DEFAULT_MODULES.penalties.trackLimits,
24
+ strictness: 0.85,
25
+ },
26
+ collision: {
27
+ ...DEFAULT_MODULES.penalties.collision,
28
+ strictness: 0.65,
29
+ },
30
+ tireRequirement: {
31
+ ...DEFAULT_MODULES.penalties.tireRequirement,
32
+ strictness: 1,
33
+ },
34
+ pitLaneSpeeding: {
35
+ ...DEFAULT_MODULES.penalties.pitLaneSpeeding,
36
+ strictness: 1,
37
+ speedLimitKph: 80,
38
+ },
39
+ },
40
+ fuelLoad: {
41
+ enabled: false,
42
+ reserved: true,
43
+ },
44
+ },
45
+ };
46
+
47
+ RULESET_MODULES.fia2025 = RULESET_MODULES.grandPrix2025;
@@ -0,0 +1,29 @@
1
+ export function calculateTireRequirementPenalty({ car, tireStrategy, rule }) {
2
+ if (!rule || !tireStrategy?.enabled) return null;
3
+
4
+ const requiredDistinctCompounds = tireStrategy.mandatoryDistinctDryCompounds;
5
+ if (!Number.isFinite(requiredDistinctCompounds) || requiredDistinctCompounds <= 0) return null;
6
+
7
+ const usedCompounds = Array.isArray(car.usedTireCompounds) && car.usedTireCompounds.length
8
+ ? [...new Set(car.usedTireCompounds.filter(Boolean))]
9
+ : [...new Set([car.tire].filter(Boolean))];
10
+ const usedDistinctCompounds = usedCompounds.length;
11
+ const missingCompounds = Math.max(0, requiredDistinctCompounds - usedDistinctCompounds);
12
+ const toleratedMissingCompounds = Math.floor((1 - rule.strictness) * requiredDistinctCompounds);
13
+
14
+ if (missingCompounds <= toleratedMissingCompounds) return null;
15
+
16
+ return {
17
+ type: 'tire-requirement',
18
+ driverId: car.id,
19
+ strictness: rule.strictness,
20
+ penaltySeconds: rule.timePenaltySeconds,
21
+ consequences: rule.consequences,
22
+ requiredDistinctCompounds,
23
+ usedDistinctCompounds,
24
+ usedCompounds,
25
+ missingCompounds,
26
+ toleratedMissingCompounds,
27
+ reason: 'Mandatory dry compound requirement not met',
28
+ };
29
+ }
@@ -0,0 +1,85 @@
1
+ import { calculateWheelSurfaceState, isWholeCarOutsideTrackLimits } from '../vehicle/wheelSurface.js';
2
+ import { metersToSimUnits } from '../units.js';
3
+
4
+ const STEWARD_TRACK_LIMIT_EPSILON = metersToSimUnits(0.2);
5
+
6
+ export function calculateTrackLimitReview({ car, rule, track, stewardState }) {
7
+ if (!rule) {
8
+ return {
9
+ nextState: stewardState ?? { active: false, violations: 0 },
10
+ event: null,
11
+ penalty: null,
12
+ };
13
+ }
14
+
15
+ const current = stewardState ?? { active: false, violations: 0 };
16
+ const wheelSurfaceState = car.wheelStates?.length
17
+ ? {
18
+ wheels: car.wheelStates,
19
+ trackLimits: isWholeCarOutsideTrackLimits(car.wheelStates, track),
20
+ }
21
+ : calculateWheelSurfaceState({ car, track });
22
+ const wheelStates = wheelSurfaceState.wheels;
23
+ if (wheelStates.some((wheelState) => wheelState.inPitLane)) {
24
+ return {
25
+ nextState: { ...current, active: false },
26
+ event: null,
27
+ penalty: null,
28
+ };
29
+ }
30
+ const relaxedMargin = ((rule.relaxedMargin ?? 0) * (1 - rule.strictness)) + STEWARD_TRACK_LIMIT_EPSILON;
31
+ const trackLimitState = isWholeCarOutsideTrackLimits(wheelStates, track, relaxedMargin);
32
+ const outsideBy = trackLimitState.outsideBy;
33
+ const isViolation = trackLimitState.violating;
34
+
35
+ if (!isViolation) {
36
+ return {
37
+ nextState: { ...current, active: false },
38
+ event: null,
39
+ penalty: null,
40
+ };
41
+ }
42
+
43
+ if (current.active) {
44
+ return {
45
+ nextState: current,
46
+ event: null,
47
+ penalty: null,
48
+ };
49
+ }
50
+
51
+ const violationCount = current.violations + 1;
52
+ const nextState = {
53
+ active: true,
54
+ violations: violationCount,
55
+ };
56
+ const event = {
57
+ type: 'track-limits',
58
+ carId: car.id,
59
+ violationCount,
60
+ warningsBeforePenalty: rule.warningsBeforePenalty,
61
+ decision: violationCount <= rule.warningsBeforePenalty ? 'warning' : 'penalty',
62
+ strictness: rule.strictness,
63
+ };
64
+
65
+ if (violationCount <= rule.warningsBeforePenalty) {
66
+ return { nextState, event, penalty: null };
67
+ }
68
+
69
+ return {
70
+ nextState,
71
+ event,
72
+ penalty: {
73
+ type: 'track-limits',
74
+ driverId: car.id,
75
+ strictness: rule.strictness,
76
+ penaltySeconds: rule.timePenaltySeconds,
77
+ consequences: rule.consequences,
78
+ violationCount,
79
+ margin: relaxedMargin,
80
+ outsideBy,
81
+ wheelOffsets: wheelStates.map((wheelState) => wheelState.signedOffset),
82
+ reason: 'Exceeded track limits',
83
+ },
84
+ };
85
+ }
@@ -0,0 +1,34 @@
1
+ import { DEFAULT_RULES } from './rules/ruleDefaults.js';
2
+ import { normalizeModules } from './rules/moduleConfig.js';
3
+ import { clamp01, mergeConfig } from './rules/ruleConfigMerge.js';
4
+ import { RULESET_MODULES } from './rules/rulesetPresets.js';
5
+
6
+ export { DEFAULT_RULES } from './rules/ruleDefaults.js';
7
+
8
+ export function normalizeRaceRules(rules = {}) {
9
+ const requestedRuleset = rules?.ruleset ?? rules?.profile ?? 'paddock';
10
+ const ruleset = Object.hasOwn(RULESET_MODULES, requestedRuleset) ? requestedRuleset : 'custom';
11
+ const presetModules = RULESET_MODULES[ruleset] ?? RULESET_MODULES.custom;
12
+ const modules = normalizeModules(mergeConfig(presetModules, rules.modules), rules.modules);
13
+ const flatOverrides = Object.fromEntries(
14
+ Object.entries(rules ?? {}).filter(([key]) => key !== 'ruleset' && key !== 'profile' && key !== 'modules'),
15
+ );
16
+
17
+ return {
18
+ ...DEFAULT_RULES,
19
+ ...flatOverrides,
20
+ ruleset,
21
+ modules,
22
+ };
23
+ }
24
+
25
+ export function getPenaltyRule(rules, type) {
26
+ const penalties = rules?.modules?.penalties;
27
+ if (!penalties?.enabled) return null;
28
+ const config = penalties[type];
29
+ if (!config || config.strictness <= 0) return null;
30
+ return {
31
+ ...config,
32
+ strictness: clamp01(config.strictness * penalties.stewardStrictness, config.strictness),
33
+ };
34
+ }
@@ -0,0 +1,192 @@
1
+ import { WORLD } from '../track/trackModel.js';
2
+ import { simSpeedToKph, simUnitsToMeters } from '../units.js';
3
+ import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
4
+ import { serializePenalty } from '../rules/penaltyLedger.js';
5
+ import { normalizePitIntent, PIT_INTENT_NONE } from '../pit/pitIntent.js';
6
+ import {
7
+ pitLaneStatusSnapshot,
8
+ serializeObservationPitStop,
9
+ serializePitStop,
10
+ serializeRenderPitStop,
11
+ } from '../pit/pitSnapshots.js';
12
+ import {
13
+ serializeCar,
14
+ serializeObservationCar,
15
+ serializeRenderCar,
16
+ serializeTrainingCar,
17
+ } from '../vehicle/vehicleSnapshots.js';
18
+ import { createLapTelemetry, serializeLapTelemetry } from '../timing/raceTiming.js';
19
+ import { affectsRaceOrder } from '../participants/participantInteractions.js';
20
+ import { serializeReplayGhosts } from '../replay/replayGhosts.js';
21
+
22
+ function visibleStartState(sim) {
23
+ return {
24
+ ...sim.raceControl.start,
25
+ visible: sim.raceControl.mode === 'pre-start' ||
26
+ (sim.raceControl.start.releasedAt != null && sim.time - sim.raceControl.start.releasedAt < 1.45),
27
+ };
28
+ }
29
+
30
+ function penaltySecondsFor(statsByDriver, driverId) {
31
+ return statsByDriver.get(driverId)?.seconds ?? 0;
32
+ }
33
+
34
+ export function createVehicleSnapshotDependencies(sim) {
35
+ return {
36
+ createLapTelemetry,
37
+ normalizePitIntent,
38
+ PIT_INTENT_NONE,
39
+ serializeLapTelemetry,
40
+ serializeObservationPitStop: (pitStop) => serializeObservationPitStop(
41
+ pitStop,
42
+ normalizePitIntent,
43
+ PIT_INTENT_NONE,
44
+ ),
45
+ serializePitStop: (pitStop) => serializePitStop(pitStop, normalizePitIntent, PIT_INTENT_NONE),
46
+ serializeRenderPitStop,
47
+ simSpeedToKph,
48
+ simUnitsToMeters,
49
+ VEHICLE_LIMITS,
50
+ };
51
+ }
52
+
53
+ export function snapshotRace(sim) {
54
+ const ordered = sim.orderedCars();
55
+ const snapshotCars = carsForSnapshot(sim, ordered);
56
+ const pitLaneStatus = pitLaneStatusSnapshot(sim.raceControl, sim.track.pitLane, sim.rules.modules?.pitStops);
57
+ const penaltyStats = sim.getPenaltyStatsByDriver();
58
+ const vehicleSnapshotDependencies = createVehicleSnapshotDependencies(sim);
59
+ return {
60
+ time: sim.time,
61
+ world: WORLD,
62
+ physicsMode: sim.physicsMode,
63
+ track: sim.track,
64
+ totalLaps: sim.totalLaps,
65
+ raceControl: {
66
+ mode: sim.raceControl.mode,
67
+ redFlag: Boolean(sim.raceControl.redFlag),
68
+ pitLaneOpen: pitLaneStatus.open,
69
+ pitLaneStatus,
70
+ finished: sim.raceControl.finished,
71
+ finishedAt: sim.raceControl.finishedAt,
72
+ winner: getRaceWinnerSnapshot(sim),
73
+ classification: sim.raceControl.classification.map((entry) => ({ ...entry })),
74
+ start: visibleStartState(sim),
75
+ },
76
+ pitLaneStatus,
77
+ safetyCar: { ...sim.safetyCar },
78
+ rules: sim.rules,
79
+ events: [...sim.events],
80
+ penalties: sim.penalties.map(serializePenalty),
81
+ cars: snapshotCars.map(({ car, rank }) => serializeCar(
82
+ car,
83
+ rank,
84
+ penaltySecondsFor(penaltyStats, car.id),
85
+ vehicleSnapshotDependencies,
86
+ )),
87
+ replayGhosts: serializeReplayGhosts(sim.replayGhosts),
88
+ };
89
+ }
90
+
91
+ export function snapshotRaceRender(sim) {
92
+ const pitLaneStatus = pitLaneStatusSnapshot(sim.raceControl, sim.track.pitLane, sim.rules.modules?.pitStops);
93
+ const vehicleSnapshotDependencies = createVehicleSnapshotDependencies(sim);
94
+ return {
95
+ time: sim.time,
96
+ world: WORLD,
97
+ physicsMode: sim.physicsMode,
98
+ track: sim.track,
99
+ totalLaps: sim.totalLaps,
100
+ raceControl: {
101
+ mode: sim.raceControl.mode,
102
+ redFlag: Boolean(sim.raceControl.redFlag),
103
+ pitLaneOpen: pitLaneStatus.open,
104
+ pitLaneStatus,
105
+ finished: sim.raceControl.finished,
106
+ start: visibleStartState(sim),
107
+ },
108
+ pitLaneStatus,
109
+ safetyCar: { ...sim.safetyCar },
110
+ cars: carsForSnapshot(sim, sim.orderedCars()).map(({ car }) => serializeRenderCar(car, vehicleSnapshotDependencies)),
111
+ replayGhosts: serializeReplayGhosts(sim.replayGhosts),
112
+ };
113
+ }
114
+
115
+ export function snapshotRaceObservation(sim) {
116
+ const pitLaneStatus = pitLaneStatusSnapshot(sim.raceControl, sim.track.pitLane, sim.rules.modules?.pitStops);
117
+ const vehicleSnapshotDependencies = createVehicleSnapshotDependencies(sim);
118
+ return {
119
+ time: sim.time,
120
+ world: WORLD,
121
+ physicsMode: sim.physicsMode,
122
+ track: sim.track,
123
+ totalLaps: sim.totalLaps,
124
+ raceControl: {
125
+ mode: sim.raceControl.mode,
126
+ redFlag: Boolean(sim.raceControl.redFlag),
127
+ pitLaneOpen: pitLaneStatus.open,
128
+ pitLaneStatus,
129
+ finished: sim.raceControl.finished,
130
+ },
131
+ pitLaneStatus,
132
+ safetyCar: { ...sim.safetyCar },
133
+ events: [...sim.events],
134
+ cars: carsForSnapshot(sim, sim.orderedCars()).map(({ car, rank }) => serializeObservationCar(
135
+ car,
136
+ rank,
137
+ vehicleSnapshotDependencies,
138
+ )),
139
+ replayGhosts: serializeReplayGhosts(sim.replayGhosts),
140
+ };
141
+ }
142
+
143
+ export function snapshotRaceTraining(sim) {
144
+ const pitLaneStatus = pitLaneStatusSnapshot(sim.raceControl, sim.track.pitLane, sim.rules.modules?.pitStops);
145
+ const vehicleSnapshotDependencies = createVehicleSnapshotDependencies(sim);
146
+ return {
147
+ time: sim.time,
148
+ world: WORLD,
149
+ physicsMode: sim.physicsMode,
150
+ track: sim.track,
151
+ totalLaps: sim.totalLaps,
152
+ raceControl: {
153
+ mode: sim.raceControl.mode,
154
+ redFlag: Boolean(sim.raceControl.redFlag),
155
+ pitLaneOpen: pitLaneStatus.open,
156
+ pitLaneStatus,
157
+ finished: sim.raceControl.finished,
158
+ },
159
+ pitLaneStatus,
160
+ safetyCar: { ...sim.safetyCar },
161
+ events: [...sim.events],
162
+ cars: carsForSnapshot(sim, sim.orderedCars()).map(({ car, rank }) => serializeTrainingCar(
163
+ car,
164
+ rank,
165
+ vehicleSnapshotDependencies,
166
+ )),
167
+ replayGhosts: serializeReplayGhosts(sim.replayGhosts),
168
+ };
169
+ }
170
+
171
+ export function getRaceWinnerSnapshot(sim) {
172
+ if (!sim.raceControl.winnerId) return null;
173
+ const car = sim.cars.find((item) => item.id === sim.raceControl.winnerId);
174
+ if (!car) return null;
175
+ const rank = car.classifiedRank ?? car.rank ?? 1;
176
+ return serializeCar(
177
+ car,
178
+ rank,
179
+ penaltySecondsFor(sim.getPenaltyStatsByDriver(), car.id),
180
+ createVehicleSnapshotDependencies(sim),
181
+ );
182
+ }
183
+
184
+ function carsForSnapshot(sim, ordered) {
185
+ const orderedIds = new Set(ordered.map((car) => car.id));
186
+ const ranked = ordered.map((car, index) => ({ car, rank: index + 1 }));
187
+ const unranked = sim.cars
188
+ .filter((car) => !orderedIds.has(car.id))
189
+ .sort((a, b) => a.index - b.index)
190
+ .map((car) => ({ car, rank: affectsRaceOrder(car) ? (car.rank ?? null) : null }));
191
+ return [...ranked, ...unranked];
192
+ }
@@ -0,0 +1,87 @@
1
+ import { wrapDistance } from '../simMath.js';
2
+
3
+ function wrapProgress(value, length) {
4
+ return wrapDistance(value, length);
5
+ }
6
+
7
+ function distanceForward(from, to, length) {
8
+ return wrapProgress(to - from, length);
9
+ }
10
+
11
+ function crossesDistance(previous, current, target, length) {
12
+ const travelled = distanceForward(previous, current, length);
13
+ if (travelled <= 0 || travelled > length / 2) return false;
14
+ const targetOffset = distanceForward(previous, target, length);
15
+ return targetOffset > 0 && targetOffset <= travelled + 0.001;
16
+ }
17
+
18
+ function isProgressInZone(track, progress, zone) {
19
+ const wrapped = wrapProgress(progress, track.length);
20
+ const start = wrapProgress(zone.start, track.length);
21
+ const end = wrapProgress(zone.end, track.length);
22
+ if (zone.end - zone.start >= track.length) return true;
23
+ return end >= start
24
+ ? wrapped >= start && wrapped <= end
25
+ : wrapped >= start || wrapped <= end;
26
+ }
27
+
28
+ export function recordDrsDetection(car, zoneId, currentTime) {
29
+ const previous = car.drsDetection?.[zoneId] ?? { passage: 0, time: -Infinity };
30
+ const next = { passage: previous.passage + 1, time: currentTime };
31
+ car.drsDetection = {
32
+ ...(car.drsDetection ?? {}),
33
+ [zoneId]: next,
34
+ };
35
+ return next;
36
+ }
37
+
38
+ export function updateDrsLatch(car, ahead, {
39
+ hasReference = Boolean(ahead),
40
+ safetyCarDeployed = false,
41
+ time,
42
+ track,
43
+ rules,
44
+ } = {}) {
45
+ if (safetyCarDeployed || car.finished) {
46
+ car.drsEligible = false;
47
+ car.drsActive = false;
48
+ car.drsZoneId = null;
49
+ car.drsZoneEnabled = false;
50
+ return;
51
+ }
52
+
53
+ const previousProgress = car.previousProgress ?? car.progress;
54
+ const currentZone = car.drsZoneId
55
+ ? track.drsZones.find((zone) => zone.id === car.drsZoneId)
56
+ : null;
57
+
58
+ if (currentZone && !isProgressInZone(track, car.progress, currentZone)) {
59
+ car.drsZoneId = null;
60
+ car.drsZoneEnabled = false;
61
+ }
62
+
63
+ if (!car.drsZoneId) {
64
+ const crossedZone = track.drsZones.find((zone) => (
65
+ crossesDistance(previousProgress, car.progress, zone.start, track.length)
66
+ ));
67
+ if (crossedZone) {
68
+ car.drsZoneId = crossedZone.id;
69
+ const crossing = recordDrsDetection(car, crossedZone.id, time);
70
+ const aheadCrossing = ahead?.drsDetection?.[crossedZone.id];
71
+ car.drsZoneEnabled = Boolean(
72
+ hasReference &&
73
+ aheadCrossing &&
74
+ aheadCrossing.passage === crossing.passage &&
75
+ crossing.time >= aheadCrossing.time &&
76
+ crossing.time - aheadCrossing.time <= rules.drsDetectionSeconds + 1e-6
77
+ );
78
+ }
79
+ }
80
+
81
+ const activeZone = car.drsZoneId
82
+ ? track.drsZones.find((zone) => zone.id === car.drsZoneId)
83
+ : null;
84
+ const inLatchedZone = activeZone ? isProgressInZone(track, car.progress, activeZone) : false;
85
+ car.drsEligible = Boolean(car.drsZoneEnabled && inLatchedZone);
86
+ car.drsActive = car.drsEligible;
87
+ }