@inventure71/paddockjs 0.2.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 (63) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/INSTALL_AND_UPDATE.md +165 -0
  3. package/README.md +217 -20
  4. package/docs/architecture.md +84 -15
  5. package/docs/concepts.md +20 -3
  6. package/docs/data_contract.md +412 -20
  7. package/docs/data_model.md +2 -1
  8. package/docs/index.md +1 -0
  9. package/docs/learnings.md +1 -1
  10. package/docs/rules.md +140 -19
  11. package/docs/system_specs.md +74 -20
  12. package/docs/training.md +206 -0
  13. package/examples/train-basic-policy.mjs +276 -0
  14. package/examples/trainingData.mjs +133 -0
  15. package/package.json +18 -8
  16. package/src/api/PaddockSimulatorController.js +39 -4
  17. package/src/app/BrowserExpertAdapter.js +32 -0
  18. package/src/app/F1SimulatorApp.js +1251 -146
  19. package/src/app/domBindings.js +10 -2
  20. package/src/config/defaultOptions.js +11 -3
  21. package/src/data/championship.js +13 -0
  22. package/src/data/normalizeDrivers.js +7 -2
  23. package/src/environment/actions.js +68 -0
  24. package/src/environment/episode.js +17 -0
  25. package/src/environment/evaluation.js +184 -0
  26. package/src/environment/events.js +17 -0
  27. package/src/environment/index.d.ts +521 -0
  28. package/src/environment/index.js +15 -0
  29. package/src/environment/observationOptions.js +12 -0
  30. package/src/environment/observations.js +284 -0
  31. package/src/environment/options.js +157 -0
  32. package/src/environment/recorder.js +28 -0
  33. package/src/environment/rewards.js +69 -0
  34. package/src/environment/runtime.js +175 -0
  35. package/src/environment/scenarios.js +220 -0
  36. package/src/environment/sensors.js +367 -0
  37. package/src/environment/specs.js +163 -0
  38. package/src/environment/workerProtocol.js +52 -0
  39. package/src/index.d.ts +405 -6
  40. package/src/index.js +27 -2
  41. package/src/rendering/proceduralTrackAsset.js +343 -24
  42. package/src/simulation/collisionGeometry.js +214 -0
  43. package/src/simulation/driverController.js +431 -65
  44. package/src/simulation/raceSimulation.js +2140 -171
  45. package/src/simulation/rules/collisionSteward.js +69 -0
  46. package/src/simulation/rules/penaltyLedger.js +153 -0
  47. package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
  48. package/src/simulation/rules/tireRequirementSteward.js +29 -0
  49. package/src/simulation/rules/trackLimitsSteward.js +85 -0
  50. package/src/simulation/rulesConfig.js +352 -0
  51. package/src/simulation/trackModel.js +1154 -76
  52. package/src/simulation/units.js +6 -4
  53. package/src/simulation/vehicleGeometry.js +247 -0
  54. package/src/simulation/vehiclePhysics.js +82 -50
  55. package/src/simulation/wheelSurface.js +338 -0
  56. package/src/styles.css +369 -67
  57. package/src/ui/bannerTemplates.js +69 -0
  58. package/src/ui/componentTemplates.js +45 -81
  59. package/src/ui/raceControlStatusBanner.js +39 -0
  60. package/src/ui/shellTemplate.js +1 -1
  61. package/src/ui/templateUtils.js +23 -0
  62. package/AGENTS.md +0 -86
  63. package/src/standalone.js +0 -15
