@inventure71/paddockjs 1.0.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 (190) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/INSTALL_AND_UPDATE.md +4 -3
  3. package/README.md +194 -12
  4. package/docs/architecture.md +119 -44
  5. package/docs/concepts.md +4 -2
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +299 -17
  8. package/docs/index.md +1 -0
  9. package/docs/learnings.md +22 -0
  10. package/docs/rules.md +37 -10
  11. package/docs/system_specs.md +23 -10
  12. package/docs/training.md +272 -30
  13. package/examples/python/README.md +62 -0
  14. package/examples/python/base_policy_server.py +191 -0
  15. package/examples/python/requirements.txt +2 -0
  16. package/package.json +12 -4
  17. package/src/api/PaddockSimulatorController.js +4 -0
  18. package/src/app/BrowserExpertAdapter.js +367 -9
  19. package/src/app/F1SimulatorApp.js +305 -1603
  20. package/src/app/banners/bannerFormatters.js +55 -0
  21. package/src/app/banners/raceDataBannerController.js +141 -0
  22. package/src/app/banners/stewardMessageController.js +99 -0
  23. package/src/app/camera/cameraConstants.js +21 -0
  24. package/src/app/camera/cameraController.js +265 -0
  25. package/src/app/camera/cameraGeometry.js +176 -0
  26. package/src/app/domBindings.js +4 -0
  27. package/src/app/readouts/carOverviewRenderer.js +109 -0
  28. package/src/app/readouts/controlStateRenderer.js +37 -0
  29. package/src/app/readouts/raceStatusRenderer.js +133 -0
  30. package/src/app/readouts/readoutFormatters.js +58 -0
  31. package/src/app/readouts/telemetryRenderer.js +113 -0
  32. package/src/app/readouts/timingTowerRenderer.js +177 -0
  33. package/src/app/rendering/appAssets.js +38 -0
  34. package/src/app/rendering/carRenderer.js +286 -0
  35. package/src/app/rendering/displayUtils.js +22 -0
  36. package/src/app/rendering/drsTrailRenderer.js +90 -0
  37. package/src/app/rendering/expertSensorRenderer.js +126 -0
  38. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  39. package/src/app/rendering/replayGhostRenderer.js +124 -0
  40. package/src/app/rendering/trackRenderer.js +25 -0
  41. package/src/app/runtime/frameLoop.js +66 -0
  42. package/src/app/runtime/runtimeTiming.js +34 -0
  43. package/src/app/runtime/runtimeVisibility.js +45 -0
  44. package/src/config/defaultOptions.js +20 -0
  45. package/src/environment/actions.js +13 -5
  46. package/src/environment/controllerLoop.js +246 -0
  47. package/src/environment/episode.js +86 -2
  48. package/src/environment/evaluation.js +54 -32
  49. package/src/environment/index.d.ts +361 -9
  50. package/src/environment/index.js +1 -0
  51. package/src/environment/metrics.js +159 -0
  52. package/src/environment/observationOptions.js +22 -1
  53. package/src/environment/observations.js +325 -40
  54. package/src/environment/options.js +37 -5
  55. package/src/environment/runtime.js +222 -21
  56. package/src/environment/scenarios.js +37 -8
  57. package/src/environment/sensors/bodySenses.js +23 -0
  58. package/src/environment/sensors/boundarySenses.js +17 -0
  59. package/src/environment/sensors/carRays.js +75 -0
  60. package/src/environment/sensors/contactSenses.js +32 -0
  61. package/src/environment/sensors/index.js +157 -0
  62. package/src/environment/sensors/indexedRayBands.js +103 -0
  63. package/src/environment/sensors/nearbyCars.js +52 -0
  64. package/src/environment/sensors/opponentRadar.js +65 -0
  65. package/src/environment/sensors/rayConfig.js +110 -0
  66. package/src/environment/sensors/rayDefaults.js +6 -0
  67. package/src/environment/sensors/rayGeometry.js +60 -0
  68. package/src/environment/sensors/rayGuards.js +19 -0
  69. package/src/environment/sensors/sensorTargets.js +60 -0
  70. package/src/environment/sensors/surfaceRays.js +296 -0
  71. package/src/environment/sensors/trackRays.js +261 -0
  72. package/src/environment/sensors.js +1 -367
  73. package/src/environment/specs.js +100 -5
  74. package/src/environment/workerProtocol.js +13 -1
  75. package/src/index.d.ts +169 -6
  76. package/src/index.js +5 -0
  77. package/src/rendering/proceduralTrackAsset.js +20 -823
  78. package/src/rendering/renderSnapshot.js +38 -12
  79. package/src/rendering/track/finishLineRenderer.js +34 -0
  80. package/src/rendering/track/gridRenderer.js +47 -0
  81. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  82. package/src/rendering/track/pitLaneRenderer.js +248 -0
  83. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  84. package/src/rendering/track/trackRenderConstants.js +38 -0
  85. package/src/rendering/track/trackRenderGeometry.js +94 -0
  86. package/src/simulation/collisionGeometry.js +1 -1
  87. package/src/simulation/driver/driverControlConstants.js +28 -0
  88. package/src/simulation/driver/driverController.js +47 -0
  89. package/src/simulation/driver/driverInput.js +28 -0
  90. package/src/simulation/driver/driverMath.js +6 -0
  91. package/src/simulation/driver/driverPersonality.js +25 -0
  92. package/src/simulation/driver/edgeRecovery.js +48 -0
  93. package/src/simulation/driver/racingControls.js +399 -0
  94. package/src/simulation/driver/racingLinePlan.js +166 -0
  95. package/src/simulation/driver/recoveryDynamics.js +38 -0
  96. package/src/simulation/driver/rejoinControls.js +213 -0
  97. package/src/simulation/driver/safetyCarControls.js +35 -0
  98. package/src/simulation/driver/trafficScan.js +51 -0
  99. package/src/simulation/driverController.js +4 -619
  100. package/src/simulation/participants/participantInteractions.js +120 -0
  101. package/src/simulation/pit/pitFlow.js +234 -0
  102. package/src/simulation/pit/pitIntent.js +92 -0
  103. package/src/simulation/pit/pitOccupancy.js +47 -0
  104. package/src/simulation/pit/pitPenaltyService.js +71 -0
  105. package/src/simulation/pit/pitQueue.js +55 -0
  106. package/src/simulation/pit/pitRouteMovement.js +160 -0
  107. package/src/simulation/pit/pitRouting.js +319 -0
  108. package/src/simulation/pit/pitService.js +41 -0
  109. package/src/simulation/pit/pitServiceConstants.js +21 -0
  110. package/src/simulation/pit/pitServiceProfile.js +45 -0
  111. package/src/simulation/pit/pitSnapshots.js +61 -0
  112. package/src/simulation/pit/pitState.js +113 -0
  113. package/src/simulation/pit/pitTireService.js +92 -0
  114. package/src/simulation/race/classification.js +104 -0
  115. package/src/simulation/race/gridPenalties.js +37 -0
  116. package/src/simulation/race/raceControlState.js +38 -0
  117. package/src/simulation/race/raceDistance.js +32 -0
  118. package/src/simulation/race/raceFinish.js +65 -0
  119. package/src/simulation/race/raceLifecycle.js +71 -0
  120. package/src/simulation/race/raceOrder.js +113 -0
  121. package/src/simulation/race/raceProgress.js +124 -0
  122. package/src/simulation/race/raceSetup.js +93 -0
  123. package/src/simulation/race/raceStep.js +80 -0
  124. package/src/simulation/race/redFlag.js +15 -0
  125. package/src/simulation/race/retirements.js +44 -0
  126. package/src/simulation/race/safetyCar.js +86 -0
  127. package/src/simulation/race/stalledDnf.js +86 -0
  128. package/src/simulation/raceSimulation.js +230 -2817
  129. package/src/simulation/replay/replayGhosts.js +128 -0
  130. package/src/simulation/rules/moduleConfig.js +72 -0
  131. package/src/simulation/rules/penaltyConfig.js +99 -0
  132. package/src/simulation/rules/penaltyEvents.js +18 -0
  133. package/src/simulation/rules/penaltyStats.js +26 -0
  134. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  135. package/src/simulation/rules/ruleDefaults.js +85 -0
  136. package/src/simulation/rules/rulesReview.js +141 -0
  137. package/src/simulation/rules/rulesetPresets.js +47 -0
  138. package/src/simulation/rules/trackLimitsSteward.js +1 -1
  139. package/src/simulation/rulesConfig.js +5 -323
  140. package/src/simulation/snapshots/raceSnapshots.js +192 -0
  141. package/src/simulation/timing/drsTiming.js +87 -0
  142. package/src/simulation/timing/gapEstimation.js +38 -0
  143. package/src/simulation/timing/lapTelemetry.js +182 -0
  144. package/src/simulation/timing/raceTiming.js +15 -0
  145. package/src/simulation/timing/sectorPerformance.js +60 -0
  146. package/src/simulation/timing/timingConstants.js +5 -0
  147. package/src/simulation/timing/timingHistory.js +46 -0
  148. package/src/simulation/timing/timingLines.js +79 -0
  149. package/src/simulation/track/centerlineSpline.js +59 -0
  150. package/src/simulation/track/drsZones.js +67 -0
  151. package/src/simulation/track/pitLaneAccess.js +103 -0
  152. package/src/simulation/track/pitLaneLayout.js +395 -0
  153. package/src/simulation/track/pitLaneState.js +160 -0
  154. package/src/simulation/track/proceduralCenterline.js +237 -0
  155. package/src/simulation/track/sampleGeometry.js +49 -0
  156. package/src/simulation/track/spatialQueries.js +154 -0
  157. package/src/simulation/track/startStraight.js +141 -0
  158. package/src/simulation/track/trackConstants.js +109 -0
  159. package/src/simulation/track/trackGenerationOptions.js +283 -0
  160. package/src/simulation/track/trackMath.js +241 -0
  161. package/src/simulation/track/trackModel.js +240 -0
  162. package/src/simulation/track/trackQueryIndex.js +945 -0
  163. package/src/simulation/track/trackSectors.js +19 -0
  164. package/src/simulation/track/trackStatePolicy.js +24 -0
  165. package/src/simulation/track/trackValidation.js +98 -0
  166. package/src/simulation/trackModel.js +1 -1706
  167. package/src/simulation/vehicle/contactResolution.js +116 -0
  168. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  169. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  170. package/src/simulation/vehicle/runoffResponse.js +88 -0
  171. package/src/simulation/vehicle/surfacePriority.js +28 -0
  172. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  173. package/src/simulation/vehicle/vehiclePhysics.js +363 -0
  174. package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
  175. package/src/simulation/vehicle/vehicleState.js +186 -0
  176. package/src/simulation/vehicle/wheelSurface.js +150 -0
  177. package/src/simulation/vehicleGeometry.js +1 -247
  178. package/src/simulation/vehiclePhysics.js +1 -136
  179. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  180. package/src/simulation/wheelSurface.js +1 -338
  181. package/src/styles.css +42 -0
  182. package/src/ui/cameraControlsTemplate.js +20 -0
  183. package/src/ui/carOverviewTemplate.js +33 -0
  184. package/src/ui/componentTemplates.js +14 -360
  185. package/src/ui/raceCanvasTemplate.js +69 -0
  186. package/src/ui/raceControlsTemplate.js +36 -0
  187. package/src/ui/shellTemplate.js +5 -4
  188. package/src/ui/telemetryDrawerTemplate.js +55 -0
  189. package/src/ui/telemetryTemplates.js +136 -0
  190. package/src/ui/timingTowerTemplate.js +34 -0
