@inventure71/paddockjs 1.0.0 → 2.0.1

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 (203) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/INSTALL_AND_UPDATE.md +4 -3
  3. package/README.md +219 -12
  4. package/docs/architecture.md +126 -45
  5. package/docs/concepts.md +9 -5
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +317 -22
  8. package/docs/data_model.md +1 -1
  9. package/docs/index.md +10 -0
  10. package/docs/learnings.md +22 -0
  11. package/docs/rules.md +71 -14
  12. package/docs/sense_contract.md +59 -0
  13. package/docs/system_specs.md +75 -13
  14. package/docs/training.md +280 -32
  15. package/examples/python/README.md +62 -0
  16. package/examples/python/base_policy_server.py +191 -0
  17. package/examples/python/requirements.txt +2 -0
  18. package/package.json +12 -4
  19. package/src/api/PaddockSimulatorController.js +6 -33
  20. package/src/app/BrowserExpertAdapter.js +76 -9
  21. package/src/app/BrowserExpertExternalRenderer.js +303 -0
  22. package/src/app/F1SimulatorApp.js +291 -1630
  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 +4 -0
  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 +20 -0
  48. package/src/config/restartOptions.js +34 -0
  49. package/src/environment/actions.js +13 -5
  50. package/src/environment/controllerLoop.js +261 -0
  51. package/src/environment/episode.js +98 -2
  52. package/src/environment/evaluation.js +54 -32
  53. package/src/environment/index.d.ts +376 -10
  54. package/src/environment/index.js +1 -0
  55. package/src/environment/metrics.js +159 -0
  56. package/src/environment/observationOptions.js +22 -1
  57. package/src/environment/observationVector.js +200 -0
  58. package/src/environment/observations.js +199 -141
  59. package/src/environment/options.js +37 -5
  60. package/src/environment/recorder.js +13 -6
  61. package/src/environment/rewards.js +13 -2
  62. package/src/environment/runtime.js +272 -23
  63. package/src/environment/scenarios.js +57 -9
  64. package/src/environment/sensors/bodySenses.js +23 -0
  65. package/src/environment/sensors/boundarySenses.js +17 -0
  66. package/src/environment/sensors/carRays.js +75 -0
  67. package/src/environment/sensors/contactSenses.js +32 -0
  68. package/src/environment/sensors/index.js +157 -0
  69. package/src/environment/sensors/indexedRayBands.js +103 -0
  70. package/src/environment/sensors/nearbyCars.js +52 -0
  71. package/src/environment/sensors/opponentRadar.js +75 -0
  72. package/src/environment/sensors/rayConfig.js +110 -0
  73. package/src/environment/sensors/rayDefaults.js +6 -0
  74. package/src/environment/sensors/rayGeometry.js +60 -0
  75. package/src/environment/sensors/rayGuards.js +19 -0
  76. package/src/environment/sensors/sensorTargets.js +66 -0
  77. package/src/environment/sensors/surfaceRays.js +360 -0
  78. package/src/environment/sensors/trackRays.js +424 -0
  79. package/src/environment/sensors.js +1 -367
  80. package/src/environment/specs.js +159 -26
  81. package/src/environment/workerProtocol.js +13 -1
  82. package/src/index.d.ts +171 -6
  83. package/src/index.js +6 -32
  84. package/src/rendering/proceduralTrackAsset.js +20 -823
  85. package/src/rendering/renderSnapshot.js +38 -12
  86. package/src/rendering/track/finishLineRenderer.js +34 -0
  87. package/src/rendering/track/gridRenderer.js +47 -0
  88. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  89. package/src/rendering/track/pitLaneRenderer.js +248 -0
  90. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  91. package/src/rendering/track/trackRenderConstants.js +38 -0
  92. package/src/rendering/track/trackRenderGeometry.js +94 -0
  93. package/src/simulation/collisionGeometry.js +1 -1
  94. package/src/simulation/driver/driverControlConstants.js +28 -0
  95. package/src/simulation/driver/driverController.js +47 -0
  96. package/src/simulation/driver/driverInput.js +28 -0
  97. package/src/simulation/driver/driverMath.js +6 -0
  98. package/src/simulation/driver/driverPersonality.js +25 -0
  99. package/src/simulation/driver/edgeRecovery.js +48 -0
  100. package/src/simulation/driver/racingControls.js +399 -0
  101. package/src/simulation/driver/racingLinePlan.js +166 -0
  102. package/src/simulation/driver/recoveryDynamics.js +38 -0
  103. package/src/simulation/driver/rejoinControls.js +213 -0
  104. package/src/simulation/driver/safetyCarControls.js +35 -0
  105. package/src/simulation/driver/trafficScan.js +51 -0
  106. package/src/simulation/driverController.js +4 -619
  107. package/src/simulation/participants/participantInteractions.js +120 -0
  108. package/src/simulation/pit/pitFlow.js +239 -0
  109. package/src/simulation/pit/pitIntent.js +92 -0
  110. package/src/simulation/pit/pitKinematics.js +24 -0
  111. package/src/simulation/pit/pitOccupancy.js +47 -0
  112. package/src/simulation/pit/pitPenaltyService.js +71 -0
  113. package/src/simulation/pit/pitQueue.js +60 -0
  114. package/src/simulation/pit/pitRouteMovement.js +167 -0
  115. package/src/simulation/pit/pitRouting.js +319 -0
  116. package/src/simulation/pit/pitService.js +43 -0
  117. package/src/simulation/pit/pitServiceConstants.js +21 -0
  118. package/src/simulation/pit/pitServiceProfile.js +45 -0
  119. package/src/simulation/pit/pitSnapshots.js +61 -0
  120. package/src/simulation/pit/pitState.js +113 -0
  121. package/src/simulation/pit/pitTireService.js +94 -0
  122. package/src/simulation/race/classification.js +104 -0
  123. package/src/simulation/race/gridPenalties.js +37 -0
  124. package/src/simulation/race/raceControlState.js +38 -0
  125. package/src/simulation/race/raceDistance.js +32 -0
  126. package/src/simulation/race/raceFinish.js +65 -0
  127. package/src/simulation/race/raceLifecycle.js +67 -0
  128. package/src/simulation/race/raceOrder.js +113 -0
  129. package/src/simulation/race/raceProgress.js +129 -0
  130. package/src/simulation/race/raceSetup.js +93 -0
  131. package/src/simulation/race/raceStep.js +80 -0
  132. package/src/simulation/race/redFlag.js +15 -0
  133. package/src/simulation/race/retirements.js +44 -0
  134. package/src/simulation/race/safetyCar.js +92 -0
  135. package/src/simulation/race/stalledDnf.js +77 -0
  136. package/src/simulation/raceSimulation.js +231 -2817
  137. package/src/simulation/replay/replayGhosts.js +128 -0
  138. package/src/simulation/rules/moduleConfig.js +72 -0
  139. package/src/simulation/rules/penaltyConfig.js +99 -0
  140. package/src/simulation/rules/penaltyEvents.js +18 -0
  141. package/src/simulation/rules/penaltyStats.js +26 -0
  142. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  143. package/src/simulation/rules/ruleDefaults.js +85 -0
  144. package/src/simulation/rules/rulesReview.js +144 -0
  145. package/src/simulation/rules/rulesetPresets.js +47 -0
  146. package/src/simulation/rules/trackLimitsSteward.js +1 -1
  147. package/src/simulation/rulesConfig.js +5 -323
  148. package/src/simulation/snapshots/raceSnapshots.js +193 -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/trackQueryGrid.js +94 -0
  171. package/src/simulation/track/trackQueryIndex.js +659 -0
  172. package/src/simulation/track/trackQueryPitIndex.js +140 -0
  173. package/src/simulation/track/trackQueryScratch.js +46 -0
  174. package/src/simulation/track/trackQueryStats.js +28 -0
  175. package/src/simulation/track/trackSectors.js +19 -0
  176. package/src/simulation/track/trackStatePolicy.js +24 -0
  177. package/src/simulation/track/trackValidation.js +98 -0
  178. package/src/simulation/trackModel.js +1 -1706
  179. package/src/simulation/vehicle/contactResolution.js +181 -0
  180. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  181. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  182. package/src/simulation/vehicle/runoffResponse.js +87 -0
  183. package/src/simulation/vehicle/surfacePriority.js +28 -0
  184. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  185. package/src/simulation/vehicle/vehicleKinematics.js +37 -0
  186. package/src/simulation/vehicle/vehiclePhysics.js +364 -0
  187. package/src/simulation/vehicle/vehicleSnapshots.js +327 -0
  188. package/src/simulation/vehicle/vehicleState.js +220 -0
  189. package/src/simulation/vehicle/wheelSurface.js +152 -0
  190. package/src/simulation/vehicleGeometry.js +1 -247
  191. package/src/simulation/vehiclePhysics.js +1 -136
  192. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  193. package/src/simulation/wheelSurface.js +1 -338
  194. package/src/styles.css +42 -0
  195. package/src/ui/cameraControlsTemplate.js +20 -0
  196. package/src/ui/carOverviewTemplate.js +33 -0
  197. package/src/ui/componentTemplates.js +14 -360
  198. package/src/ui/raceCanvasTemplate.js +69 -0
  199. package/src/ui/raceControlsTemplate.js +36 -0
  200. package/src/ui/shellTemplate.js +5 -4
  201. package/src/ui/telemetryDrawerTemplate.js +55 -0
  202. package/src/ui/telemetryTemplates.js +136 -0
  203. package/src/ui/timingTowerTemplate.js +34 -0
