@inventure71/paddockjs 0.3.0 → 1.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 (54) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/INSTALL_AND_UPDATE.md +3 -1
  3. package/README.md +142 -9
  4. package/docs/architecture.md +49 -15
  5. package/docs/concepts.md +20 -3
  6. package/docs/data_contract.md +185 -27
  7. package/docs/data_model.md +1 -1
  8. package/docs/learnings.md +1 -1
  9. package/docs/rules.md +140 -19
  10. package/docs/system_specs.md +39 -16
  11. package/docs/training.md +103 -1
  12. package/examples/train-basic-policy.mjs +5 -6
  13. package/examples/trainingData.mjs +133 -0
  14. package/package.json +5 -2
  15. package/src/api/PaddockSimulatorController.js +32 -2
  16. package/src/app/F1SimulatorApp.js +1101 -128
  17. package/src/app/domBindings.js +10 -1
  18. package/src/config/defaultOptions.js +4 -4
  19. package/src/environment/actions.js +25 -2
  20. package/src/environment/evaluation.js +184 -0
  21. package/src/environment/index.d.ts +281 -1
  22. package/src/environment/index.js +13 -0
  23. package/src/environment/observationOptions.js +12 -0
  24. package/src/environment/observations.js +189 -17
  25. package/src/environment/options.js +7 -0
  26. package/src/environment/recorder.js +28 -0
  27. package/src/environment/runtime.js +41 -6
  28. package/src/environment/scenarios.js +220 -0
  29. package/src/environment/sensors.js +145 -35
  30. package/src/environment/specs.js +92 -8
  31. package/src/environment/workerProtocol.js +52 -0
  32. package/src/index.d.ts +366 -3
  33. package/src/index.js +21 -0
  34. package/src/rendering/proceduralTrackAsset.js +336 -23
  35. package/src/simulation/collisionGeometry.js +214 -0
  36. package/src/simulation/driverController.js +431 -65
  37. package/src/simulation/raceSimulation.js +2132 -171
  38. package/src/simulation/rules/collisionSteward.js +69 -0
  39. package/src/simulation/rules/penaltyLedger.js +153 -0
  40. package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
  41. package/src/simulation/rules/tireRequirementSteward.js +29 -0
  42. package/src/simulation/rules/trackLimitsSteward.js +85 -0
  43. package/src/simulation/rulesConfig.js +352 -0
  44. package/src/simulation/trackModel.js +1154 -76
  45. package/src/simulation/units.js +6 -4
  46. package/src/simulation/vehicleGeometry.js +247 -0
  47. package/src/simulation/vehiclePhysics.js +82 -50
  48. package/src/simulation/wheelSurface.js +338 -0
  49. package/src/styles.css +300 -42
  50. package/src/ui/bannerTemplates.js +69 -0
  51. package/src/ui/componentTemplates.js +36 -96
  52. package/src/ui/raceControlStatusBanner.js +39 -0
  53. package/src/ui/shellTemplate.js +1 -1
  54. package/src/ui/templateUtils.js +23 -0
@@ -0,0 +1,352 @@
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
+ 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
+ }
325
+
326
+ export function normalizeRaceRules(rules = {}) {
327
+ const requestedRuleset = rules?.ruleset ?? rules?.profile ?? 'paddock';
328
+ const ruleset = Object.hasOwn(RULESET_MODULES, requestedRuleset) ? requestedRuleset : 'custom';
329
+ const presetModules = RULESET_MODULES[ruleset] ?? RULESET_MODULES.custom;
330
+ const modules = normalizeModules(mergeConfig(presetModules, rules.modules), rules.modules);
331
+ const flatOverrides = Object.fromEntries(
332
+ Object.entries(rules ?? {}).filter(([key]) => key !== 'ruleset' && key !== 'profile' && key !== 'modules'),
333
+ );
334
+
335
+ return {
336
+ ...DEFAULT_RULES,
337
+ ...flatOverrides,
338
+ ruleset,
339
+ modules,
340
+ };
341
+ }
342
+
343
+ export function getPenaltyRule(rules, type) {
344
+ const penalties = rules?.modules?.penalties;
345
+ if (!penalties?.enabled) return null;
346
+ const config = penalties[type];
347
+ if (!config || config.strictness <= 0) return null;
348
+ return {
349
+ ...config,
350
+ strictness: clamp01(config.strictness * penalties.stewardStrictness, config.strictness),
351
+ };
352
+ }