package/docs/rules.md CHANGED
@@ -4,7 +4,7 @@ This file documents the rules currently implemented by the simulator. If the rac
4
4
 
5
5
  ## Default Race Rules
6
6
 
7
- Default rules are defined in `src/simulation/raceSimulation.js` as `DEFAULT_RULES`:
7
+ Default rules are defined in `src/simulation/rules/ruleDefaults.js` and re-exported through `src/simulation/rulesConfig.js` as `DEFAULT_RULES`:
8
8
 
9
9
  - DRS detection gap: `1` second.
10
10
  - Safety car speed: `166 km/h`.
@@ -48,6 +48,14 @@ rules: {
48
48
  compounds: ['S', 'M', 'H'],
49
49
  mandatoryDistinctDryCompounds: 2,
50
50
  },
51
+ tireDegradation: {
52
+ enabled: true,
53
+ },
54
+ stalledDnf: {
55
+ enabled: true,
56
+ maxStoppedSeconds: 12,
57
+ speedThresholdKph: 5,
58
+ },
51
59
  penalties: {
52
60
  enabled: true,
53
61
  stewardStrictness: 0.85,
@@ -58,12 +66,18 @@ rules: {
58
66
  },
59
67
  weather: { enabled: false },
60
68
  reliability: { enabled: false },
61
- fuelLoad: { enabled: true },
69
+ fuelLoad: { enabled: false },
62
70
  },
63
71
  }