@@ -0,0 +1,69 @@
1
+ import { simSpeedToKph } from '../units.js';
2
+
3
+ export function calculateCollisionPenalties({ first, second, collision = null, rule }) {
4
+ if (!rule) return [];
5
+ const severity = collision?.depth ?? 0;
6
+ const severityThreshold = (rule.minimumSeverity ?? 0) + (rule.relaxedSeverityMargin ?? 0) * (1 - rule.strictness);
7
+ const impactSpeed = collision?.impactSpeed ?? 0;
8
+ const impactSpeedThreshold = (rule.minimumImpactSpeed ?? 0) + (rule.relaxedImpactSpeed ?? 0) * (1 - rule.strictness);
9
+ const atFaultDriverId = collision?.atFaultDriverId ?? null;
10
+ const aheadDriverId = collision?.aheadDriverId ?? null;
11
+ const sharedFaultDriverIds = Array.isArray(collision?.sharedFaultDriverIds)
12
+ ? collision.sharedFaultDriverIds
13
+ : [];
14
+
15
+ if (severity < severityThreshold) return [];
16
+ if (impactSpeed < impactSpeedThreshold) return [];
17
+
18
+ if (sharedFaultDriverIds.length > 0) {
19
+ return sharedFaultDriverIds
20
+ .map((driverId) => {
21
+ const car = first.id === driverId ? first : second.id === driverId ? second : null;
22
+ if (!car) return null;
23
+ const other = first.id === driverId ? second : first;
24
+ return buildCollisionPenalty(car, other, rule, {
25
+ severity,
26
+ severityThreshold,
27
+ impactSpeed,
28
+ impactSpeedThreshold,
29
+ aheadDriverId: null,
30
+ atFaultDriverId: driverId,
31
+ sharedFault: true,
32
+ reason: 'Unclear collision responsibility',
33
+ });
34
+ })
35
+ .filter(Boolean);
36
+ }
37
+
38
+ if (!atFaultDriverId || !aheadDriverId) return [];
39
+
40
+ const atFault = first.id === atFaultDriverId ? first : second;
41
+ const other = first.id === atFaultDriverId ? second : first;
42
+ return [buildCollisionPenalty(atFault, other, rule, {
43
+ severity,
44
+ severityThreshold,
45
+ impactSpeed,
46
+ impactSpeedThreshold,
47
+ aheadDriverId,
48
+ atFaultDriverId,
49
+ })];
50
+ }
51
+
52
+ function buildCollisionPenalty(car, other, rule, context) {
53
+ return {
54
+ type: 'collision',
55
+ driverId: car.id,
56
+ otherCarId: other.id,
57
+ aheadDriverId: context.aheadDriverId,
58
+ atFaultDriverId: context.atFaultDriverId,
59
+ strictness: rule.strictness,
60
+ penaltySeconds: rule.timePenaltySeconds,
61
+ consequences: rule.consequences,
62
+ severity: context.severity,
63
+ threshold: context.severityThreshold,
64
+ impactSpeedKph: simSpeedToKph(context.impactSpeed),
65
+ impactSpeedThresholdKph: simSpeedToKph(context.impactSpeedThreshold),
66
+ sharedFault: Boolean(context.sharedFault),
67
+ reason: context.reason ?? 'Avoidable contact with car ahead',
68
+ };
69
+ }
@@ -0,0 +1,153 @@
1
+ const DEFAULT_SERVICE_CONVERSION_SECONDS = {
2
+ driveThrough: 20,
3
+ stopGo: 30,
4
+ };
5
+
6
+ export function createPenaltyRecord({ sequence, time, lap, penalty }) {
7
+ const consequences = normalizePenaltyConsequences(penalty.consequences, penalty.penaltySeconds);
8
+ const immediatePenaltySeconds = sumTimeConsequences(consequences);
9
+ const serviceConsequence = consequences.find((consequence) => (
10
+ consequence.type === 'driveThrough' || consequence.type === 'stopGo'
11
+ ));
12
+ const status = penalty.status ?? (serviceConsequence ? 'issued' : 'applied');
13
+
14
+ return {
15
+ id: `penalty-${sequence}`,
16
+ at: time,
17
+ lap,
18
+ ...penalty,
19
+ status,
20
+ consequences,
21
+ penaltySeconds: Number.isFinite(Number(penalty.penaltySeconds))
22
+ ? Number(penalty.penaltySeconds)
23
+ : immediatePenaltySeconds,
24
+ pendingPenaltySeconds: serviceConsequence
25
+ ? serviceConversionSeconds(serviceConsequence)
26
+ : 0,
27
+ serviceType: serviceConsequence?.type ?? null,
28
+ serviceRequired: Boolean(serviceConsequence),
29
+ serviceServedAt: null,
30
+ unserved: false,
31
+ positionDrop: sumPositionDropConsequences(consequences),
32
+ gridDrop: sumGridDropConsequences(consequences),
33
+ disqualified: consequences.some((consequence) => consequence.type === 'disqualification'),
34
+ };
35
+ }
36
+
37
+ export function createPenaltyEvent(entry) {
38
+ return {
39
+ type: 'penalty',
40
+ at: entry.at,
41
+ penaltyId: entry.id,
42
+ penaltyType: entry.type,
43
+ driverId: entry.driverId,
44
+ penaltySeconds: entry.penaltySeconds,
45
+ status: entry.status,
46
+ strictness: entry.strictness,
47
+ };
48
+ }
49
+
50
+ export function serializePenalty(penalty) {
51
+ return { ...penalty };
52
+ }
53
+
54
+ export function servePenaltyRecord(penalty, time) {
55
+ if (!penalty || penalty.status === 'cancelled') return penalty;
56
+ if (!penalty.serviceRequired) return penalty;
57
+ penalty.status = 'served';
58
+ penalty.serviceServedAt = time;
59
+ penalty.unserved = false;
60
+ return penalty;
61
+ }
62
+
63
+ export function getPitServicePenaltySeconds(penalty) {
64
+ if (!penalty || penalty.status === 'cancelled' || penalty.status === 'served') return 0;
65
+ if (penalty.serviceRequired && penalty.status === 'issued') {
66
+ if (penalty.serviceType === 'stopGo') return sumStopGoServiceSeconds(penalty.consequences);
67
+ if (penalty.serviceType === 'driveThrough') return 0;
68
+ }
69
+ if (!penalty.serviceRequired && penalty.status === 'applied') {
70
+ return sumTimeConsequences(penalty.consequences);
71
+ }
72
+ return 0;
73
+ }
74
+
75
+ export function isPenaltyPitServiceable(penalty) {
76
+ if (!penalty || penalty.status === 'cancelled' || penalty.status === 'served') return false;
77
+ if (penalty.serviceRequired && penalty.status === 'issued') return true;
78
+ return !penalty.serviceRequired &&
79
+ penalty.status === 'applied' &&
80
+ getPitServicePenaltySeconds(penalty) > 0;
81
+ }
82
+
83
+ export function servePitPenaltyRecord(penalty, time) {
84
+ if (!isPenaltyPitServiceable(penalty)) return penalty;
85
+ penalty.status = 'served';
86
+ penalty.serviceServedAt = time;
87
+ penalty.unserved = false;
88
+ penalty.penaltySeconds = 0;
89
+ penalty.pendingPenaltySeconds = 0;
90
+ return penalty;
91
+ }
92
+
93
+ export function cancelPenaltyRecord(penalty, time) {
94
+ if (!penalty) return penalty;
95
+ penalty.status = 'cancelled';
96
+ penalty.cancelledAt = time;
97
+ penalty.penaltySeconds = 0;
98
+ penalty.pendingPenaltySeconds = 0;
99
+ penalty.positionDrop = 0;
100
+ penalty.gridDrop = 0;
101
+ penalty.disqualified = false;
102
+ penalty.unserved = false;
103
+ return penalty;
104
+ }
105
+
106
+ export function applyUnservedServicePenalty(penalty, time) {
107
+ if (!penalty || penalty.status !== 'issued' || !penalty.serviceRequired) return penalty;
108
+ penalty.status = 'applied';
109
+ penalty.appliedAt = time;
110
+ penalty.unserved = true;
111
+ penalty.penaltySeconds += penalty.pendingPenaltySeconds;
112
+ return penalty;
113
+ }
114
+
115
+ export function isPenaltyActive(penalty) {
116
+ return penalty?.status !== 'cancelled';
117
+ }
118
+
119
+ function normalizePenaltyConsequences(consequences, fallbackSeconds = 0) {
120
+ if (Array.isArray(consequences) && consequences.length) return consequences.map((consequence) => ({ ...consequence }));
121
+ const seconds = Number(fallbackSeconds) || 0;
122
+ return seconds > 0 ? [{ type: 'time', seconds }] : [];
123
+ }
124
+
125
+ function sumTimeConsequences(consequences) {
126
+ return consequences.reduce((total, consequence) => (
127
+ consequence.type === 'time' ? total + (Number(consequence.seconds) || 0) : total
128
+ ), 0);
129
+ }
130
+
131
+ function sumStopGoServiceSeconds(consequences) {
132
+ return consequences.reduce((total, consequence) => (
133
+ consequence.type === 'stopGo' ? total + (Number(consequence.seconds) || 0) : total
134
+ ), 0);
135
+ }
136
+
137
+ function sumPositionDropConsequences(consequences) {
138
+ return consequences.reduce((total, consequence) => (
139
+ consequence.type === 'positionDrop' ? total + Math.max(0, Math.floor(Number(consequence.positions) || 0)) : total
140
+ ), 0);
141
+ }
142
+
143
+ function sumGridDropConsequences(consequences) {
144
+ return consequences.reduce((total, consequence) => (
145
+ consequence.type === 'gridDrop' ? total + Math.max(0, Math.floor(Number(consequence.positions) || 0)) : total
146
+ ), 0);
147
+ }
148
+
149
+ function serviceConversionSeconds(consequence) {
150
+ return Number.isFinite(Number(consequence.conversionSeconds))
151
+ ? Math.max(0, Number(consequence.conversionSeconds))
152
+ : DEFAULT_SERVICE_CONVERSION_SECONDS[consequence.type] ?? 0;
153
+ }
@@ -0,0 +1,71 @@
1
+ const SPEED_LIMITED_PIT_PARTS = new Set(['fast-lane', 'working-lane', 'service-box', 'garage-box']);
2
+
3
+ export function calculatePitLaneSpeedingReview({ car, rule, stewardState }) {
4
+ const current = stewardState ?? { active: false, violations: 0 };
5
+ if (!rule) {
6
+ return {
7
+ nextState: current,
8
+ event: null,
9
+ penalty: null,
10
+ };
11
+ }
12
+
13
+ const speedKph = Number(car.speedKph);
14
+ const pitLanePart = car.trackState?.pitLanePart ?? car.pitLanePart;
15
+ const inLimitedPitLane = Boolean(car.trackState?.inPitLane) && SPEED_LIMITED_PIT_PARTS.has(pitLanePart);
16
+ const marginKph = (Number(rule.marginKph) || 0) + ((Number(rule.relaxedMarginKph) || 0) * (1 - rule.strictness));
17
+ const speedLimitKph = Number(rule.speedLimitKph) || 80;
18
+ const excessKph = speedKph - speedLimitKph;
19
+ const isViolation = inLimitedPitLane && excessKph > marginKph;
20
+
21
+ if (!isViolation) {
22
+ return {
23
+ nextState: { ...current, active: false },
24
+ event: null,
25
+ penalty: null,
26
+ };
27
+ }
28
+
29
+ if (current.active) {
30
+ return {
31
+ nextState: current,
32
+ event: null,
33
+ penalty: null,
34
+ };
35
+ }
36
+
37
+ const violationCount = current.violations + 1;
38
+ const nextState = {
39
+ active: true,
40
+ violations: violationCount,
41
+ };
42
+ const event = {
43
+ type: 'pit-lane-speeding',
44
+ carId: car.id,
45
+ violationCount,
46
+ speedKph,
47
+ speedLimitKph,
48
+ excessKph,
49
+ pitLanePart,
50
+ strictness: rule.strictness,
51
+ };
52
+
53
+ return {
54
+ nextState,
55
+ event,
56
+ penalty: {
57
+ type: 'pit-lane-speeding',
58
+ driverId: car.id,
59
+ strictness: rule.strictness,
60
+ penaltySeconds: rule.timePenaltySeconds,
61
+ consequences: rule.consequences,
62
+ violationCount,
63
+ speedKph,
64
+ speedLimitKph,
65
+ excessKph,
66
+ marginKph,
67
+ pitLanePart,
68
+ reason: 'Exceeded pit lane speed limit',
69
+ },
70
+ };
71
+ }
@@ -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 '../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
+ }