@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,186 @@
1
+ import { buildDriverPersonality } from '../driverController.js';
2
+ import { clamp, normalizeAngle, seededRange } from '../simMath.js';
3
+ import { offsetTrackPoint, pointAt } from '../track/trackModel.js';
4
+ import { kphToSimSpeed, metersToSimUnits } from '../units.js';
5
+ import { VEHICLE_LIMITS } from './vehiclePhysics.js';
6
+ import { applyWheelSurfaceState } from './wheelSurface.js';
7
+ import { nearestTrackStateForCar } from '../track/trackStatePolicy.js';
8
+ import { clearCarDnf } from '../race/retirements.js';
9
+
10
+ const GRID_SLOT_SPACING = metersToSimUnits(8);
11
+ const ROLLING_START_SLOT_SPACING = metersToSimUnits(35);
12
+ const GRID_FIRST_SLOT_DISTANCE = metersToSimUnits(-6);
13
+ const ROLLING_START_FIRST_DISTANCE = metersToSimUnits(-30);
14
+ const GRID_LATERAL_OFFSET = metersToSimUnits(3.2);
15
+
16
+ export function getStartGridSlot(index, { standingStart = false } = {}) {
17
+ const gridDistance = standingStart
18
+ ? GRID_FIRST_SLOT_DISTANCE - index * GRID_SLOT_SPACING
19
+ : ROLLING_START_FIRST_DISTANCE - index * ROLLING_START_SLOT_SPACING;
20
+ const gridOffset = index % 2 === 0 ? -GRID_LATERAL_OFFSET : GRID_LATERAL_OFFSET;
21
+ return { gridDistance, gridOffset };
22
+ }
23
+
24
+ export function createCar(driver, index, random, track, { standingStart = false, createLapTelemetry } = {}) {
25
+ const { gridDistance, gridOffset } = getStartGridSlot(index, { standingStart });
26
+ const start = pointAt(track, gridDistance);
27
+ const position = offsetTrackPoint(start, gridOffset);
28
+ const pace = clamp(driver.pace ?? seededRange(random, 0.95, 1.05), 0.88, 1.12);
29
+ const racecraft = clamp(driver.racecraft ?? seededRange(random, 0.65, 0.92), 0.45, 1);
30
+ const personality = buildDriverPersonality(driver, index, racecraft, random);
31
+ const launchSpeed = kphToSimSpeed(Math.max(70, 84 - index * 0.55 + pace * 4));
32
+ const vehicle = driver.vehicle ?? {};
33
+
34
+ return {
35
+ id: driver.id ?? `car-${index + 1}`,
36
+ code: driver.code ?? `C${index + 1}`,
37
+ timingCode: driver.timingCode ?? driver.code ?? `C${index + 1}`,
38
+ driverNumber: driver.driverNumber ?? index + 1,
39
+ icon: driver.icon ?? String(index + 1).padStart(2, '0'),
40
+ raceName: driver.raceName ?? driver.code ?? `CAR${index + 1}`,
41
+ name: driver.name ?? `Car ${index + 1}`,
42
+ color: driver.color ?? '#e10600',
43
+ team: driver.team ? { ...driver.team } : null,
44
+ tire: driver.tire ?? ['M', 'H', 'S'][index % 3],
45
+ index,
46
+ x: position.x,
47
+ y: position.y,
48
+ previousX: position.x,
49
+ previousY: position.y,
50
+ heading: start.heading,
51
+ previousHeading: start.heading,
52
+ steeringAngle: 0,
53
+ yawRate: 0,
54
+ turnRadius: Infinity,
55
+ speed: standingStart ? 0 : launchSpeed,
56
+ longitudinalAcceleration: 0,
57
+ lateralAcceleration: 0,
58
+ lateralG: 0,
59
+ longitudinalG: 0,
60
+ gripUsage: 0,
61
+ slipAngleRadians: 0,
62
+ tractionLimited: false,
63
+ stabilityState: 'stable',
64
+ throttle: 0,
65
+ brake: 0,
66
+ vehicleId: vehicle.id ?? null,
67
+ vehicleName: vehicle.name ?? null,
68
+ vehicleRatings: vehicle.ratings ? { ...vehicle.ratings } : null,
69
+ mass: vehicle.mass ?? 798 + seededRange(random, -5, 5),
70
+ powerNewtons: vehicle.powerNewtons ?? 43000 * pace,
71
+ brakeNewtons: vehicle.brakeNewtons ?? 59000,
72
+ dragCoefficient: vehicle.dragCoefficient ?? 0.33 + seededRange(random, -0.026, 0.026),
73
+ downforceCoefficient: vehicle.downforceCoefficient ?? 6.1 + seededRange(random, -0.18, 0.18),
74
+ tireGrip: vehicle.tireGrip ?? 2.22 + racecraft * 0.28 + seededRange(random, -0.03, 0.03),
75
+ tireCare: vehicle.tireCare ?? 1,
76
+ pace,
77
+ racecraft,
78
+ personality,
79
+ aggression: personality.baseAggression,
80
+ gridLocked: standingStart,
81
+ gridDistance,
82
+ gridOffset,
83
+ desiredOffset: gridOffset,
84
+ progress: start.distance,
85
+ raceDistance: gridDistance,
86
+ lap: 1,
87
+ rank: index + 1,
88
+ gapAhead: Infinity,
89
+ gapAheadSeconds: Infinity,
90
+ leaderGapSeconds: 0,
91
+ intervalAheadSeconds: Infinity,
92
+ gapAheadLaps: 0,
93
+ intervalAheadLaps: 0,
94
+ leaderGapLaps: 0,
95
+ drsEligible: false,
96
+ drsActive: false,
97
+ drsZoneId: null,
98
+ drsZoneEnabled: false,
99
+ timingHistory: [],
100
+ lapTelemetry: createLapTelemetry?.(track, 0, gridDistance) ?? null,
101
+ drsDetection: {},
102
+ canAttack: true,
103
+ trackState: start,
104
+ contactCooldown: 0,
105
+ tireEnergy: 100,
106
+ usedTireCompounds: [driver.tire ?? ['M', 'H', 'S'][index % 3]],
107
+ automaticPitIntentEnabled: true,
108
+ };
109
+ }
110
+
111
+ export function clearCarDrsState(car) {
112
+ car.drsZoneId = null;
113
+ car.drsZoneEnabled = false;
114
+ car.drsActive = false;
115
+ car.drsEligible = false;
116
+ car.drsDetection = {};
117
+ }
118
+
119
+ export function applyExternalCarState(car, partial, context) {
120
+ const {
121
+ cars,
122
+ computeLap,
123
+ nearestDistanceOnRoute,
124
+ progressDelta,
125
+ raceControl,
126
+ releaseRaceStart,
127
+ resetLapTelemetry,
128
+ resetTimingHistory,
129
+ resetTimingLineCrossings,
130
+ time,
131
+ totalLaps,
132
+ track,
133
+ } = context;
134
+ const hasExplicitRaceDistance = partial.raceDistance != null;
135
+ const nextPartial = { ...partial };
136
+ if (raceControl.finished && (car.destroyed || car.outOfRace)) {
137
+ if (partial.destroyed === false) nextPartial.destroyed = car.destroyed;
138
+ if (partial.outOfRace === false) nextPartial.outOfRace = car.outOfRace;
139
+ }
140
+ Object.assign(car, nextPartial);
141
+ if (
142
+ !raceControl.finished &&
143
+ nextPartial.destroyed === false &&
144
+ nextPartial.outOfRace === false
145
+ ) {
146
+ clearCarDnf(car);
147
+ car.destroyReason = null;
148
+ car.destroyedAt = null;
149
+ car.dnfReason = null;
150
+ car.dnfAt = null;
151
+ }
152
+ if (
153
+ nextPartial.x != null ||
154
+ nextPartial.y != null ||
155
+ nextPartial.progress != null ||
156
+ nextPartial.raceDistance != null
157
+ ) {
158
+ car.gridLocked = false;
159
+ if (raceControl.mode === 'pre-start' && cars.every((item) => !item.gridLocked)) {
160
+ releaseRaceStart();
161
+ }
162
+ }
163
+ car.speed = clamp(car.speed, 0, VEHICLE_LIMITS.maxSpeed);
164
+ car.heading = normalizeAngle(car.heading);
165
+ const centerState = nearestTrackStateForCar(track, car, car, car.progress ?? car.raceDistance);
166
+ applyWheelSurfaceState(car, track, { centerState });
167
+ if (
168
+ nextPartial.desiredOffset == null &&
169
+ (nextPartial.x != null || nextPartial.y != null || nextPartial.progress != null || nextPartial.raceDistance != null)
170
+ ) {
171
+ car.desiredOffset = centerState.signedOffset ?? 0;
172
+ }
173
+ if ((nextPartial.x != null || nextPartial.y != null) && car.pitStop?.route) {
174
+ car.pitStop.routeProgress = nearestDistanceOnRoute(car.pitStop.route, car, car.pitStop.routeProgress ?? 0);
175
+ }
176
+ if (!hasExplicitRaceDistance) {
177
+ const delta = progressDelta(car.trackState.distance, car.progress ?? car.trackState.distance, track.length);
178
+ car.raceDistance = (car.raceDistance ?? car.trackState.distance) + delta;
179
+ }
180
+ car.progress = car.trackState.distance;
181
+ car.lap = computeLap(car.raceDistance);
182
+ clearCarDrsState(car);
183
+ resetTimingHistory(car, time);
184
+ resetTimingLineCrossings(car, time);
185
+ resetLapTelemetry(car, time, track, totalLaps);
186
+ }
@@ -0,0 +1,150 @@
1
+ import { getVehicleGeometryState } from './vehicleGeometry.js';
2
+ import { analyticWheelState, wheelFullyOutside } from './mainTrackWheelSurface.js';
3
+ import { analyticPitWheelState, canUseAnalyticPitWheels, isNearPitConnector, patchSamples } from './pitWheelSurface.js';
4
+ import { getEffectiveSurface, worstState } from './surfacePriority.js';
5
+ import { nearestTrackStateForCar, pitOverrideAllowedForCar } from '../track/trackStatePolicy.js';
6
+
7
+ function finiteSignatureValue(value) {
8
+ return Number.isFinite(value) ? Number(value).toFixed(4) : '';
9
+ }
10
+
11
+ function centerStateSignature(centerState) {
12
+ if (!centerState) return 'auto';
13
+ return [
14
+ finiteSignatureValue(centerState.distance),
15
+ finiteSignatureValue(centerState.signedOffset),
16
+ finiteSignatureValue(centerState.crossTrackError),
17
+ centerState.surface ?? '',
18
+ centerState.inPitLane ? 1 : 0,
19
+ centerState.pitLanePart ?? '',
20
+ centerState.pitBoxId ?? '',
21
+ ].join(':');
22
+ }
23
+
24
+ export { getEffectiveSurface } from './surfacePriority.js';
25
+
26
+ export function isWholeCarOutsideTrackLimits(wheels = [], track, relaxedMargin = 0) {
27
+ if (!wheels.length || !track) {
28
+ return {
29
+ violating: false,
30
+ side: 0,
31
+ outsideBy: 0,
32
+ };
33
+ }
34
+ const trackLimit = track.width / 2 + relaxedMargin;
35
+ const right = wheels.every((wheel) => wheel.outsideSide === 1);
36
+ const left = wheels.every((wheel) => wheel.outsideSide === -1);
37
+ if (!right && !left) {
38
+ return {
39
+ violating: false,
40
+ side: 0,
41
+ outsideBy: 0,
42
+ };
43
+ }
44
+ const side = right ? 1 : -1;
45
+ const outsideBy = right
46
+ ? Math.min(...wheels.map((wheel) => wheel.minimumSignedOffset)) - trackLimit
47
+ : -trackLimit - Math.max(...wheels.map((wheel) => wheel.maximumSignedOffset));
48
+
49
+ return {
50
+ violating: outsideBy > 0,
51
+ side,
52
+ outsideBy: Math.max(0, outsideBy),
53
+ };
54
+ }
55
+
56
+ export function calculateWheelSurfaceState({ car, track, centerState: providedCenterState = null }) {
57
+ const geometry = getVehicleGeometryState(car);
58
+ const trackLimit = track.width / 2;
59
+ const allowPitOverride = pitOverrideAllowedForCar(car);
60
+ const centerState = providedCenterState == null
61
+ ? nearestTrackStateForCar(track, car)
62
+ : !allowPitOverride && providedCenterState.inPitLane
63
+ ? nearestTrackStateForCar(track, car, car, car.progress, { allowPitOverride: false })
64
+ : providedCenterState;
65
+ const useAnalyticPitSampling = canUseAnalyticPitWheels(geometry, centerState);
66
+ const useFullSampling = !useAnalyticPitSampling && Boolean(centerState.inPitLane || isNearPitConnector(track, centerState));
67
+ const wheels = useAnalyticPitSampling
68
+ ? geometry.contactPatches.map((patch) => analyticPitWheelState(patch, centerState))
69
+ : useFullSampling
70
+ ? geometry.contactPatches.map((patch) => {
71
+ const sampleState = (point) => nearestTrackStateForCar(track, car, point, car.progress);
72
+ const sampledStates = patchSamples(patch).map(sampleState);
73
+ const state = worstState(sampledStates);
74
+ const offsets = sampledStates.map((sample) => sample.signedOffset);
75
+ const outside = wheelFullyOutside(sampledStates, trackLimit);
76
+
77
+ return {
78
+ id: patch.id,
79
+ x: patch.center.x,
80
+ y: patch.center.y,
81
+ signedOffset: state.signedOffset,
82
+ crossTrackError: state.crossTrackError,
83
+ surface: state.surface,
84
+ onTrack: Boolean(state.onTrack),
85
+ inPitLane: Boolean(state.inPitLane),
86
+ pitLanePart: state.pitLanePart ?? null,
87
+ pitBoxId: state.pitBoxId ?? null,
88
+ minimumSignedOffset: Math.min(...offsets),
89
+ maximumSignedOffset: Math.max(...offsets),
90
+ fullyOutsideWhiteLine: outside.fullyOutsideWhiteLine,
91
+ outsideSide: outside.outsideSide,
92
+ sampledStates,
93
+ };
94
+ })
95
+ : geometry.contactPatches.map((patch) => analyticWheelState(patch, centerState, track, trackLimit));
96
+ const effectiveSurface = getEffectiveSurface(wheels);
97
+ const representative = wheels.reduce((best, wheel) => (
98
+ Math.abs(wheel.signedOffset) > Math.abs(best.signedOffset) ? wheel : best
99
+ ), wheels[0]);
100
+ const trackLimits = isWholeCarOutsideTrackLimits(wheels, track);
101
+
102
+ return {
103
+ wheels,
104
+ effectiveSurface,
105
+ trackLimits,
106
+ sampleMode: useAnalyticPitSampling ? 'pit-analytic' : useFullSampling ? 'full' : 'analytic',
107
+ representativeState: representative ? {
108
+ ...centerState,
109
+ surface: effectiveSurface,
110
+ onTrack: wheels.every((wheel) => wheel.onTrack || wheel.inPitLane),
111
+ inPitLane: wheels.every((wheel) => wheel.inPitLane),
112
+ signedOffset: representative.signedOffset,
113
+ crossTrackError: representative.crossTrackError,
114
+ } : nearestTrackStateForCar(track, car),
115
+ };
116
+ }
117
+
118
+ export function applyWheelSurfaceState(car, track, options = {}) {
119
+ const geometry = getVehicleGeometryState(car);
120
+ const cacheKey = {
121
+ track,
122
+ geometrySignature: geometry.signature,
123
+ centerSignature: centerStateSignature(options.centerState),
124
+ };
125
+ const cached = car.wheelSurfaceCache;
126
+ if (
127
+ cached?.track === cacheKey.track &&
128
+ cached.geometrySignature === cacheKey.geometrySignature &&
129
+ cached.centerSignature === cacheKey.centerSignature
130
+ ) {
131
+ car.wheelStates = cached.result.wheels;
132
+ car.trackLimitState = cached.result.trackLimits;
133
+ car.trackState = cached.trackState;
134
+ return cached.result;
135
+ }
136
+
137
+ const result = calculateWheelSurfaceState({ car, track, centerState: options.centerState });
138
+ car.wheelStates = result.wheels;
139
+ car.trackLimitState = result.trackLimits;
140
+ car.trackState = {
141
+ ...result.representativeState,
142
+ wheelSurface: result.effectiveSurface,
143
+ };
144
+ car.wheelSurfaceCache = {
145
+ ...cacheKey,
146
+ result,
147
+ trackState: car.trackState,
148
+ };
149
+ return result;
150
+ }
@@ -0,0 +1 @@
1
+ export * from './vehicle/vehicleGeometry.js';
@@ -1,104 +1 @@
1
- import { clamp, normalizeAngle } from './simMath.js';
2
-
3
- const G = 9.80665;
4
-
5
- export const VEHICLE_LIMITS = {
6
- wheelbase: 3.65,
7
- maxSteer: 0.56,
8
- steerRate: 2.35,
9
- maxSpeed: 192,
10
- carLength: 66,
11
- carWidth: 21,
12
- };
13
-
14
- const SURFACE_MODEL = {
15
- track: { grip: 1, drag: 0, rollingResistance: 0 },
16
- kerb: { grip: 0.92, drag: 0.12, rollingResistance: 0.045 },
17
- gravel: { grip: 0.43, drag: 2.6, rollingResistance: 0.48 },
18
- grass: { grip: 0.34, drag: 1.9, rollingResistance: 0.36 },
19
- barrier: { grip: 0.18, drag: 9, rollingResistance: 1.2 },
20
- };
21
-
22
- function accelerationLimit(speed, surfaceGrip) {
23
- const speedRatio = clamp(speed / VEHICLE_LIMITS.maxSpeed, 0, 1);
24
- return surfaceGrip * (17 * (1 - speedRatio ** 2.1) + 1.2);
25
- }
26
-
27
- function brakingLimit(speed, surfaceGrip) {
28
- const speedRatio = clamp(speed / VEHICLE_LIMITS.maxSpeed, 0, 1);
29
- return surfaceGrip * (20 + speedRatio * 14);
30
- }
31
-
32
- export function integrateVehiclePhysics(car, controls, dt) {
33
- const steeringTarget = clamp(controls.steering ?? 0, -VEHICLE_LIMITS.maxSteer, VEHICLE_LIMITS.maxSteer);
34
- const steerDelta = clamp(
35
- steeringTarget - car.steeringAngle,
36
- -VEHICLE_LIMITS.steerRate * dt,
37
- VEHICLE_LIMITS.steerRate * dt,
38
- );
39
- car.steeringAngle += steerDelta;
40
-
41
- const throttle = clamp(controls.throttle ?? 0, 0, 1);
42
- const brake = clamp(controls.brake ?? 0, 0, 1);
43
- const surface = SURFACE_MODEL[car.trackState?.surface] ?? SURFACE_MODEL.track;
44
- const surfaceGrip = surface.grip;
45
- const dragMultiplier = car.drsActive ? 0.42 : 1;
46
- const speedRatio = clamp(car.speed / VEHICLE_LIMITS.maxSpeed, 0, 1);
47
- const engineForce = throttle * car.powerNewtons * Math.max(0.12, 1 - speedRatio ** 1.15);
48
- const brakeForce = brake * car.brakeNewtons;
49
- const dragForce = (car.dragCoefficient * dragMultiplier * 0.12 + surface.drag) * car.speed * car.speed;
50
- const rollingForce = surface.rollingResistance * car.mass * G;
51
- const driveAcceleration = clamp(engineForce / car.mass, 0, accelerationLimit(car.speed, surfaceGrip));
52
- const brakeDeceleration = clamp(brakeForce / car.mass, 0, brakingLimit(car.speed, surfaceGrip));
53
- const dragDeceleration = (dragForce + rollingForce) / car.mass;
54
- const acceleration = driveAcceleration - brakeDeceleration - dragDeceleration;
55
-
56
- car.speed = clamp(car.speed + acceleration * dt, 0, VEHICLE_LIMITS.maxSpeed);
57
-
58
- const rawYawRate = car.speed / VEHICLE_LIMITS.wheelbase * Math.tan(car.steeringAngle);
59
- const downforceGrip = car.downforceCoefficient * car.speed * car.speed / car.mass;
60
- const tyreConditionGrip = clamp(0.82 + (car.tireEnergy ?? 100) / 560, 0.82, 1);
61
- const maxYawRate = ((car.tireGrip * tyreConditionGrip * G + downforceGrip) * surfaceGrip) / Math.max(car.speed, 8);
62
- car.yawRate = clamp(rawYawRate, -maxYawRate, maxYawRate);
63
- car.turnRadius = Math.abs(car.yawRate) < 0.001 ? Infinity : car.speed / Math.abs(car.yawRate);
64
- car.heading = normalizeAngle(car.heading + car.yawRate * dt);
65
- car.x += Math.cos(car.heading) * car.speed * dt;
66
- car.y += Math.sin(car.heading) * car.speed * dt;
67
- car.throttle = throttle;
68
- car.brake = brake;
69
- car.lateralAcceleration = car.speed * car.yawRate;
70
- car.steerSaturation = rawYawRate === 0 ? 0 : Math.abs(car.yawRate / rawYawRate);
71
- const tyreLoad = Math.abs(car.lateralAcceleration) / G;
72
- const wearRate = 0.035 + tyreLoad * 0.11 + brake * 0.07 + throttle * 0.025;
73
- car.tireEnergy = clamp((car.tireEnergy ?? 100) - wearRate * dt, 38, 100);
74
-
75
- return car;
76
- }
77
-
78
- export function getCarCorners(car) {
79
- const halfLength = VEHICLE_LIMITS.carLength / 2;
80
- const halfWidth = VEHICLE_LIMITS.carWidth / 2;
81
- const cos = Math.cos(car.heading);
82
- const sin = Math.sin(car.heading);
83
- const forward = { x: cos, y: sin };
84
- const right = { x: -sin, y: cos };
85
-
86
- return [
87
- {
88
- x: car.x + forward.x * halfLength + right.x * halfWidth,
89
- y: car.y + forward.y * halfLength + right.y * halfWidth,
90
- },
91
- {
92
- x: car.x + forward.x * halfLength - right.x * halfWidth,
93
- y: car.y + forward.y * halfLength - right.y * halfWidth,
94
- },
95
- {
96
- x: car.x - forward.x * halfLength - right.x * halfWidth,
97
- y: car.y - forward.y * halfLength - right.y * halfWidth,
98
- },
99
- {
100
- x: car.x - forward.x * halfLength + right.x * halfWidth,
101
- y: car.y - forward.y * halfLength + right.y * halfWidth,
102
- },
103
- ];
104
- }
1
+ export * from './vehicle/vehiclePhysics.js';
@@ -0,0 +1,224 @@
1
+ const DEFAULT_WARMUP_POLICY = 'config-change';
2
+ const DEFAULT_WARMUP_STEPS = Object.freeze({
3
+ simulation: 20,
4
+ browser: 20,
5
+ environment: 20,
6
+ });
7
+ const SUPPORTED_WARMUP_POLICIES = new Set(['config-change', 'always', 'never']);
8
+ const MAX_CACHE_ENTRIES_PER_SURFACE = 256;
9
+ const warmupFingerprintCache = new Map();
10
+
11
+ export function normalizeWarmupOptions(input, surface = 'simulation') {
12
+ const fallbackSteps = DEFAULT_WARMUP_STEPS[surface] ?? DEFAULT_WARMUP_STEPS.simulation;
13
+ if (input === false) {
14
+ return {
15
+ enabled: false,
16
+ policy: 'never',
17
+ steps: fallbackSteps,
18
+ surface,
19
+ };
20
+ }
21
+ if (input === true || input == null) {
22
+ return {
23
+ enabled: true,
24
+ policy: DEFAULT_WARMUP_POLICY,
25
+ steps: fallbackSteps,
26
+ surface,
27
+ };
28
+ }
29
+ const enabled = input.enabled == null ? true : Boolean(input.enabled);
30
+ const policy = normalizeWarmupPolicy(input.policy, enabled);
31
+ return {
32
+ enabled,
33
+ policy,
34
+ steps: normalizeWarmupSteps(input.steps, fallbackSteps),
35
+ surface: typeof input.surface === 'string' && input.surface.length > 0 ? input.surface : surface,
36
+ };
37
+ }
38
+
39
+ export function withWarmupSurfaceOptions(options = {}, surface = 'simulation') {
40
+ return {
41
+ ...options,
42
+ warmup: normalizeWarmupOptions(options.warmup, surface),
43
+ };
44
+ }
45
+
46
+ export function disableWarmupOptions(options = {}, surface = 'simulation') {
47
+ const warmup = normalizeWarmupOptions(options.warmup, surface);
48
+ return {
49
+ ...options,
50
+ warmup: {
51
+ ...warmup,
52
+ enabled: false,
53
+ policy: 'never',
54
+ surface,
55
+ },
56
+ };
57
+ }
58
+
59
+ export function runWarmupWithGuard({
60
+ options = {},
61
+ surface = 'simulation',
62
+ execute,
63
+ } = {}) {
64
+ const warmup = normalizeWarmupOptions(options.warmup, surface);
65
+ const fingerprint = createWarmupFingerprint(options, {
66
+ surface: warmup.surface,
67
+ steps: warmup.steps,
68
+ });
69
+ if (!shouldRunWarmup(warmup, fingerprint)) {
70
+ return { ran: false, warmup, fingerprint };
71
+ }
72
+ try {
73
+ execute?.({ warmup, fingerprint });
74
+ markWarmupComplete(warmup.surface, fingerprint);
75
+ return { ran: true, warmup, fingerprint };
76
+ } catch {
77
+ return { ran: false, warmup, fingerprint };
78
+ }
79
+ }
80
+
81
+ export function resetWarmupRuntimeCache() {
82
+ warmupFingerprintCache.clear();
83
+ }
84
+
85
+ function shouldRunWarmup(warmup, fingerprint) {
86
+ if (!warmup.enabled) return false;
87
+ if (warmup.policy === 'never') return false;
88
+ if (warmup.policy === 'always') return true;
89
+ return !hasWarmupFingerprint(warmup.surface, fingerprint);
90
+ }
91
+
92
+ function hasWarmupFingerprint(surface, fingerprint) {
93
+ return warmupFingerprintCache.get(surface)?.has(fingerprint) ?? false;
94
+ }
95
+
96
+ function markWarmupComplete(surface, fingerprint) {
97
+ let entries = warmupFingerprintCache.get(surface);
98
+ if (!entries) {
99
+ entries = new Map();
100
+ warmupFingerprintCache.set(surface, entries);
101
+ }
102
+ entries.set(fingerprint, Date.now());
103
+ while (entries.size > MAX_CACHE_ENTRIES_PER_SURFACE) {
104
+ const firstKey = entries.keys().next().value;
105
+ entries.delete(firstKey);
106
+ }
107
+ }
108
+
109
+ function createWarmupFingerprint(options, { surface, steps }) {
110
+ return stableStringify({
111
+ surface,
112
+ steps,
113
+ seed: finiteOrNull(options.seed),
114
+ trackSeed: finiteOrNull(options.trackSeed),
115
+ totalLaps: finiteOrNull(options.totalLaps),
116
+ trackGeneration: options.trackGeneration ?? null,
117
+ track: summarizeTrack(options.track),
118
+ physicsMode: options.physicsMode ?? null,
119
+ trackQueryIndex: options.trackQueryIndex !== false,
120
+ rules: options.rules ?? null,
121
+ participantInteractions: options.participantInteractions ?? null,
122
+ replayGhosts: summarizeReplayGhosts(options.replayGhosts),
123
+ drivers: summarizeDrivers(options.drivers),
124
+ entries: summarizeEntries(options.entries),
125
+ controlledDrivers: summarizeStringArray(options.controlledDrivers),
126
+ sensors: options.sensors ?? null,
127
+ sensorsByDriver: summarizeSensorsByDriver(options.sensorsByDriver),
128
+ scenario: summarizeScenario(options.scenario),
129
+ frameSkip: finiteOrNull(options.frameSkip),
130
+ });
131
+ }
132
+
133
+ function normalizeWarmupPolicy(value, enabled) {
134
+ if (!enabled) return 'never';
135
+ if (typeof value !== 'string') return DEFAULT_WARMUP_POLICY;
136
+ return SUPPORTED_WARMUP_POLICIES.has(value) ? value : DEFAULT_WARMUP_POLICY;
137
+ }
138
+
139
+ function normalizeWarmupSteps(value, fallback) {
140
+ if (value == null) return fallback;
141
+ const steps = Math.floor(Number(value));
142
+ if (!Number.isFinite(steps) || steps < 1) return fallback;
143
+ return steps;
144
+ }
145
+
146
+ function summarizeTrack(track) {
147
+ if (!track || typeof track !== 'object') return null;
148
+ const samples = Array.isArray(track.samples) ? track.samples.length : null;
149
+ const centerlineControls = Array.isArray(track.centerlineControls) ? track.centerlineControls.length : null;
150
+ const drsZones = Array.isArray(track.drsZones) ? track.drsZones.length : null;
151
+ return {
152
+ name: track.name ?? null,
153
+ seed: finiteOrNull(track.seed),
154
+ length: finiteOrNull(track.length),
155
+ width: finiteOrNull(track.width),
156
+ sampleCount: finiteOrNull(track.sampleCount) ?? samples,
157
+ centerlineControls,
158
+ drsZones,
159
+ hasPitLane: Boolean(track.pitLane?.enabled),
160
+ };
161
+ }
162
+
163
+ function summarizeReplayGhosts(replayGhosts) {
164
+ if (!Array.isArray(replayGhosts)) return null;
165
+ return replayGhosts.map((ghost) => ({
166
+ id: ghost?.id ?? null,
167
+ visible: ghost?.visible !== false,
168
+ samples: Array.isArray(ghost?.trajectory) ? ghost.trajectory.length : 0,
169
+ }));
170
+ }
171
+
172
+ function summarizeDrivers(drivers) {
173
+ if (!Array.isArray(drivers)) return null;
174
+ return {
175
+ count: drivers.length,
176
+ ids: drivers.map((driver) => driver?.id ?? null),
177
+ };
178
+ }
179
+
180
+ function summarizeEntries(entries) {
181
+ if (!Array.isArray(entries)) return null;
182
+ return {
183
+ count: entries.length,
184
+ driverIds: entries.map((entry) => entry?.driverId ?? null),
185
+ };
186
+ }
187
+
188
+ function summarizeStringArray(values) {
189
+ if (!Array.isArray(values)) return null;
190
+ return values.map((value) => String(value));
191
+ }
192
+
193
+ function summarizeSensorsByDriver(sensorsByDriver) {
194
+ if (!sensorsByDriver || typeof sensorsByDriver !== 'object') return null;
195
+ const ids = Object.keys(sensorsByDriver);
196
+ return {
197
+ count: ids.length,
198
+ ids: ids.sort(),
199
+ };
200
+ }
201
+
202
+ function summarizeScenario(scenario) {
203
+ if (!scenario || typeof scenario !== 'object') return null;
204
+ return {
205
+ participants: scenario.participants ?? null,
206
+ preset: scenario.preset ?? null,
207
+ placements: scenario.placements ?? null,
208
+ traffic: scenario.traffic ?? null,
209
+ };
210
+ }
211
+
212
+ function stableStringify(value) {
213
+ if (value == null) return 'null';
214
+ if (typeof value === 'number' || typeof value === 'boolean') return JSON.stringify(value);
215
+ if (typeof value === 'string') return JSON.stringify(value);
216
+ if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(',')}]`;
217
+ if (typeof value !== 'object') return JSON.stringify(String(value));
218
+ const keys = Object.keys(value).sort();
219
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
220
+ }
221
+
222
+ function finiteOrNull(value) {
223
+ return Number.isFinite(Number(value)) ? Number(value) : null;
224
+ }
@@ -0,0 +1 @@
1
+ export * from './vehicle/wheelSurface.js';