64
72
  ```
65
73
 
66
- The current implementation normalizes and exposes all module config, records a penalty ledger, enforces collision penalties, track-limit penalties, tire-requirement penalties, and pit-lane speeding penalties, creates/renders pit-lane geometry for every track, treats pit-lane asphalt, working-lane service areas, and garage boxes as legal drivable surfaces, and runs a first automated pit-stop pass when `pitStops.enabled` is true. Weather effects, reliability failures, and fuel-load performance effects are staged behind the module contract and are not fully simulated yet.
74
+ The current implementation normalizes and exposes all module config, records a penalty ledger, enforces collision penalties, track-limit penalties, tire-requirement penalties, pit-lane speeding penalties, and opt-in stalled off-track DNFs, creates/renders pit-lane geometry for every track, treats pit-lane asphalt, working-lane service areas, and garage boxes as legal drivable surfaces, applies nonlinear tire-energy degradation when `tireDegradation.enabled` is not `false`, and runs automated pit stops when `pitStops.enabled` is true. Weather effects, reliability failures, and fuel-load performance effects are reserved future modules only; the simulation does not change grip, power, retirement risk, mass, or pace from those module keys.
75
+
76
+ `tireDegradation.enabled: false` freezes each car's current `tireEnergy` instead of updating wear from throttle, braking, and lateral load. This is separate from `tireStrategy.enabled`: tire strategy controls compounds, tire-requirement stewarding, and pit-stop tire choices, while tire degradation controls the physics wear curve.
77
+
78
+ `stalledDnf` defaults to `enabled: false`. When a host sets `rules.modules.stalledDnf.enabled: true`, a car that is off legal racing or pit surfaces and remains below `speedThresholdKph` for `maxStoppedSeconds` is marked out of race with `dnfReason: 'stalled-off-track'`, emits a `car-dnf` event, freezes controls/motion, and leaves active collision, sensor, pit-blocking, and race-order participation. The timer does not run during pre-start, red flag, after race completion, for already-finished cars, or while pit entry/queue/service/exit owns the car. Returning to legal surface or moving above the threshold resets the timer.
79
+
80
+ Participant interaction profiles can opt real cars out of specific interaction systems without turning them into replay ghosts. A non-colliding profile skips vehicle collision resolution and collision stewarding for that car pair, but the car still uses normal steering/throttle/brake physics. A non-blocking pit profile is ignored by pit service occupancy and queue-blocking checks. A car with `affectsRaceOrder: false` remains in `snapshot.cars` but is excluded from ranking, DRS references, finish order, and final classification. Replay ghosts are separate trajectory overlays in `snapshot.replayGhosts`; they never participate in rules, collisions, timing, pit stops, or penalties.
67
81
 
68
82
  ## Steward Strictness
69
83
 
@@ -86,7 +100,7 @@ Track-limit enforcement uses the white line as the legal edge. The steward check
86
100
 
87
101
  The built-in driver AI is expected to respect that same white-line rule through normal control inputs. Its racing-line planner keeps a centerline comfort margin for the car footprint, and the controller progressively lifts, applies mild braking when needed, and steers back inward when the car approaches the legal edge. This does not let the car defy physics; it only changes when the default AI chooses to brake, coast, and steer. Kerbs still allow normal recovery speed rather than gravel-style stopping behavior.
88
102
 
89
- Pit-lane surfaces are legal road for track-limit purposes. `pit-entry`, `pit-lane`, `pit-exit`, and `pit-box` track states set `inPitLane: true`, so ray sensors, runoff response, and the track-limit steward do not treat normal pit entry, service, or exit as off-track excursions.
103
+ Pit-lane surfaces are legal road for track-limit purposes. `pit-entry`, `pit-lane`, `pit-exit`, and `pit-box` track states set `inPitLane: true`, so ray sensors, runoff response, and the track-limit steward do not treat normal pit entry, service, or exit as off-track excursions. Environment-controlled cars only receive this pit-lane override when pit routing is active or pit intent is committed; otherwise they stay on main-track classification even near pit connectors. Hard barrier destruction remains based on main-track geometry and does not depend on pit classification.
90
104
 
91
105
  Penalty decisions are recorded in `snapshot.penalties` and emitted as `penalty` events in the same step. Each entry includes the penalty type, driver id, strictness, status, penalty seconds, pending service conversion seconds, lap, timestamp, and rule-specific context. Multiple time penalties for the same driver are additive: two separate +5s entries produce `penaltySeconds: 10` on that car's snapshot and classification adjustment.
92
106
 
@@ -148,15 +162,23 @@ Seconds gaps are the difference between when two cars crossed the same timing li
148
162
 
149
163
  The race engine uses simulator units internally. `src/simulation/units.js` converts simulator distance and speed to public meter and km/h values. Public timer, lap-time, sector-time, gap-time, penalty-time, and service-countdown values are seconds. Public distance values with a `Meters` suffix are meters. The current speed calibration maps the simulation maximum speed to an F1-like `330 km/h`; rendered car sprite dimensions are a visual scale and are not used as the physical distance scale.
150
164
 
165
+ ## Physics Modes
166
+
167
+ The default `physicsMode` is `'arcade'`, which preserves the existing forgiving vehicle behavior for current browser hosts and demos.
168
+
169
+ `physicsMode: 'simulator'` is opt-in for more believable F1-style limits. It still accepts only normal steering, throttle, brake, and pit-intent inputs, but the vehicle integrator keeps a real 2D world velocity, derives speed from that velocity, and applies a traction budget between longitudinal and lateral demand. Steering changes yaw through a rate-limited response, while body slip is derived from the difference between the car heading and actual velocity direction instead of being an artificial movement offset. Kerbs remain legal racing surface, but high-load kerb use is less stable and slower than track asphalt. Gravel and grass are averaged per wheel so one wheel over the edge creates partial grip/drag loss, while all-wheel runoff still costs major time. The rendered barrier wall is the hard outer runoff boundary in both physics modes; cars whose footprint reaches the wall's inner face are marked `destroyed`, stopped, removed from active collision and sensor participation, and placed in DNF timing order. `physicsMode` changes vehicle integration and grip behavior, not barrier consequences. Simulator mode does not teleport cars, magnetize them to the track, or bypass physics with hidden pose correction.
170
+
171
+ Simulator snapshots and environment observations expose additional vehicle telemetry: `lateralG`, `longitudinalG`, `gripUsage`, `slipAngleRadians`, `tractionLimited`, and `stabilityState`. These fields are optional public telemetry; existing `speedKph`, wheel-surface, steering, throttle, and brake fields remain unchanged.
172
+
151
173
  ## Laps
152
174
 
153
175
  Lap is computed from each car's cumulative race distance over the track length. Total laps are provided by mount options and default to `10`.
154
176
 
155
177
  Each track is automatically divided into three equal sectors. Sector and lap telemetry is recorded when a car's cumulative race distance crosses a sector boundary or start/finish boundary. Completed current-lap sector split times before the active sector are exposed as `currentSectors`, while `liveSectors` keeps only the active sector's elapsed time updated before the boundary is crossed and clears any future-sector values. `sectorProgress` is recomputed every snapshot as the live S1/S2/S3 fill state for the current lap, so sector-map UI does not derive progress from completed split state or keep stale future sectors lit. Timing values are stored in seconds and exposed on each car snapshot as `lapTelemetry`.
156
178
 
157
- Each car is marked `finished` when it reaches `track.length * totalLaps`. It receives a frozen provisional `finishRank`, exposes `raceStatus: 'waved-flag'` / `wavedFlag: true`, and emits a `car-finish` event. The first finisher becomes provisional `winner`, but the race keeps running until every car has crossed the finish distance. While the field is still finishing, timing order keeps already-finished cars in provisional finish order ahead of cars still running, so a waved-through car cannot lose timing-table position just because another car circulates farther before final classification.
179
+ Each car is marked `finished` when it reaches `track.length * totalLaps`. It receives a frozen provisional `finishRank`, exposes `raceStatus: 'waved-flag'` / `wavedFlag: true`, and emits a `car-finish` event. The first finisher becomes provisional `winner`, but the race keeps running until every race participant has either crossed the finish distance or is currently DNF. While the field is still finishing, timing order keeps already-finished cars in provisional finish order ahead of cars still running, and DNF cars sit below active cars in the order they retired. A car that has already crossed the finish line keeps that result even if it later touches the barrier or satisfies stalled-off-track detection while the rest of the field is still finishing; post-finish retirement checks do not turn it into DNF. A DNF car restored before final classification re-enters the live order and must finish before the race can complete; restoring it after final classification does not reopen the race.
158
180
 
159
- When all cars have finished, the simulator records `finishedAt` and final `classification`, emits a `race-finish` event, freezes order to the classified result after outstanding penalties and classification consequences, deploys the safety car, and switches race mode to `safety-car`. Finished cars keep circulating under safety-car behavior instead of hard-stopping. The current implementation does not yet implement championship scoring.
181
+ When all cars have finished or reached DNF, the simulator records `finishedAt` and final `classification`, emits a `race-finish` event, freezes order to the classified result after outstanding penalties and classification consequences, deploys the safety car, and switches race mode to `safety-car`. DNF classification entries appear after finishers with `finished: false`, no finish time, and DNF reason/time/order metadata. Finished cars keep circulating under safety-car behavior instead of hard-stopping. The current implementation does not yet implement championship scoring.
160
182
 
161
183
  ## DRS
162
184
 
@@ -166,6 +188,7 @@ DRS behavior:
166
188
  - Each track has DRS zones.
167
189
  - A car latches into a DRS zone when it crosses that zone start.
168
190
  - A car becomes DRS eligible if it was close enough to the physically-ahead car at the relevant detection crossing, including lapped traffic.
191
+ - Finished, DNF/out-of-race, non-race-order, and active pit-stop cars are ignored when selecting the physically-ahead DRS reference.
169
192
  - The current detection window is controlled by `drsDetectionSeconds`.
170
193
  - When eligible inside the latched zone, `drsActive` becomes true.
171
194
 
@@ -199,7 +222,7 @@ When safety car is cleared:
199
222
 
200
223
  ## Driver AI
201
224
 
202
- Driver control decisions live in `src/simulation/driverController.js`.
225
+ Driver control decisions are exported from `src/simulation/driverController.js`; canonical driver policy modules live under `src/simulation/driver/`.
203
226
 
204
227
  The AI has separate behavior for:
205
228
 
@@ -221,11 +244,13 @@ Green-flag behavior uses:
221
244
  - Nearby traffic.
222
245
  - Preferred and available lane offsets.
223
246
 
224
- The built-in racing controller still drives through normal vehicle inputs only. It chooses a forward racing target, blends steering from the target angle, upcoming track heading, lateral offset, edge recovery pressure, and a corner-apex racing-line offset, then sends clamped steering/throttle/brake values into the vehicle physics integrator. It is expected to use the available track width and kerbs on generated circuits, including sharp generated turns. When heading alignment or steering load shows the car cannot yet carry throttle through a corner, throttle is cut before braking is requested so the controller does not fight itself. It does not snap car position, override heading, or bypass steering-rate/yaw-rate limits.
247
+ The built-in racing controller still drives through normal vehicle inputs only. It chooses a forward racing target, blends steering from the target angle, upcoming track heading, lateral offset, edge recovery pressure, outward edge motion, and a corner-apex racing-line offset, then sends clamped steering/throttle/brake values into the vehicle physics integrator. It is expected to use the available track width and kerbs on generated circuits, including sharp generated turns. When heading alignment, steering load, or outward edge motion shows the car cannot yet carry throttle through a corner, throttle is cut before braking is requested so the controller does not fight itself. It does not snap car position, override heading, or bypass steering-rate/yaw-rate limits.
248
+
249
+ In simulator mode, the built-in controller uses separate tuning: earlier lookahead for braking, smaller high-speed steering requests, stronger edge recovery before the legal limit, throttle reduction when grip and slip telemetry show the car is overloaded, a higher legal-road speed floor so the baseline AI does not crawl through generated bends, and lower rejoin targets from kerb, grass, or gravel states. Extra stability braking is applied only when upcoming corner load, edge pressure, outward drift, high straight speed, or near-target speed makes it useful; ordinary below-target straight-line correction should not trigger brake pulses. This is intentionally a baseline driver, not a trained policy.
225
250
 
226
251
  Traffic decisions use real meter-scaled gaps and lateral spacing, not raw simulator-size constants. A trailing car inside the attack window commits toward a visible passing lane instead of sitting on the centerline. A leading car with a close rear threat can defend the threatened side before the rival is fully alongside. Side-by-side overlap still creates spacing pressure so defensive and attacking moves are expressed as target-line choices and normal steering/brake/throttle inputs, not as collision avoidance teleports or artificial position overrides.
227
252
 
228
- Rejoin behavior also remains physics-driven. When a car leaves the legal racing surface, the controller enters a short recovery hold and keeps using rejoin controls after the center point first reaches track or kerb until heading and edge margin are stable. That prevents the car from immediately returning to full racing throttle while still pointed across the track. Recovery targets look forward along the racing surface, bias inward from the edge, and request bounded surface-specific speeds for track, kerb, grass, gravel, and barrier states.
253
+ Rejoin behavior also remains physics-driven. When a car leaves the legal racing surface, the controller enters a short recovery hold and keeps using rejoin controls after the center point first reaches track or kerb until heading and edge margin are stable. That prevents the car from immediately returning to full racing throttle while still pointed across the track. Recovery targets look forward along the racing surface, bias inward from the edge, and request bounded surface-specific speeds for track, kerb, grass, and gravel states. In simulator mode, rejoin controls also watch signed outward velocity relative to the track normal: a car sliding farther into runoff cuts throttle or brakes before trying to accelerate out, while very low-speed gravel/grass recovery limits steering lock and caps throttle when the heading is still outward so steering scrub does not pin the car in place. Barrier contact is not a recovery state; it destroys the car.
229
254
 
230
255
  Tire energy can degrade to 1%. The vehicle physics layer converts tire energy into a nonlinear grip factor, so degradation has a visible performance cost across the full 100% to 1% range while still leaving a damaged car controllable enough to return to the pits.
231
256
 
@@ -264,7 +289,9 @@ Track state can classify a car as on:
264
289
 
265
290
  Surface affects grip, drag, and rolling resistance.
266
291
 
267
- Surface physics is wheel-based. `src/simulation/wheelSurface.js` classifies each wheel contact patch against the track model and the car uses the worst current surface for physics: `barrier > gravel > grass > kerb > pit-box > pit-lane > pit-entry/pit-exit > track`. Normal main-track running uses an analytic signed-offset fast path from cached wheel geometry, while pit-lane and pit-connector edge cases use full patch sampling. One wheel on gravel slows the car with gravel behavior even if the other three wheels are on asphalt. In addition, `vehiclePhysics.js` compares left-side and right-side wheel resistance and applies a small capped yaw-rate bias toward the slower side, so asymmetric gravel/grass/kerb contact can tug the car toward the dirty side without becoming a full tire-force simulation. One wheel on kerb reports `kerb` and applies kerb-level grip/drag without causing a track-limit violation by itself. Pit entry, pit lane, pit exit, and pit-box surfaces remain legal drivable surfaces.
292
+ Surface physics is wheel-based. `src/simulation/vehicle/wheelSurface.js` classifies each wheel contact patch against the track model and the car uses the worst current surface for physics: `barrier > gravel > grass > kerb > pit-box > pit-lane > pit-entry/pit-exit > track`. Normal main-track running uses an analytic signed-offset fast path from cached wheel geometry, while pit-lane and pit-connector edge cases use full patch sampling. One wheel on gravel slows the car with gravel behavior even if the other three wheels are on asphalt. In addition, `vehiclePhysics.js` compares left-side and right-side wheel resistance and applies a small capped yaw-rate bias toward the slower side, so asymmetric gravel/grass/kerb contact can tug the car toward the dirty side without becoming a full tire-force simulation. One wheel on kerb reports `kerb` and applies kerb-level grip/drag without causing a track-limit violation by itself. If the car footprint reaches the rendered barrier wall's inner face, every physics mode marks the car destroyed instead of treating the wall as recoverable low-grip road. Pit entry, pit lane, pit exit, and pit-box surfaces remain legal drivable surfaces and do not count toward stalled off-track DNF timing.
293
+
294
+ Rendered material bands use those same thresholds. The visible gravel band starts at the outer kerb edge and ends at the simulated gravel edge; the visible runoff band starts at the gravel edge and ends at the barrier centerline; the barrier wall is drawn from the same `track.barrierWidth` used by destruction. Rendering, ray surface hits, wheel-surface classification, and barrier destruction must stay on this shared geometry contract.
268
295
 
269
296
  When pit stops are enabled, each team is assigned one shared service area in the working lane plus two garage boxes. Driver pairs share the service area, and the service area, queue slot, and garage boxes inherit the team color from `driver.team.color` when present, otherwise the lead driver's color. The current automatic stop plan distributes pit calls across available race laps as bounded pit trains, then lets opportunistic cars join the entry only when fewer than `pitStops.maxConcurrentPitLaneCars` are active and the nearest active pit-lane car is at least `pitStops.minimumPitLaneGapMeters` ahead; committed cars enter at the next pit-entry window and can queue in the working lane if their team service area is busy. Built-in AI cars also request stops from tire condition: below `pitStops.tirePitRequestThresholdPercent` they request `pitIntent: 1`, and below `pitStops.tirePitCommitThresholdPercent` they request `pitIntent: 2`. Expert-controlled cars do not receive those tire-threshold automatic calls; their model or host must request the stop with `pitIntent`. Hosts and expert actions can change a pending stop with `pitIntent`: `0` clears the pending call, `1` stays active until a free-enough pit-entry window appears, and `2` commits to entering at the next pit-entry window even if capacity or gap checks would block mode `1`. The same request may include a target compound, either through `setPitIntent(driverId, intent, targetCompound)` or expert action `pitCompound`; invalid compounds are rejected. Pit intent and target compound are locked while the car is entering, queued, servicing, or exiting, and intent resets to `0` after pit exit. Completed stops can be re-armed by later tire condition or host intent, so pit stops are not one-use per race. Safety-car mode does not block pit intent. Closed pit-lane race-control state keeps pending calls on track until `setPitLaneOpen(true)`, while red-flag state freezes cars and closes the effective pit lane until cleared. If the gap is too small for mode `1`, the following car stays on the main track and retries later rather than being forced to stop in the lane. Allowed cars steer through a forward main-track approach into the pit-entry road, slow to the configured limiter speed before the main pit-lane start, follow the main fast lane, pass through the team-colored queue point, hold for any pit-served penalties, hold for the resolved tire-service time, change to the requested compound or default alternate compound, add that compound to `usedTireCompounds`, then steer back to the racing surface through `pit-exit`. While a car is stationary in service, the browser renderer shows a small countdown above it: red `+Ns` for pit-served penalty hold time and yellow `Ns` for normal pit-service time. The queue point is a rolling gate when the active service area is free, so the car continues into service without a full stop. If a team-mate is already servicing or still exiting from the active service area, the arriving car targets the queue point behind that service area, holds a bounded approach speed until the final capture distance, waits without obstructing the main fast lane, then follows a short queue-release route only after the active service area is physically clear; this keeps both the queue stop and the move into service continuous instead of snapping the car between slots or crawling through the last several meters. Cars whose position is controlled by pit entry, queue, service, or exit logic are not laterally displaced by generic collision correction; if another car overlaps one, the movable car is corrected away while the pit-controlled car stays on its authored route or stop point. `pitStops.variability.enabled` optionally adjusts service time from `team.pitCrew.speed`, `team.pitCrew.consistency`, and `team.pitCrew.reliability`; `pitStops.variability.perfect: true` forces `pitStops.defaultStopSeconds` for training. The pit limiter applies only on the straight main pit lane and working lane between the lane start and lane end; the pit-entry connector road and pit-exit connector road are legal pit-lane surfaces but are not speed-limited by `pitStops.pitLaneSpeedLimitKph`, although automatic entry routing caps connector overspeed so worn-tire cars can still reach the limiter and assigned service area.
270
297
 
@@ -28,16 +28,18 @@ Headless expert environment import:
28
28
  ```js
