@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,166 @@
1
+ import { clamp, TWO_PI } from '../simMath.js';
2
+ import { kphToSimSpeed, metersToSimUnits, simSpeedToKph, simUnitsToMeters } from '../units.js';
3
+ import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
4
+ import { DEFEND_MIN_OFFSET, DEFEND_REAR_GAP, LANE_EDGE_CLEARANCE_TARGET, LANE_OFFSETS, OVERTAKE_COMMIT_FRAMES, OVERTAKE_GAP_AGGRESSION, OVERTAKE_GAP_BASE, OVERTAKE_LATERAL_MIN, OVERTAKE_LATERAL_REWARD, PASS_SIDE_AGGRESSION, PASS_SIDE_BASE, TRAFFIC_GAP_AHEAD, TRAFFIC_REAR_WINDOW, TRAFFIC_SIDE_GAP, TRAFFIC_SIDE_OVERLAP } from './driverControlConstants.js';
5
+ import { findDefensiveThreat, findLaneTrafficAhead, findLaneTrafficBeside, scanNearbyTraffic } from './trafficScan.js';
6
+
7
+ export function planRacingLine(car, orderIndex, race) {
8
+ const aggression = car.aggression ?? car.personality?.baseAggression ?? 0.5;
9
+ const riskTolerance = car.personality?.riskTolerance ?? aggression;
10
+ const trackLimit = race.track.width / 2 - VEHICLE_LIMITS.carWidth * clamp(1.55 - aggression * 0.28, 1.26, 1.55);
11
+ const preferred = Math.sin((car.index / Math.max(1, race.cars.length)) * TWO_PI) * metersToSimUnits(1.1);
12
+ const currentOffset = clamp(car.desiredOffset ?? preferred, -trackLimit, trackLimit);
13
+ const ahead = race.orderedCars[orderIndex - 1];
14
+ const traffic = scanNearbyTraffic(car, race);
15
+ const rearThreat = findDefensiveThreat(traffic);
16
+ const attackPlan = planAttackCommitment(car, ahead, traffic, trackLimit, aggression, race);
17
+
18
+ let bestOffset = currentOffset;
19
+ let bestScore = -Infinity;
20
+
21
+ LANE_OFFSETS.forEach((rawOffset) => {
22
+ const offset = clamp(rawOffset, -trackLimit, trackLimit);
23
+ const edgeClearance = trackLimit - Math.abs(offset);
24
+ let score = 80;
25
+ score -= Math.abs(offset - preferred) * 0.18;
26
+ score -= Math.abs(offset - currentOffset) * (0.09 - aggression * 0.025);
27
+ score -= Math.max(0, LANE_EDGE_CLEARANCE_TARGET - edgeClearance) * (0.92 - aggression * 0.42);
28
+
29
+ traffic.forEach((entry) => {
30
+ const lateral = Math.abs(entry.signedOffset - offset);
31
+ if (entry.gap > 0 && entry.gap < TRAFFIC_GAP_AHEAD) {
32
+ const overlapRisk = clamp(TRAFFIC_SIDE_OVERLAP - lateral, 0, TRAFFIC_SIDE_OVERLAP);
33
+ score -= overlapRisk * (TRAFFIC_GAP_AHEAD - entry.gap) * 0.038 * (1 - riskTolerance * 0.34);
34
+ if (entry.gap < OVERTAKE_GAP_BASE + aggression * OVERTAKE_GAP_AGGRESSION && lateral > OVERTAKE_LATERAL_MIN - aggression * metersToSimUnits(4)) {
35
+ score += Math.min(metersToSimUnits(14) + aggression * metersToSimUnits(8.5), lateral - (OVERTAKE_LATERAL_REWARD - aggression * metersToSimUnits(3))) * (0.62 + aggression * 0.82);
36
+ }
37
+ } else if (entry.gap <= 0 && entry.gap > -TRAFFIC_SIDE_OVERLAP) {
38
+ const sideOverlapRisk = clamp(TRAFFIC_SIDE_OVERLAP - lateral, 0, TRAFFIC_SIDE_OVERLAP);
39
+ score -= sideOverlapRisk * (TRAFFIC_SIDE_OVERLAP + entry.gap) * 0.052 * (1 - riskTolerance * 0.22);
40
+ }
41
+ });
42
+
43
+ if (ahead && car.gapAhead < TRAFFIC_GAP_AHEAD) {
44
+ const passSide = attackPlan?.targetOffset ?? clamp(
45
+ (ahead.trackState.signedOffset * -0.65) + (car.index % 2 === 0 ? -1 : 1) * (PASS_SIDE_BASE + aggression * PASS_SIDE_AGGRESSION),
46
+ -trackLimit,
47
+ trackLimit,
48
+ );
49
+ const attackPressure = attackPlan?.pressure ?? 0.45;
50
+ score -= Math.abs(offset - passSide) * (0.12 + aggression * 0.16) * attackPressure;
51
+ if (attackPlan && Math.sign(offset || passSide) === attackPlan.side && Math.abs(offset - ahead.trackState.signedOffset) > OVERTAKE_LATERAL_MIN) {
52
+ score += metersToSimUnits(4 + aggression * 5) * attackPressure;
53
+ }
54
+ }
55
+
56
+ const defenseIntensity = clamp((aggression - 0.5) / 0.3, 0, 1) *
57
+ clamp((Math.max(riskTolerance, aggression) - 0.42) / 0.4, 0, 1);
58
+ if (rearThreat && defenseIntensity > 0.04) {
59
+ const threatSide = Math.sign(rearThreat.signedOffset || currentOffset || (car.index % 2 === 0 ? 1 : -1));
60
+ const defendOffset = clamp(
61
+ threatSide * Math.max(Math.abs(rearThreat.signedOffset) + metersToSimUnits(0.8), DEFEND_MIN_OFFSET),
62
+ -trackLimit,
63
+ trackLimit,
64
+ );
65
+ const closingPressure = clamp((DEFEND_REAR_GAP + rearThreat.gap) / DEFEND_REAR_GAP, 0, 1);
66
+ score -= Math.abs(offset - defendOffset) * (0.75 + aggression * 0.45) * closingPressure * defenseIntensity;
67
+ if (Math.sign(offset) === threatSide && Math.abs(offset) >= DEFEND_MIN_OFFSET) {
68
+ score += metersToSimUnits(10) * (0.42 + aggression * 0.36) * closingPressure * defenseIntensity;
69
+ }
70
+ }
71
+
72
+ if (score > bestScore) {
73
+ bestScore = score;
74
+ bestOffset = offset;
75
+ }
76
+ });
77
+
78
+ const laneChangeRate = metersToSimUnits(0.95 + car.racecraft * 0.32 + aggression * 0.78);
79
+ car.desiredOffset = currentOffset + clamp(bestOffset - currentOffset, -laneChangeRate, laneChangeRate);
80
+
81
+ return {
82
+ offset: car.desiredOffset,
83
+ sameLaneAhead: findLaneTrafficAhead(traffic, car.desiredOffset, TRAFFIC_GAP_AHEAD),
84
+ actualLaneAhead: findLaneTrafficAhead(traffic, car.trackState?.signedOffset ?? car.desiredOffset, TRAFFIC_GAP_AHEAD),
85
+ sideRisk: findLaneTrafficBeside(traffic, car.desiredOffset),
86
+ attackCommitted: Boolean(attackPlan),
87
+ };
88
+ }
89
+
90
+ export function calculatePlannedTrafficPenalty(entry, plannedPassingOverlap) {
91
+ return clamp(
92
+ simUnitsToMeters(TRAFFIC_GAP_AHEAD - entry.gap) * (plannedPassingOverlap ? 0.08 : 0.16),
93
+ 0,
94
+ plannedPassingOverlap ? 24 : 32,
95
+ );
96
+ }
97
+
98
+ export function calculateActualOverlapPenalty(car, entry) {
99
+ const gapMeters = simUnitsToMeters(entry.gap);
100
+ const closingKph = Math.max(0, simSpeedToKph(car.speed - (entry.speed ?? car.speed)));
101
+ const urgent = gapMeters < 12
102
+ ? (12 - gapMeters) * (2.8 + closingKph * 0.04)
103
+ : 0;
104
+
105
+ return clamp(urgent, 0, 72);
106
+ }
107
+
108
+ export function planAttackCommitment(car, ahead, traffic, trackLimit, aggression, race) {
109
+ const existingFrames = Math.max(0, Math.floor(car.attackCommitmentFrames ?? 0));
110
+ const relativeSpeed = ahead ? car.speed - ahead.speed : 0;
111
+ const closeEnough = ahead &&
112
+ car.gapAhead > 0 &&
113
+ car.gapAhead < OVERTAKE_GAP_BASE + aggression * OVERTAKE_GAP_AGGRESSION + metersToSimUnits(28);
114
+ const speedPressure = clamp(relativeSpeed / kphToSimSpeed(34), -0.4, 1);
115
+
116
+ if (closeEnough && speedPressure > -0.15) {
117
+ const aheadOffset = ahead.trackState?.signedOffset ?? 0;
118
+ const currentSide = Math.sign(car.attackSide ?? 0);
119
+ const blockedCommitment = currentSide &&
120
+ Math.sign(aheadOffset || currentSide) === currentSide &&
121
+ Math.abs(aheadOffset - (car.trackState?.signedOffset ?? 0)) < TRAFFIC_SIDE_GAP &&
122
+ Math.abs(aheadOffset) > trackLimit * 0.55;
123
+
124
+ if (!existingFrames || !currentSide || blockedCommitment) {
125
+ const leftSpace = trackLimit + aheadOffset;
126
+ const rightSpace = trackLimit - aheadOffset;
127
+ const preferredSide = leftSpace > rightSpace ? -1 : 1;
128
+ const freeSide = chooseFreerAttackSide(traffic, preferredSide);
129
+ car.attackSide = freeSide;
130
+ }
131
+ car.attackCommitmentFrames = OVERTAKE_COMMIT_FRAMES;
132
+ } else if (existingFrames > 0) {
133
+ car.attackCommitmentFrames = existingFrames - 1;
134
+ } else {
135
+ car.attackSide = 0;
136
+ car.attackCommitmentFrames = 0;
137
+ }
138
+
139
+ const side = Math.sign(car.attackSide ?? 0);
140
+ if (!side || !ahead) return null;
141
+
142
+ const targetOffset = clamp(
143
+ (ahead.trackState?.signedOffset ?? 0) + side * (PASS_SIDE_BASE + aggression * PASS_SIDE_AGGRESSION + metersToSimUnits(2.4)),
144
+ -trackLimit,
145
+ trackLimit,
146
+ );
147
+ const pressure = clamp(
148
+ 0.48 +
149
+ aggression * 0.34 +
150
+ speedPressure * 0.28 +
151
+ (car.attackCommitmentFrames ?? 0) / OVERTAKE_COMMIT_FRAMES * 0.22,
152
+ 0.45,
153
+ 1.15,
154
+ );
155
+
156
+ return { side, targetOffset, pressure };
157
+ }
158
+
159
+ export function chooseFreerAttackSide(traffic, preferredSide) {
160
+ const sideScore = (side) => traffic.reduce((score, entry) => {
161
+ if (entry.gap < -TRAFFIC_REAR_WINDOW || entry.gap > TRAFFIC_GAP_AHEAD) return score;
162
+ const sameSide = Math.sign(entry.signedOffset || side) === side;
163
+ return score - (sameSide ? 1 : 0) * (1 - clamp(Math.abs(entry.gap) / TRAFFIC_GAP_AHEAD, 0, 1));
164
+ }, side === preferredSide ? 0.18 : 0);
165
+ return sideScore(1) >= sideScore(-1) ? 1 : -1;
166
+ }
@@ -0,0 +1,38 @@
1
+ import { normalizeAngle } from '../simMath.js';
2
+ import { simUnitsToMeters } from '../units.js';
3
+
4
+ export function analyzeTrackEdgeMotion(car, race) {
5
+ const state = car.trackState ?? {};
6
+ const signedOffset = Number(state.signedOffset ?? 0);
7
+ const side = Math.sign(signedOffset);
8
+ const heading = Number(car.heading ?? 0);
9
+ const normalX = Number(state.normalX ?? 0);
10
+ const normalY = Number(state.normalY ?? 0);
11
+ const velocity = resolveVelocity(car);
12
+ const signedLateralVelocity = velocity.x * normalX + velocity.y * normalY;
13
+ const outwardVelocity = side === 0 ? 0 : signedLateralVelocity * side;
14
+ const headingDelta = normalizeAngle(heading - Number(state.heading ?? heading));
15
+ const trackLimit = race?.track?.width != null ? race.track.width / 2 : 0;
16
+
17
+ return {
18
+ side,
19
+ signedOffset,
20
+ headingDelta,
21
+ headingOutward: side !== 0 && Math.sign(headingDelta) === side,
22
+ outwardSpeedMps: simUnitsToMeters(Math.max(0, outwardVelocity)),
23
+ inwardSpeedMps: simUnitsToMeters(Math.max(0, -outwardVelocity)),
24
+ distanceFromRoadMeters: simUnitsToMeters(Math.max(0, Number(state.crossTrackError ?? 0) - trackLimit)),
25
+ };
26
+ }
27
+
28
+ function resolveVelocity(car) {
29
+ if (Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)) {
30
+ return { x: car.velocityX, y: car.velocityY };
31
+ }
32
+ const heading = Number(car.heading ?? 0);
33
+ const speed = Number(car.speed ?? 0);
34
+ return {
35
+ x: Math.cos(heading) * speed,
36
+ y: Math.sin(heading) * speed,
37
+ };
38
+ }
@@ -0,0 +1,213 @@
1
+ import { clamp } from '../simMath.js';
2
+ import { kphToSimSpeed, simUnitsToMeters } from '../units.js';
3
+ import { offsetTrackPoint, pointAt } from '../track/trackModel.js';
4
+ import { REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_MAX } from './driverControlConstants.js';
5
+ import { angleToPoint } from './driverMath.js';
6
+ import { createDriverInput } from './driverInput.js';
7
+ import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
8
+ import { analyzeTrackEdgeMotion } from './recoveryDynamics.js';
9
+
10
+ export function decideRejoinControls(car, race) {
11
+ if (race.physicsMode === 'simulator') {
12
+ return decideSimulatorRejoinControls(car, race);
13
+ }
14
+ return decideArcadeRejoinControls(car, race);
15
+ }
16
+
17
+ export function decideArcadeRejoinControls(car, race) {
18
+ return decideRejoinControlsForMode(car, race, {
19
+ simulatorMode: false,
20
+ inwardOffsetRatio: 0.34,
21
+ surfaceTargetSpeeds: {
22
+ track: 88,
23
+ kerb: 72,
24
+ gravel: 56,
25
+ grass: 48,
26
+ barrier: 34,
27
+ },
28
+ lowSpeedRecovery: {
29
+ barrier: 0.34,
30
+ default: 0.18,
31
+ },
32
+ throttleLimits: {
33
+ barrier: 0.68,
34
+ gravel: 0.46,
35
+ grass: 0.42,
36
+ default: 0.42,
37
+ },
38
+ brakeResponseKph: 30,
39
+ onTrackBrakeLimit: 0.46,
40
+ offTrackBrakeLimit: 0.74,
41
+ steerGain: 1.65,
42
+ outwardBrakeStartMps: 1.2,
43
+ outwardBrakeGain: 0.08,
44
+ misalignmentBrakeGain: 0.06,
45
+ slideThrottleDamping: 0.36,
46
+ offTrackForwardTargetScale: 0.42,
47
+ lowSpeedForwardTargetScale: 0,
48
+ lowSpeedOffTrackSteerLimit: 0.52,
49
+ lowSpeedRecoveryThrottle: 0.32,
50
+ lowSpeedOutwardBrakeStartMps: 1.2,
51
+ lowSpeedCrawlOutwardToleranceMps: 0,
52
+ lowSpeedHeadingOutwardThrottleLimit: 1,
53
+ stabilizeLowSpeedOutward: false,
54
+ stabilizeRejoinHoldOutsideRoad: false,
55
+ unsettledScale: () => 1,
56
+ });
57
+ }
58
+
59
+ export function decideSimulatorRejoinControls(car, race) {
60
+ return decideRejoinControlsForMode(car, race, {
61
+ simulatorMode: true,
62
+ inwardOffsetRatio: 0.24,
63
+ surfaceTargetSpeeds: {
64
+ track: 78,
65
+ kerb: 58,
66
+ gravel: 62,
67
+ grass: 52,
68
+ barrier: 22,
69
+ },
70
+ lowSpeedRecovery: {
71
+ barrier: 0.42,
72
+ default: 0.36,
73
+ },
74
+ throttleLimits: {
75
+ barrier: 0.5,
76
+ gravel: 0.82,
77
+ grass: 0.76,
78
+ default: 0.52,
79
+ },
80
+ brakeResponseKph: 24,
81
+ onTrackBrakeLimit: 0.34,
82
+ offTrackBrakeLimit: 0.24,
83
+ steerGain: 0.94,
84
+ outwardBrakeStartMps: 0.45,
85
+ outwardBrakeGain: 0.12,
86
+ misalignmentBrakeGain: 0.08,
87
+ slideThrottleDamping: 0.62,
88
+ offTrackForwardTargetScale: 0.32,
89
+ lowSpeedForwardTargetScale: 0.22,
90
+ lowSpeedOffTrackSteerLimit: 0.68,
91
+ lowSpeedRecoveryThrottle: 0.68,
92
+ lowSpeedOutwardBrakeStartMps: 0.24,
93
+ lowSpeedCrawlOutwardToleranceMps: 0.42,
94
+ lowSpeedHeadingOutwardThrottleLimit: 0.24,
95
+ stabilizeLowSpeedOutward: true,
96
+ stabilizeRejoinHoldOutsideRoad: true,
97
+ unsettledScale: (entry) => clamp(
98
+ 1 - Math.max(0, (entry.gripUsage ?? 0) - 0.55) * 0.75 - Math.abs(entry.slipAngleRadians ?? 0) * 1.65,
99
+ 0.22,
100
+ 1,
101
+ ),
102
+ });
103
+ }
104
+
105
+ function decideRejoinControlsForMode(car, race, profile) {
106
+ const simulatorMode = race.physicsMode === 'simulator';
107
+ const lookahead = clamp(car.speed * (profile.simulatorMode ? 0.58 : 0.72) + REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_MAX);
108
+ const edgeMotion = analyzeTrackEdgeMotion(car, race);
109
+ const lowSpeedOffTrack = !car.trackState.onTrack && car.speed < kphToSimSpeed(24);
110
+ const distanceFromRoadMeters = simUnitsToMeters(Math.max(0, car.trackState.crossTrackError - race.track.width / 2));
111
+ const inRejoinHold = (car.rejoinRecoveryFrames ?? 0) > 0;
112
+ const recoveringOutsideRoad = distanceFromRoadMeters > 0.25 &&
113
+ (!car.trackState.onTrack || (profile.stabilizeRejoinHoldOutsideRoad && inRejoinHold));
114
+ const offTrackTargetScale = lowSpeedOffTrack
115
+ ? profile.lowSpeedForwardTargetScale
116
+ : profile.offTrackForwardTargetScale;
117
+ const targetBase = !car.trackState.onTrack && distanceFromRoadMeters > 2.5
118
+ ? pointAt(race.track, car.progress + lookahead * offTrackTargetScale)
119
+ : pointAt(race.track, car.progress + lookahead * 0.78);
120
+ const signedSide = Math.sign(car.trackState?.signedOffset ?? 0);
121
+ const rejoinOffset = signedSide !== 0 && (
122
+ distanceFromRoadMeters > 0.5 ||
123
+ car.trackState.crossTrackError > race.track.width / 2 - VEHICLE_LIMITS.carWidth * 1.6
124
+ )
125
+ ? -signedSide * race.track.width * profile.inwardOffsetRatio
126
+ : 0;
127
+ const target = offsetTrackPoint(targetBase, rejoinOffset);
128
+ const angleError = angleToPoint(car, target);
129
+ const surfaceTargetSpeedKph = car.trackState.surface === 'track'
130
+ ? profile.surfaceTargetSpeeds.track
131
+ : car.trackState.surface === 'kerb'
132
+ ? profile.surfaceTargetSpeeds.kerb
133
+ : car.trackState.surface === 'gravel'
134
+ ? profile.surfaceTargetSpeeds.gravel
135
+ : car.trackState.surface === 'grass'
136
+ ? profile.surfaceTargetSpeeds.grass
137
+ : profile.surfaceTargetSpeeds.barrier;
138
+ const alignment = clamp(1 - Math.abs(angleError) / Math.PI, 0.22, 1);
139
+ const desiredSpeed = kphToSimSpeed(clamp(
140
+ surfaceTargetSpeedKph * (0.62 + alignment * 0.38) - distanceFromRoadMeters * 0.065,
141
+ car.trackState.surface === 'barrier' ? 12 : 24,
142
+ surfaceTargetSpeedKph,
143
+ ));
144
+ const speedError = desiredSpeed - car.speed;
145
+ const lowSpeedRecovery = car.speed < kphToSimSpeed(18)
146
+ ? car.trackState.surface === 'barrier' ? profile.lowSpeedRecovery.barrier : profile.lowSpeedRecovery.default
147
+ : 0;
148
+ const outwardBrakeStart = lowSpeedOffTrack
149
+ ? profile.lowSpeedOutwardBrakeStartMps
150
+ : profile.outwardBrakeStartMps;
151
+ const canCrawlSteerRecovery = lowSpeedOffTrack &&
152
+ car.speed < kphToSimSpeed(8) &&
153
+ edgeMotion.outwardSpeedMps < profile.lowSpeedCrawlOutwardToleranceMps;
154
+ const needsOutwardStabilization = recoveringOutsideRoad &&
155
+ (!lowSpeedOffTrack || profile.stabilizeLowSpeedOutward) &&
156
+ !canCrawlSteerRecovery &&
157
+ edgeMotion.outwardSpeedMps > outwardBrakeStart &&
158
+ distanceFromRoadMeters > 0.25;
159
+ const recoveryThrottleFloorBase = profile.simulatorMode && !car.trackState.onTrack
160
+ ? (car.trackState.surface === 'barrier' ? profile.lowSpeedRecovery.barrier : profile.lowSpeedRecovery.default)
161
+ : lowSpeedRecovery;
162
+ const lowSpeedRecoveryThrottle = lowSpeedOffTrack && car.trackState.surface !== 'barrier'
163
+ ? profile.lowSpeedRecoveryThrottle
164
+ : 0;
165
+ const recoveryThrottleFloor = needsOutwardStabilization
166
+ ? 0
167
+ : Math.max(recoveryThrottleFloorBase, lowSpeedRecoveryThrottle);
168
+ const surfaceThrottleLimit = car.trackState.surface === 'barrier'
169
+ ? profile.throttleLimits.barrier
170
+ : car.trackState.surface === 'gravel'
171
+ ? profile.throttleLimits.gravel
172
+ : car.trackState.surface === 'grass'
173
+ ? profile.throttleLimits.grass
174
+ : profile.throttleLimits.default;
175
+ const throttleLimit = lowSpeedOffTrack && edgeMotion.headingOutward && distanceFromRoadMeters > 0.5
176
+ ? Math.min(surfaceThrottleLimit, profile.lowSpeedHeadingOutwardThrottleLimit)
177
+ : surfaceThrottleLimit;
178
+ let brakeAmount = speedError < -kphToSimSpeed(5)
179
+ ? clamp(Math.abs(speedError) / kphToSimSpeed(profile.brakeResponseKph), 0.04, car.trackState.onTrack ? profile.onTrackBrakeLimit : profile.offTrackBrakeLimit)
180
+ : 0;
181
+ if (needsOutwardStabilization) {
182
+ brakeAmount = Math.max(
183
+ brakeAmount,
184
+ clamp(
185
+ (edgeMotion.outwardSpeedMps - outwardBrakeStart) * profile.outwardBrakeGain +
186
+ Math.max(0, Math.abs(angleError) - 1.05) * profile.misalignmentBrakeGain,
187
+ 0,
188
+ car.trackState.onTrack ? profile.onTrackBrakeLimit : profile.offTrackBrakeLimit,
189
+ ),
190
+ );
191
+ }
192
+ const unsettledScale = lowSpeedOffTrack
193
+ ? Math.max(profile.unsettledScale(car), 0.82)
194
+ : profile.unsettledScale(car);
195
+ const slideThrottleScale = 1 - clamp(
196
+ Math.max(0, edgeMotion.outwardSpeedMps - profile.outwardBrakeStartMps) * profile.slideThrottleDamping +
197
+ Math.max(0, Math.abs(angleError) - 1.25) * 0.22,
198
+ 0,
199
+ 0.94,
200
+ );
201
+ const steeringRequest = lowSpeedOffTrack
202
+ ? clamp(angleError * profile.steerGain, -profile.lowSpeedOffTrackSteerLimit, profile.lowSpeedOffTrackSteerLimit)
203
+ : angleError * profile.steerGain;
204
+ const minimumAcceleration = needsOutwardStabilization ? 0 : 0.04;
205
+
206
+ return createDriverInput()
207
+ .steer(steeringRequest)
208
+ .accelerate(brakeAmount > 0.05 ? 0 : speedError > 0
209
+ ? clamp(((speedError / kphToSimSpeed(24)) * alignment + recoveryThrottleFloor) * unsettledScale * slideThrottleScale, minimumAcceleration, throttleLimit)
210
+ : recoveryThrottleFloor * unsettledScale * slideThrottleScale)
211
+ .brake(brakeAmount)
212
+ .controls();
213
+ }
@@ -0,0 +1,35 @@
1
+ import { clamp, normalizeAngle } from '../simMath.js';
2
+ import { kphToSimSpeed, metersToSimUnits, simUnitsToMeters } from '../units.js';
3
+ import { offsetTrackPoint, pointAt } from '../track/trackModel.js';
4
+ import { SAFETY_CAR_LOOKAHEAD_MAX, SAFETY_CAR_LOOKAHEAD_MIN } from './driverControlConstants.js';
5
+ import { angleToPoint } from './driverMath.js';
6
+ import { createDriverInput } from './driverInput.js';
7
+
8
+ export function decideSafetyCarControls(car, orderIndex, race) {
9
+ const queueSlot = race.safetyCar.progress - race.rules.safetyCarLeadDistance - orderIndex * race.rules.safetyCarGap;
10
+ const lookahead = clamp(car.speed * 0.55 + metersToSimUnits(20), SAFETY_CAR_LOOKAHEAD_MIN, SAFETY_CAR_LOOKAHEAD_MAX);
11
+ const targetBase = pointAt(race.track, car.progress + lookahead);
12
+ const target = offsetTrackPoint(targetBase, 0);
13
+ const slotError = queueSlot - car.raceDistance;
14
+ const headingError = normalizeAngle(targetBase.heading - car.heading);
15
+ const lateralError = car.trackState?.signedOffset ?? 0;
16
+ const centerlineCorrection = -Math.atan2(
17
+ lateralError * 1.25,
18
+ Math.max(car.speed, kphToSimSpeed(35)),
19
+ );
20
+ const cornerRadiusMeters = simUnitsToMeters(1 / Math.max(Math.abs(targetBase.curvature), 1e-7));
21
+ const cornerSpeed = kphToSimSpeed(clamp(Math.sqrt(4.2 * cornerRadiusMeters) * 3.6, 45, 158));
22
+ const edgePenalty = clamp(simUnitsToMeters(Math.abs(lateralError)) - simUnitsToMeters(race.track.width / 2) * 0.68, 0, 3) * kphToSimSpeed(18);
23
+ const desiredSpeed = clamp(
24
+ race.rules.safetyCarSpeed + slotError * 0.24 - edgePenalty,
25
+ kphToSimSpeed(22),
26
+ Math.min(race.rules.safetyCarSpeed + kphToSimSpeed(32), cornerSpeed),
27
+ );
28
+ const speedError = desiredSpeed - car.speed;
29
+
30
+ return createDriverInput()
31
+ .steer(angleToPoint(car, target) * 1.18 + headingError * 0.32 + centerlineCorrection)
32
+ .accelerate(speedError > kphToSimSpeed(1) ? clamp(speedError / kphToSimSpeed(16), 0, 0.5) : 0)
33
+ .brake(speedError < -kphToSimSpeed(0.5) ? clamp(Math.abs(speedError) / kphToSimSpeed(14), 0, 1) : 0)
34
+ .controls();
35
+ }
@@ -0,0 +1,51 @@
1
+ import { metersToSimUnits } from '../units.js';
2
+ import { DEFEND_REAR_GAP, TRAFFIC_GAP_AHEAD, TRAFFIC_SIDE_GAP, TRAFFIC_REAR_WINDOW } from './driverControlConstants.js';
3
+ import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
4
+
5
+ export function scanNearbyTraffic(car, race) {
6
+ return race.cars
7
+ .filter((other) => other !== car)
8
+ .map((other) => ({
9
+ car: other,
10
+ gap: other.raceDistance - car.raceDistance,
11
+ signedOffset: other.trackState?.signedOffset ?? 0,
12
+ speed: other.speed,
13
+ }))
14
+ .filter((entry) => entry.gap > -metersToSimUnits(39) && entry.gap < metersToSimUnits(134));
15
+ }
16
+
17
+ export function findDefensiveThreat(traffic) {
18
+ let closest = null;
19
+
20
+ traffic.forEach((entry) => {
21
+ if (entry.gap >= -VEHICLE_LIMITS.carLength * 1.1 || entry.gap < -DEFEND_REAR_GAP) return;
22
+ if (!closest || entry.gap > closest.gap) closest = entry;
23
+ });
24
+
25
+ return closest;
26
+ }
27
+
28
+ export function findLaneTrafficAhead(traffic, offset, maxDistance) {
29
+ let closest = null;
30
+
31
+ traffic.forEach((entry) => {
32
+ if (entry.gap <= 0 || entry.gap > maxDistance) return;
33
+ if (Math.abs(entry.signedOffset - offset) > TRAFFIC_SIDE_GAP) return;
34
+ if (!closest || entry.gap < closest.gap) closest = entry;
35
+ });
36
+
37
+ return closest;
38
+ }
39
+
40
+ export function findLaneTrafficBeside(traffic, offset) {
41
+ let closest = null;
42
+
43
+ traffic.forEach((entry) => {
44
+ const lateral = Math.abs(entry.signedOffset - offset);
45
+ if (Math.abs(entry.gap) > metersToSimUnits(28) || lateral > TRAFFIC_SIDE_GAP) return;
46
+ const risk = (metersToSimUnits(28) - Math.abs(entry.gap)) + (TRAFFIC_SIDE_GAP - lateral);
47
+ if (!closest || risk > closest.risk) closest = { ...entry, lateral, risk };
48
+ });
49
+
50
+ return closest;
51
+ }