@@ -1,327 +1,9 @@
1
- import { kphToSimSpeed, metersToSimUnits } from './units.js';
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';
2
5
 
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
- 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
- penalties: {
40
- enabled: false,
41
- stewardStrictness: 1,
42
- trackLimits: {
43
- strictness: 0,
44
- warningsBeforePenalty: 3,
45
- timePenaltySeconds: 5,
46
- relaxedMarginMeters: 3,
47
- },
48
- collision: {
49
- strictness: 0,
50
- timePenaltySeconds: 5,
51
- minimumSeverity: 2,
52
- relaxedSeverityMargin: 6,
53
- minimumImpactSpeedKph: 20,
54
- relaxedImpactSpeedKph: 20,
55
- },
56
- tireRequirement: {
57
- strictness: 0,
58
- timePenaltySeconds: 10,
59
- },
60
- pitLaneSpeeding: {
61
- strictness: 0,
62
- speedLimitKph: 80,
63
- marginKph: 0.5,
64
- relaxedMarginKph: 5,
65
- timePenaltySeconds: 5,
66
- },
67
- },
68
- weather: {
69
- enabled: false,
70
- },
71
- reliability: {
72
- enabled: false,
73
- },
74
- fuelLoad: {
75
- enabled: false,
76
- },
77
- };
78
-
79
- const RULESET_MODULES = {
80
- paddock: DEFAULT_MODULES,
81
- custom: DEFAULT_MODULES,
82
- grandPrix2025: {
83
- ...DEFAULT_MODULES,
84
- pitStops: {
85
- ...DEFAULT_MODULES.pitStops,
86
- enabled: true,
87
- pitLaneSpeedLimitKph: 80,
88
- },
89
- tireStrategy: {
90
- ...DEFAULT_MODULES.tireStrategy,
91
- enabled: true,
92
- mandatoryDistinctDryCompounds: 2,
93
- },
94
- penalties: {
95
- ...DEFAULT_MODULES.penalties,
96
- enabled: true,
97
- stewardStrictness: 0.85,
98
- trackLimits: {
99
- ...DEFAULT_MODULES.penalties.trackLimits,
100
- strictness: 0.85,
101
- },
102
- collision: {
103
- ...DEFAULT_MODULES.penalties.collision,
104
- strictness: 0.65,
105
- },
106
- tireRequirement: {
107
- ...DEFAULT_MODULES.penalties.tireRequirement,
108
- strictness: 1,
109
- },
110
- pitLaneSpeeding: {
111
- ...DEFAULT_MODULES.penalties.pitLaneSpeeding,
112
- strictness: 1,
113
- speedLimitKph: 80,
114
- },
115
- },
116
- fuelLoad: {
117
- enabled: true,
118
- },
119
- },
120
- };
121
-
122
- RULESET_MODULES.fia2025 = RULESET_MODULES.grandPrix2025;
123
-
124
- const MODULE_NAMES = Object.keys(DEFAULT_MODULES);
125
- const PENALTY_SUBSECTIONS = ['trackLimits', 'collision', 'tireRequirement', 'pitLaneSpeeding'];
126
- const CONSEQUENCE_TYPES = new Set([
127
- 'warning',
128
- 'time',
129
- 'driveThrough',
130
- 'stopGo',
131
- 'positionDrop',
132
- 'gridDrop',
133
- 'disqualification',
134
- ]);
135
-
136
- function isPlainObject(value) {
137
- return value != null && typeof value === 'object' && !Array.isArray(value);
138
- }
139
-
140
- function clone(value) {
141
- if (Array.isArray(value)) return value.map((item) => clone(item));
142
- if (!isPlainObject(value)) return value;
143
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, clone(item)]));
144
- }
145
-
146
- function mergeConfig(base, override) {
147
- if (!isPlainObject(override)) return clone(base);
148
- const next = clone(base);
149
- Object.entries(override).forEach(([key, value]) => {
150
- next[key] = isPlainObject(value) && isPlainObject(next[key])
151
- ? mergeConfig(next[key], value)
152
- : clone(value);
153
- });
154
- return next;
155
- }
156
-
157
- function clamp01(value, fallback = 0) {
158
- const numeric = Number(value);
159
- if (!Number.isFinite(numeric)) return fallback;
160
- return Math.max(0, Math.min(1, numeric));
161
- }
162
-
163
- function positiveNumber(value, fallback) {
164
- const numeric = Number(value);
165
- return Number.isFinite(numeric) && numeric > 0 ? numeric : fallback;
166
- }
167
-
168
- function nonNegativeInteger(value, fallback) {
169
- const numeric = Number(value);
170
- return Number.isFinite(numeric) && numeric >= 0 ? Math.floor(numeric) : fallback;
171
- }
172
-
173
- function positiveInteger(value, fallback) {
174
- const numeric = Number(value);
175
- return Number.isFinite(numeric) && numeric > 0 ? Math.floor(numeric) : fallback;
176
- }
177
-
178
- function normalizeConsequences(value, fallbackSeconds) {
179
- const source = Array.isArray(value) && value.length
180
- ? value
181
- : [{ type: 'time', seconds: fallbackSeconds }];
182
- const normalized = source
183
- .map((consequence) => {
184
- const type = CONSEQUENCE_TYPES.has(consequence?.type) ? consequence.type : null;
185
- if (!type) return null;
186
- if (type === 'time') {
187
- return {
188
- type,
189
- seconds: positiveNumber(consequence.seconds, fallbackSeconds),
190
- };
191
- }
192
- if (type === 'driveThrough') {
193
- return {
194
- type,
195
- conversionSeconds: positiveNumber(consequence.conversionSeconds, 20),
196
- };
197
- }
198
- if (type === 'stopGo') {
199
- return {
200
- type,
201
- seconds: positiveNumber(consequence.seconds, 10),
202
- conversionSeconds: positiveNumber(consequence.conversionSeconds, 30),
203
- };
204
- }
205
- if (type === 'positionDrop' || type === 'gridDrop') {
206
- return {
207
- type,
208
- positions: nonNegativeInteger(consequence.positions, 1),
209
- };
210
- }
211
- return { type };
212
- })
213
- .filter(Boolean);
214
- return normalized.length ? normalized : [{ type: 'time', seconds: fallbackSeconds }];
215
- }
216
-
217
- function sumTimeConsequences(consequences) {
218
- return consequences.reduce((total, consequence) => (
219
- consequence.type === 'time' ? total + consequence.seconds : total
220
- ), 0);
221
- }
222
-
223
- function normalizePenaltyConfig(penalties) {
224
- const next = {
225
- ...penalties,
226
- enabled: Boolean(penalties.enabled),
227
- stewardStrictness: clamp01(penalties.stewardStrictness, 1),
228
- };
229
-
230
- PENALTY_SUBSECTIONS.forEach((name) => {
231
- next[name] = {
232
- ...penalties[name],
233
- strictness: clamp01(penalties[name]?.strictness, 0),
234
- };
235
- });
236
-
237
- next.trackLimits.warningsBeforePenalty = nonNegativeInteger(next.trackLimits.warningsBeforePenalty, 3);
238
- next.trackLimits.consequences = normalizeConsequences(next.trackLimits.consequences, positiveNumber(next.trackLimits.timePenaltySeconds, 5));
239
- next.trackLimits.timePenaltySeconds = sumTimeConsequences(next.trackLimits.consequences);
240
- next.trackLimits.relaxedMarginMeters = Math.max(0, Number(next.trackLimits.relaxedMarginMeters) || 0);
241
- next.trackLimits.relaxedMargin = metersToSimUnits(next.trackLimits.relaxedMarginMeters);
242
-
243
- next.collision.consequences = normalizeConsequences(next.collision.consequences, positiveNumber(next.collision.timePenaltySeconds, 5));
244
- next.collision.timePenaltySeconds = sumTimeConsequences(next.collision.consequences);
245
- next.collision.minimumSeverity = Math.max(0, Number(next.collision.minimumSeverity) || 0);
246
- next.collision.relaxedSeverityMargin = Math.max(0, Number(next.collision.relaxedSeverityMargin) || 0);
247
- next.collision.minimumImpactSpeedKph = Math.max(0, Number(next.collision.minimumImpactSpeedKph) || 0);
248
- next.collision.relaxedImpactSpeedKph = Math.max(0, Number(next.collision.relaxedImpactSpeedKph) || 0);
249
- next.collision.minimumImpactSpeed = metersToSimUnits(next.collision.minimumImpactSpeedKph / 3.6);
250
- next.collision.relaxedImpactSpeed = metersToSimUnits(next.collision.relaxedImpactSpeedKph / 3.6);
251
- next.tireRequirement.consequences = normalizeConsequences(next.tireRequirement.consequences, positiveNumber(next.tireRequirement.timePenaltySeconds, 10));
252
- next.tireRequirement.timePenaltySeconds = sumTimeConsequences(next.tireRequirement.consequences);
253
- next.pitLaneSpeeding.speedLimitKph = positiveNumber(next.pitLaneSpeeding.speedLimitKph, 80);
254
- next.pitLaneSpeeding.marginKph = Math.max(0, Number(next.pitLaneSpeeding.marginKph) || 0);
255
- next.pitLaneSpeeding.relaxedMarginKph = Math.max(0, Number(next.pitLaneSpeeding.relaxedMarginKph) || 0);
256
- next.pitLaneSpeeding.consequences = normalizeConsequences(next.pitLaneSpeeding.consequences, positiveNumber(next.pitLaneSpeeding.timePenaltySeconds, 5));
257
- next.pitLaneSpeeding.timePenaltySeconds = sumTimeConsequences(next.pitLaneSpeeding.consequences);
258
-
259
- next.enabled = next.enabled || PENALTY_SUBSECTIONS.some((name) => next[name].strictness > 0);
260
- return next;
261
- }
262
-
263
- function normalizeModules(modules, explicitModules = {}) {
264
- const next = mergeConfig(DEFAULT_MODULES, modules);
265
- const explicitPitSpeed = isPlainObject(explicitModules?.penalties?.pitLaneSpeeding) &&
266
- Object.hasOwn(explicitModules.penalties.pitLaneSpeeding, 'speedLimitKph');
267
-
268
- MODULE_NAMES.forEach((name) => {
269
- next[name] = {
270
- ...next[name],
271
- enabled: Boolean(next[name].enabled),
272
- };
273
- });
274
-
275
- next.pitStops.pitLaneSpeedLimitKph = positiveNumber(next.pitStops.pitLaneSpeedLimitKph, 80);
276
- next.pitStops.defaultStopSeconds = positiveNumber(next.pitStops.defaultStopSeconds, 2.8);
277
- next.pitStops.variability = {
278
- ...DEFAULT_MODULES.pitStops.variability,
279
- ...(isPlainObject(next.pitStops.variability) ? next.pitStops.variability : {}),
280
- };
281
- next.pitStops.variability.enabled = Boolean(next.pitStops.variability.enabled);
282
- next.pitStops.variability.perfect = Boolean(next.pitStops.variability.perfect);
283
- next.pitStops.variability.speedImpactSeconds = Math.max(
284
- 0,
285
- Number(next.pitStops.variability.speedImpactSeconds) || DEFAULT_MODULES.pitStops.variability.speedImpactSeconds,
286
- );
287
- next.pitStops.variability.consistencyJitterSeconds = Math.max(
288
- 0,
289
- Number(next.pitStops.variability.consistencyJitterSeconds) || DEFAULT_MODULES.pitStops.variability.consistencyJitterSeconds,
290
- );
291
- next.pitStops.variability.issueChance = clamp01(
292
- next.pitStops.variability.issueChance,
293
- DEFAULT_MODULES.pitStops.variability.issueChance,
294
- );
295
- next.pitStops.variability.issueMaxDelaySeconds = Math.max(
296
- 0,
297
- Number(next.pitStops.variability.issueMaxDelaySeconds) || DEFAULT_MODULES.pitStops.variability.issueMaxDelaySeconds,
298
- );
299
- next.pitStops.maxConcurrentPitLaneCars = positiveInteger(next.pitStops.maxConcurrentPitLaneCars, 3);
300
- next.pitStops.minimumPitLaneGapMeters = positiveNumber(next.pitStops.minimumPitLaneGapMeters, 20);
301
- next.pitStops.minimumPitLaneGap = metersToSimUnits(next.pitStops.minimumPitLaneGapMeters);
302
- next.pitStops.doubleStacking = Boolean(next.pitStops.doubleStacking);
303
- next.pitStops.tirePitRequestThresholdPercent = clamp01(
304
- Number(next.pitStops.tirePitRequestThresholdPercent) / 100,
305
- 0.5,
306
- ) * 100;
307
- next.pitStops.tirePitCommitThresholdPercent = Math.min(
308
- next.pitStops.tirePitRequestThresholdPercent,
309
- clamp01(Number(next.pitStops.tirePitCommitThresholdPercent) / 100, 0.3) * 100,
310
- );
311
-
312
- next.tireStrategy.compounds = Array.isArray(next.tireStrategy.compounds) && next.tireStrategy.compounds.length
313
- ? [...next.tireStrategy.compounds]
314
- : ['S', 'M', 'H'];
315
- next.tireStrategy.mandatoryDistinctDryCompounds = next.tireStrategy.mandatoryDistinctDryCompounds == null
316
- ? null
317
- : nonNegativeInteger(next.tireStrategy.mandatoryDistinctDryCompounds, 0);
318
-
319
- next.penalties = normalizePenaltyConfig(next.penalties);
320
- if (!explicitPitSpeed) {
321
- next.penalties.pitLaneSpeeding.speedLimitKph = next.pitStops.pitLaneSpeedLimitKph;
322
- }
323
- return next;
324
- }
6
+ export { DEFAULT_RULES } from './rules/ruleDefaults.js';
325
7
 