29
29
  import {
30
30
  createPaddockEnvironment,
31
+ createPaddockDriverControllerLoop,
31
32
  createRolloutRecorder,
32
33
  runEnvironmentEvaluation,
33
34
  } from '@inventure71/paddockjs/environment';
34
35
  ```
35
36
 
36
37
  The environment subpath is the only public headless training import. It must stay free of DOM, PixiJS, CSS, and browser app dependencies.
37
- `createProgressReward()` is published from the same subpath only as non-canonical demo reward code for examples and smoke tests. It must remain optional and replaceable; environment stepping must continue to work with a custom `reward(context)` callback or no reward callback.
38
- Ray observations expose track-transition distance and car distance per ray. Track transitions use `kind: 'exit'` when the ray starts on track and reaches the border, `kind: 'entry'` when the ray starts off track and reaches the road again, and `kind: null` with max distance when no transition is visible.
38
+ `createProgressReward()` is published from the same subpath only as non-canonical demo reward code for examples and smoke tests. It must remain optional and replaceable; environment stepping must continue to work with a custom `reward(context)` callback or no reward callback. Reward callbacks are user-owned formulas over package-owned facts, including neutral per-driver `metrics` and `info.drivers[driverId]` episode state. Non-finite or missing callback results are normalized to a neutral `0` reward for that controlled driver.
39
+ Ray observations expose track-transition distance and car distance per ray. Track transitions use `kind: 'exit'` when the ray starts on track and reaches the border, `kind: 'entry'` when the ray starts off track and reaches the road again, and `kind: null` with max distance when no transition is visible. Track-position, surface, pit-lane, and ray fallback queries can use an internal non-enumerable track query index built with the track model. Browser/expert mounts, headless environments, and direct race-simulation construction now default to the indexed path (opt-out with `trackQueryIndex: false`); compact training-style environment runs (including `batch-training` rays with full state output) still auto-enable it. This changes runtime cost, not public observation shapes.
39
40
  The package must not own model training, model persistence, model registries, or trained policy behavior. The supported contract is: external code reads observations, returns normalized actions, and advances either the headless environment or browser expert mode.
40
41
  The shared expert runtime exposes `getActionSpec()` and `getObservationSpec()` so external code can inspect controlled drivers, action ranges, sensor layout, nearby-car limits, track lookahead fields, and the versioned vector schema before connecting a model. Environment reset may apply deterministic scenario placement presets or explicit placement/traffic layouts; model steps may not mutate position and still use normalized actions only. Neutral rollout recording, deterministic evaluation metrics, and the JSON worker protocol are environment utilities, not training algorithms.
42
+ `createPaddockDriverControllerLoop({ runtime, controller, actionRepeat })` is the shared public controller orchestrator for browser playback and JavaScript training-style loops. `runtime` may be browser expert mode or a headless environment, and `controller.decideBatch(context)` is called once per policy step for all controlled drivers in stable spec order. The loop lazily resets when scheduled playback starts from a fresh instance, caches `actionSpec` and `observationSpec`, reuses the active observation payload, supports async controllers, forwards actions through the normal `step(actions)` API, and repeats each decision for `actionRepeat` runtime frames. Scheduled playback stops and records `stats.lastError` when controller initialization, decision, stepping, or `onStep` hooks fail. It does not load checkpoints, run neural networks, parse model formats, compute rewards, or mutate vehicle state.
41
43
 
42
44
  All-in-one mount call:
43
45
 
@@ -48,7 +50,9 @@ const simulator = await mountF1Simulator(root, {
48
50
  onDriverOpen,
49
51
  seed,
50
52
  trackSeed,
53
+ warmup,
51
54
  totalLaps,
55
+ physicsMode,
52
56
  initialCameraMode,
53
57
  preset,
54
58
  title,
@@ -166,6 +170,7 @@ Returned controller:
166
170
  setPitIntent(driverId, intent, targetCompound),
167
171
  getPitIntent(driverId),
168
172
  getPitTargetCompound(driverId),
173
+ getSimulationSpeed(),
169
174
  servePenalty(penaltyId),
170
175
  cancelPenalty(penaltyId),
171
176
  callSafetyCar(),
@@ -208,27 +213,35 @@ Returned controller:
208
213
  - The host passes data, not internal DOM.
209
214
  - Host driver IDs and entry `driverId` values must be unique. Entries may omit `driverNumber`; provided numbers must be unique.
210
215
  - `totalLaps` is normalized to a finite positive integer before simulation so invalid input cannot produce zero-lap, negative-lap, or non-finite snapshots.
216
+ - `physicsMode` accepts `'arcade'` and `'simulator'`. The default is `'arcade'` to preserve existing hosts. `'simulator'` is opt-in and keeps cars controlled only through steering, throttle, brake, and pit intent while enabling traction-budget limits, steering scrub, velocity-heading slip, surface-specific grip/drag, and simulator telemetry. Snapshots expose `physicsMode` plus per-car `lateralG`, `longitudinalG`, `gripUsage`, `slipAngleRadians`, `tractionLimited`, `stabilityState`, and the latest `appliedControls`.
211
217
  - `restart(nextOptions)` can change race data and deterministic seeds such as `trackSeed`, but it does not support changing asset URLs. Asset changes require `destroy()` and a fresh mount because PixiJS texture loading is an initialization boundary.
212
218
  - `onDriverOpen(driver)` is the navigation boundary.
213
219
  - Lifecycle callbacks are optional: `onLoadingChange`, `onReady`, `onError`, `onDriverSelect`, `onRaceEvent`, `onLapChange`, and `onRaceFinish`. Host callback failures are routed to `onError` when possible and must not stop the simulator loop.
214
- - Race completion is part of the simulation snapshot. Cars receive individual `finished`, `finishTime`, `finishRank`, `status: 'waved-flag'`, `raceStatus: 'waved-flag'`, `wavedFlag`, `penaltySeconds`, `adjustedFinishTime`, and `classifiedRank` values as they cross the finish distance. The first finisher sets a provisional `raceControl.winner`; already-finished cars remain frozen in provisional finish order while the remaining cars complete the distance. Final `raceControl.classification` and `raceControl.finished` are set only after every car finishes. Final classification converts unserved drive-through and stop-go penalties into configured time, sorts by `finishTime + penaltySeconds`, applies position-drop and disqualification consequences, then race control switches to `safety-car`, the final order freezes to the classified result, and the field keeps circulating under safety-car behavior.
215
- - Race rules support package presets and custom module config. Supported rulesets are `paddock`, `grandPrix2025`, `fia2025`, and `custom`. Advanced modules include pit stops, tire strategy, penalties, weather, reliability, and fuel load. The current simulator normalizes all module config, creates/renders track-owned pit-lane geometry on an explicit start/finish straight, treats pit-lane asphalt, working-lane service areas, and garage boxes as legal drivable surfaces, runs automatic bounded pit-train entry/service/exit through the main fast lane and shared team service areas when pit stops are enabled, lets host/expert pit calls choose a target tire compound, uses the team queue point as a rolling gate when the service area is free, queues a second team car behind the active service area and moves it forward through a queue-release route only after the active service area is physically clear, keeps pit-route approach speed bounded until the final queue/service capture instead of crawling through open route, optionally varies pit-service time from team pit-crew stats with a perfect-training override, supports pit-lane open/closed state plus red flags, requests pit stops from configurable tire-energy thresholds, applies nonlinear tire-grip degradation down to 1%, applies the pit speed limiter only on the main pit lane/working lane, enforces pit-lane speeding on speed-limited pit-lane parts but not on entry/exit connectors, keeps the safety car about `55m` ahead of the leader with compact `22m` frozen-order queue slots, lets the built-in driver AI ride kerbs, attack, defend, and recover through normal steering/throttle/brake decisions with short controller-state commitments for rejoin and attack decisions, and enforces stewarded penalties for collisions, track limits, pit-lane speeding, and tire requirements.
220
+ - Race completion is part of the simulation snapshot. Cars receive individual `finished`, `finishTime`, `finishRank`, `status: 'waved-flag'`, `raceStatus: 'waved-flag'`, `wavedFlag`, `penaltySeconds`, `adjustedFinishTime`, and `classifiedRank` values as they cross the finish distance. Destroyed/out-of-race cars expose `dnf`, `dnfReason`, `dnfAt`, and `dnfOrder`, appear below active cars in timing, render faded/gray in the race canvas, and do not block race completion while they remain DNF. The first finisher sets a provisional `raceControl.winner`; already-finished cars remain frozen in provisional finish order while the remaining cars complete the distance. Final `raceControl.classification` and `raceControl.finished` are set only after every race participant has finished or is DNF. DNF entries are included after finishers with no finish time. A DNF car restored before final classification re-enters live timing and must finish; after final classification, resurrection does not reopen the race. Final classification converts unserved drive-through and stop-go penalties into configured time, sorts finishers by `finishTime + penaltySeconds`, applies position-drop and disqualification consequences, then race control switches to `safety-car`, the final order freezes to the classified result, and the field keeps circulating under safety-car behavior.
221
+ - Race rules support package presets and custom module config. Supported rulesets are `paddock`, `grandPrix2025`, `fia2025`, and `custom`. Advanced modules include pit stops, tire strategy, tire degradation, stalled off-track DNF, penalties, weather, reliability, and fuel load. The current simulator normalizes all module config, creates/renders track-owned pit-lane geometry on an explicit start/finish straight, treats pit-lane asphalt, working-lane service areas, and garage boxes as legal drivable surfaces, runs automatic bounded pit-train entry/service/exit through the main fast lane and shared team service areas when pit stops are enabled, lets host/expert pit calls choose a target tire compound, uses the team queue point as a rolling gate when the service area is free, queues a second team car behind the active service area and moves it forward through a queue-release route only after the active service area is physically clear, keeps pit-route approach speed bounded until the final queue/service capture instead of crawling through open route, optionally varies pit-service time from team pit-crew stats with a perfect-training override, supports pit-lane open/closed state plus red flags, requests pit stops from configurable tire-energy thresholds, applies nonlinear tire-grip degradation down to 1% unless `rules.modules.tireDegradation.enabled` is `false`, can retire cars that remain off legal racing/pit surfaces below the configured stalled-DNF speed threshold for the configured duration when `rules.modules.stalledDnf.enabled` is `true`, applies the pit speed limiter only on the main pit lane/working lane, enforces pit-lane speeding on speed-limited pit-lane parts but not on entry/exit connectors, keeps the safety car about `55m` ahead of the leader with compact `22m` frozen-order queue slots, lets the built-in driver AI ride kerbs, attack, defend, and recover through normal steering/throttle/brake decisions with short controller-state commitments for rejoin and attack decisions, and enforces stewarded penalties for collisions, track limits, pit-lane speeding, and tire requirements.
216
222
  - Penalty subsections use `strictness` from `0` to `1`, not only boolean enablement. `strictness: 0` means the subsection is not enforced; `strictness: 1` applies the configured rule margin. Steward decisions are exposed as `penalty` events and as top-level `snapshot.penalties`. Penalty entries include normalized consequences and lifecycle status. Immediate consequences apply time, grid, position, or disqualification effects directly; drive-through and stop-go consequences remain issued until served, cancelled, or converted at final classification.
217
223
  - The browser UI can opt into top steward messages with `ui.penaltyBanners` and timing-row penalty badges with `ui.timingPenaltyBadges`. Steward messages render track-limit warning events and penalty decisions from the simulation; time-penalty messages put the penalty seconds in a large left chip and use penalty-colored backgrounds, while warnings use warning-colored backgrounds. Timing-row `!` badges are rendered only from `snapshot.penalties`; warning events do not count. UI code must not recalculate steward decisions.
218
224
  - The simulator must stay interactive after being installed through `npm install @inventure71/paddockjs`.
219
225
  - The package must build correctly through a browser bundler that supports JavaScript modules, CSS imports, and image imports.
220
226
  - The simulation should remain deterministic for the same seed, track seed, drivers, entries, and rules.
221
- - When `trackSeed` is omitted in a browser mount, the simulator creates a fresh procedural circuit for that mount. Explicit `trackSeed` values are deterministic and cached by seed for repeated mounts. Procedural generation rejects circuits with excessive local heading jumps or turn accumulation so the built-in physics-driven AI can push the generated layout without requiring pose correction.
227
+ - Warmup is enabled by default for browser mounts, headless environments, and direct simulation creation. It runs on a disposable runtime and must not mutate the visible initial race state; under the default `warmup.policy: 'config-change'`, identical configuration fingerprints reuse cached warmup while seed/config changes rerun warmup automatically.
228
+ - When `trackSeed` is omitted in a browser mount, the simulator creates a fresh procedural circuit for that mount. Explicit `trackSeed` values are deterministic and cached by seed plus resolved generation options for repeated mounts. Cached procedural definitions are immutable; callers that import `createProceduralTrack(seed, options)` should treat the returned definition as read-only and clone it before custom mutation. `trackGeneration` forwards the same procedural options used by `createProceduralTrack(seed, options)`: `profile`, `length`, `startStraight`, `pitLane`, `shape`, `validation`, and `attempts`. The `race` profile preserves the default full circuit with pit lane; `training-short`, `training-medium`, and `training-technical` are smaller pitless presets intended for training or demos. Explicit option fields override profile defaults after the profile is resolved. Procedural generation traces seeded connected region boundaries, smooths and warps them into centerline controls, then rejects circuits with excessive local heading jumps, turn accumulation, self-intersections, poor clearance, invalid length, or weak shape variation.
222
229
  - The renderer should target a paced 60 FPS simulation/render loop.
223
230
  - The render loop should pause while the race canvas is offscreen or the document is hidden, then resume without catching up the elapsed hidden time. Layout measurements needed for overlay camera safe areas should be cached between resize/layout invalidations. Runtime DOM updates should skip unchanged text/markup so visible embeds do not rewrite stable readouts every frame.
224
231
  - Restart and rerender paths must destroy replaced PixiJS display children while preserving shared loaded textures.
225
232
  - Restart supports race/data/seed changes but does not support asset URL changes or browser expert mode changes. Texture loading and ticker ownership are mount-time boundaries; hosts must destroy and mount again to change either boundary.
226
- - Expert environment code can create a headless `createPaddockEnvironment()` from the `@inventure71/paddockjs/environment` subpath. It requires explicit `controlledDrivers`, accepts normalized actions `{ steering, throttle, brake }` plus optional `pitIntent` and `pitCompound`, advances only through `step(actions)`, and returns environment-loop JavaScript results with `observation`, `reward`, `terminated`, `truncated`, `done`, `events`, `state`, and `info`. Controlled drivers do not receive tire-threshold automatic pit calls; they request automatic pit service through `pitIntent`, may choose the target tire through `pitCompound`, and observe pit-lane/service/race-control state in `observation[driverId].object.self` and `.race`. `pitIntent: 0` is a no-op clear request and does not fail when pit stops are disabled. `self.onTrack` follows wheel-level legality, so track, kerb, and legal pit-lane/box surfaces are on-track while gravel/grass/barrier are off-track. Environment scenarios may set reset positions with `preset`, `placements`, and relative `traffic`; this is setup data, not a policy action.
227
- - Expert ray sensors originate from the controlled car center. The default compact set is `[-135, -60, -20, 0, 20, 60, 135, 180]`, giving forward, side, and rear awareness while staying small. Rays detect track edges against the actual track geometry and detect car hits by ray-to-car-footprint intersection.
233
+ - Expert environment code can create a headless `createPaddockEnvironment()` from the `@inventure71/paddockjs/environment` subpath. It requires explicit `controlledDrivers`, accepts normalized actions `{ steering, throttle, brake }` plus optional `pitIntent` and `pitCompound`, advances only through `step(actions)`, and returns environment-loop JavaScript results with `observation`, `reward`, `metrics`, `terminated`, `truncated`, `done`, `events`, `state`, and `info`. `steering` is an absolute normalized steering target: `-1` points at maximum left, `0` points at center, `1` points at maximum right, and intermediate values are percentages of the maximum steering angle; the vehicle integrator rate-limits motion toward that target instead of snapping the wheel. `steering`, `throttle`, and `brake` are required for each controlled-driver action. Strict action policy throws on missing or non-finite vehicle controls, while report policy records the error and releases stale manual controls so the built-in driver owns that physics step. `info.drivers[driverId]` exposes per-driver `terminated`, `truncated`, `endReason`, `episodeStep`, and `episodeId` for batched loops; selected-driver resets clear only selected driver truncation/termination and do not rewind global `info.step`. Controlled drivers do not receive tire-threshold automatic pit calls; they request automatic pit service through `pitIntent`, may choose the target tire through `pitCompound`, and observe pit-lane/service/race-control state in `observation[driverId].object.self` and `.race`. `pitIntent: 0` is a no-op clear request and does not fail when pit stops are disabled. `self.onTrack` follows wheel-level legality, so track, kerb, and legal pit-lane/box surfaces are on-track while gravel/grass/barrier are off-track. Barrier-wall contact in both physics modes is measured against the rendered wall's inner face; contact marks the car destroyed/DNF, removes it from active collision/sensor participation, emits a `car-destroyed` event, and terminates that driver's episode with `endReason: 'destroyed'`. Opt-in stalled off-track DNF removes the car from the same active participation systems while keeping `destroyed: false` and terminates controlled-driver episodes with `endReason: 'stalled-off-track'` when `rules.modules.stalledDnf.enabled` is `true`. Reset is still an episode-boundary `resetDrivers()` call. Environment scenarios may set reset positions with `preset`, `placements`, and relative `traffic`; `resetDrivers(placements)` may reset selected controlled drivers between episodes without recreating the whole simulation. Partial `reset(options)` calls preserve omitted nested option groups while replacing arrays and explicit scenario placement maps. After placement, selected cars are classified by the same runoff/barrier rules before observations are returned, so cars already inside terminal barrier space report destroyed metrics and stable miss-valued rays instead of doing alive-car far-out ray scans. Both scenario placement and `resetDrivers()` are setup data, not policy actions. `participantInteractions` may make real physics cars non-colliding, sensor-hidden, non-blocking for pit occupancy, or excluded from race order, but those cars still live in `snapshot.cars` and move only through normal physics/control APIs. The `batch-training` profile is non-colliding, sensor-hidden, pit-non-blocking, excluded from race order, and still rendered. `replayGhosts` are separate trajectory-driven entities in `snapshot.replayGhosts`; they are visual/reference overlays and never enter car physics, timing, pit, order, or penalty systems. Replay ghosts are sensor-hidden unless their own sensor flags explicitly opt them into ray or nearby observations.
234
+ - Headless environments also accept an optional `externalRenderer` observer hook. It receives `{ snapshot, observation, meta }` on `reset`, `step`, and `resetDrivers`, so external tooling can stream authoritative frames to visualizers without letting that transport mutate simulation state.
235
+ - Controller loop code should use `createPaddockDriverControllerLoop()` instead of duplicating browser/headless stepping. Its controller context groups observations by controlled driver id, includes `orderedObservations` for direct batch tensor assembly, exposes metrics, events, cached specs, and reset-driver ids, and keeps per-driver resets as controller reset hooks when the runtime supports `resetDrivers()`. `previousActions` is the last action map actually applied before the current `actions`; on the first controlled frame after reset it is empty, and after repeated frames it reflects the prior physics step rather than pre-copying the current decision.
236
+ - Expert ray sensors originate from the controlled car center. The default compact set is `[-135, -60, -20, 0, 20, 60, 135, 180]`, giving forward, side, and rear awareness while staying small. Rays detect track edges against the actual track geometry and detect car hits by ray-to-car-footprint intersection. Hosts may opt into per-ray lengths, predefined layouts such as `driver-front-heavy`, and surface-aware channels for kerb and illegal surface detection; those channels are computed only when requested and use analytic acceleration plus indexed ray-boundary intersections across the legal surface and nearby off-track recovery band before falling back to indexed sampled geometry where needed. Batch-training ray runs keep the same driver-facing road-edge, kerb, and illegal-surface distances by using the indexed geometry path rather than a local strip approximation. Barrier walls are rendered/physical terminal boundaries, not model-facing ray targets; active ray objects, vectors, schemas, and visualizations must not expose a `barrier` ray channel. Ray `precision` defaults to `driver`, which is the active model-facing sensor contract; `debug` precision is only for explicitly labeled diagnostics. `observation.profile: 'physical-driver'` exposes local driver-like yaw, contact-patch, boundary, richer ray, and opponent-radar senses while omitting default lookahead samples unless explicitly configured. `observation.output` can be `full`, `vector`, or `object`, `includeSchema: false` omits repeated schema payloads, and `vectorType: 'float32'` returns typed vector buffers for high-throughput JavaScript loops. `result.stateOutput` can be `full`, `minimal`, or `none`; `none` returns `state: null` and is intended for loops that rely on observations, metrics, and info instead of full snapshots.
237
+ - Track surface bands are single-source geometry. Rendering derives visible gravel, runoff, and barrier offsets from the same track widths used by wheel surfaces and ray surface hits: road edge, kerb outer edge, gravel outer edge, runoff outer edge, and barrier wall width. New code must not add independent magic offsets for these bands.
228
238
  - Browser expert mode is opt-in with `expert: { enabled: true, controlledDrivers, frameSkip }`. When enabled, the returned controller exposes `expert.reset()`, `expert.step(actions)`, `expert.getObservation()`, and `expert.getState()`.
239
+ - Browser expert mode also exposes `attachExternalRenderer(source)`, `detachExternalRenderer()`, and `getExternalRendererState()` for render-only external-frame playback. The `source` callback contract is `subscribe(onFrame) => unsubscribe`, where each frame is `{ snapshot, observation, meta? }`.
229
240
  - Browser expert mode wraps the same `RaceSimulation` instance that the visual canvas renders. It must not create a parallel simulation for the same mount.
230
241
  - Browser expert mode disables automatic ticker-driven simulation advancement. The visual canvas updates only after explicit expert `reset()` or `step(actions)` calls.
231
- - Browser expert mode may opt into `expert.visualizeSensors: true` or `expert.visualizeSensors: { rays: true }`. When enabled, ray sensors render in the race canvas world layer from the controlled cars, using the same observation result produced by explicit expert steps.
242
+ - While external renderer mode is attached, browser expert runtime is strict render-only and rejects local `step()`, `resetDrivers()`, and `reset()`. Hosts must detach before returning to local stepping; detach restores the current local simulation track surface if external frames had redrawn the shared track asset.
243
+ - Policy Runner `Live preview stream` mode accepts `ws://`/`wss://` push streams and `http://`/`https://` polling endpoints that return `preview:snapshot` frames.
244
+ - Browser expert mode may opt into `expert.visualizeSensors: true` or `expert.visualizeSensors: { rays: true }`. When enabled, ray sensors render in the race canvas world layer from the selected controlled car by default, using the same observation result produced by explicit expert steps. The overlay draws a separate colored marker for each detected active channel on each ray, including road-edge, kerb, illegal surface, and car hits. Barrier walls are visible track geometry, not ray-hit markers, and browser components must not reintroduce a barrier ray label. Hosts may request `visualizeSensors: { rays: true, drivers: 'all' }` for the heavier all-controlled-car overlay or pass an explicit driver id list. Every model-facing sense follows this rule: the model receives the active environment observation, and Policy Runner/expert visualization displays that same observation instead of recomputing a more precise or different browser-only value. Extra diagnostics must be clearly separated from model senses.
232
245
 
233
246
  ## Current Visible Features
234
247
 
@@ -270,11 +283,11 @@ npm run check
270
283
 
271
284
  Expected:
272
285
 
273
- - All Vitest tests pass.
286
+ - Fast Vitest tests pass in the normal local gate. Slow characterization tests run under `npm run check:release`.
274
287
  - `npm pack --dry-run` succeeds and includes source files plus bundled assets.
275
288
  - A packed tarball installs and builds inside a fresh temporary Vite consumer app.
276
289
  - The tracked showcase host builds.
277
- - Chromium browser smoke verifies desktop/mobile canvas rendering, package-panel overflow constraints, public API buttons, and visual policy-runner stepping.
290
+ - The quick Chromium browser smoke verifies showcase canvas rendering, overflow constraints, and one public API action. The release browser smoke verifies the desktop/mobile matrix, package-panel overflow constraints, public API buttons, and visual policy-runner stepping across generation and race configurations.
278
291
 
279
292
  Run from a browser host that consumes the published package:
280
293