326
8
  export function normalizeRaceRules(rules = {}) {
327
9
  const requestedRuleset = rules?.ruleset ?? rules?.profile ?? 'paddock';
@@ -0,0 +1,193 @@
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
+ physicsMode: sim.physicsMode,
51
+ };
52
+ }
53
+
54
+ export function snapshotRace(sim) {
55
+ const ordered = sim.orderedCars();
56
+ const snapshotCars = carsForSnapshot(sim, ordered);
57
+ const pitLaneStatus = pitLaneStatusSnapshot(sim.raceControl, sim.track.pitLane, sim.rules.modules?.pitStops);
58
+ const penaltyStats = sim.getPenaltyStatsByDriver();
59
+ const vehicleSnapshotDependencies = createVehicleSnapshotDependencies(sim);
60
+ return {
61
+ time: sim.time,
62
+ world: WORLD,
63
+ physicsMode: sim.physicsMode,
64
+ track: sim.track,
65
+ totalLaps: sim.totalLaps,
66
+ raceControl: {
67
+ mode: sim.raceControl.mode,
68
+ redFlag: Boolean(sim.raceControl.redFlag),
69
+ pitLaneOpen: pitLaneStatus.open,
70
+ pitLaneStatus,
71
+ finished: sim.raceControl.finished,
72
+ finishedAt: sim.raceControl.finishedAt,
73
+ winner: getRaceWinnerSnapshot(sim),
74
+ classification: sim.raceControl.classification.map((entry) => ({ ...entry })),
75
+ start: visibleStartState(sim),
76
+ },
77
+ pitLaneStatus,
78
+ safetyCar: { ...sim.safetyCar },
79
+ rules: sim.rules,
80
+ events: [...sim.events],
81
+ penalties: sim.penalties.map(serializePenalty),
82
+ cars: snapshotCars.map(({ car, rank }) => serializeCar(
83
+ car,
84
+ rank,
85
+ penaltySecondsFor(penaltyStats, car.id),
86
+ vehicleSnapshotDependencies,
87
+ )),
88
+ replayGhosts: serializeReplayGhosts(sim.replayGhosts),
89
+ };
90
+ }
91
+
92
+ export function snapshotRaceRender(sim) {
93
+ const pitLaneStatus = pitLaneStatusSnapshot(sim.raceControl, sim.track.pitLane, sim.rules.modules?.pitStops);
94
+ const vehicleSnapshotDependencies = createVehicleSnapshotDependencies(sim);
95
+ return {
96
+ time: sim.time,
97
+ world: WORLD,
98
+ physicsMode: sim.physicsMode,
99
+ track: sim.track,
100
+ totalLaps: sim.totalLaps,
101
+ raceControl: {
102
+ mode: sim.raceControl.mode,
103
+ redFlag: Boolean(sim.raceControl.redFlag),
104
+ pitLaneOpen: pitLaneStatus.open,
105
+ pitLaneStatus,
106
+ finished: sim.raceControl.finished,
107
+ start: visibleStartState(sim),
108
+ },
109
+ pitLaneStatus,
110
+ safetyCar: { ...sim.safetyCar },
111
+ cars: carsForSnapshot(sim, sim.orderedCars()).map(({ car }) => serializeRenderCar(car, vehicleSnapshotDependencies)),
112
+ replayGhosts: serializeReplayGhosts(sim.replayGhosts),
113
+ };
114
+ }
115
+
116
+ export function snapshotRaceObservation(sim) {
117
+ const pitLaneStatus = pitLaneStatusSnapshot(sim.raceControl, sim.track.pitLane, sim.rules.modules?.pitStops);
118
+ const vehicleSnapshotDependencies = createVehicleSnapshotDependencies(sim);
119
+ return {
120
+ time: sim.time,
121
+ world: WORLD,
122
+ physicsMode: sim.physicsMode,
123
+ track: sim.track,
124
+ totalLaps: sim.totalLaps,
125
+ raceControl: {
126
+ mode: sim.raceControl.mode,
127
+ redFlag: Boolean(sim.raceControl.redFlag),
128
+ pitLaneOpen: pitLaneStatus.open,
129
+ pitLaneStatus,
130
+ finished: sim.raceControl.finished,
131
+ },
132
+ pitLaneStatus,
133
+ safetyCar: { ...sim.safetyCar },
134
+ events: [...sim.events],
135
+ cars: carsForSnapshot(sim, sim.orderedCars()).map(({ car, rank }) => serializeObservationCar(
136
+ car,
137
+ rank,
138
+ vehicleSnapshotDependencies,
139
+ )),
140
+ replayGhosts: serializeReplayGhosts(sim.replayGhosts),
141
+ };
142
+ }
143
+
144
+ export function snapshotRaceTraining(sim) {
145
+ const pitLaneStatus = pitLaneStatusSnapshot(sim.raceControl, sim.track.pitLane, sim.rules.modules?.pitStops);
146
+ const vehicleSnapshotDependencies = createVehicleSnapshotDependencies(sim);
147
+ return {
148
+ time: sim.time,
149
+ world: WORLD,
150
+ physicsMode: sim.physicsMode,
151
+ track: sim.track,
152
+ totalLaps: sim.totalLaps,
153
+ raceControl: {
154
+ mode: sim.raceControl.mode,
155
+ redFlag: Boolean(sim.raceControl.redFlag),
156
+ pitLaneOpen: pitLaneStatus.open,
157
+ pitLaneStatus,
158
+ finished: sim.raceControl.finished,
159
+ },
160
+ pitLaneStatus,
161
+ safetyCar: { ...sim.safetyCar },
162
+ events: [...sim.events],
163
+ cars: carsForSnapshot(sim, sim.orderedCars()).map(({ car, rank }) => serializeTrainingCar(
164
+ car,
165
+ rank,
166
+ vehicleSnapshotDependencies,
167
+ )),
168
+ replayGhosts: serializeReplayGhosts(sim.replayGhosts),
169
+ };
170
+ }
171
+
172
+ export function getRaceWinnerSnapshot(sim) {
173
+ if (!sim.raceControl.winnerId) return null;
174
+ const car = sim.cars.find((item) => item.id === sim.raceControl.winnerId);
175
+ if (!car) return null;
176
+ const rank = car.classifiedRank ?? car.rank ?? 1;
177
+ return serializeCar(
178
+ car,
179
+ rank,
180
+ penaltySecondsFor(sim.getPenaltyStatsByDriver(), car.id),
181
+ createVehicleSnapshotDependencies(sim),
182
+ );
183
+ }
184
+
185
+ function carsForSnapshot(sim, ordered) {
186
+ const orderedIds = new Set(ordered.map((car) => car.id));
187
+ const ranked = ordered.map((car, index) => ({ car, rank: index + 1 }));
188
+ const unranked = sim.cars
189
+ .filter((car) => !orderedIds.has(car.id))
190
+ .sort((a, b) => a.index - b.index)
191
+ .map((car) => ({ car, rank: affectsRaceOrder(car) ? (car.rank ?? null) : null }));
192
+ return [...ranked, ...unranked];
193
+ }
@@ -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
+ }
@@ -0,0 +1,38 @@
1
+ import { interpolateTimeAtDistance } from './timingHistory.js';
2
+ import { getTimingLineCrossingTime, getTimingLineNumber } from './timingLines.js';
3
+
4
+ export function estimateGapAheadSeconds(ahead, car, currentTime, track) {
5
+ if (!ahead) return Infinity;
6
+ const timingLineGap = estimateTimingLineGapSeconds(ahead, car, currentTime, track);
7
+ if (Number.isFinite(timingLineGap)) return timingLineGap;
8
+
9
+ const crossingTime = interpolateTimeAtDistance(ahead.timingHistory, car.raceDistance);
10
+ if (Number.isFinite(crossingTime)) {
11
+ return Math.max(0, currentTime - crossingTime);
12
+ }
13
+ const speedReference = Math.max((ahead.speed + car.speed) * 0.5, 1);
14
+ return Math.max(0, (ahead.raceDistance - car.raceDistance) / speedReference);
15
+ }
16
+
17
+ export function wholeLapGap(aheadDistance, carDistance, trackLength) {
18
+ if (!Number.isFinite(aheadDistance) || !Number.isFinite(carDistance)) return 0;
19
+ if (!Number.isFinite(trackLength) || trackLength <= 0) return 0;
20
+ return Math.max(0, Math.floor((aheadDistance - carDistance + 1e-6) / trackLength));
21
+ }
22
+
23
+ export function estimateTimingLineGapSeconds(ahead, car, currentTime, track) {
24
+ if (!ahead || !car) return Infinity;
25
+ const currentLine = getTimingLineNumber(track, Math.min(ahead.raceDistance ?? 0, car.raceDistance ?? 0));
26
+ if (currentLine == null) return null;
27
+ const oldestLine = Math.max(0, currentLine - (track.timingLines?.count ?? 0));
28
+
29
+ for (let lineNumber = currentLine; lineNumber >= oldestLine; lineNumber -= 1) {
30
+ const aheadTime = getTimingLineCrossingTime(ahead, lineNumber);
31
+ const carTime = getTimingLineCrossingTime(car, lineNumber);
32
+ if (Number.isFinite(aheadTime) && Number.isFinite(carTime)) {
33
+ return Math.max(0, carTime - aheadTime);
34
+ }
35
+ }
36
+
37
+ return null;
38
+ }