@inventure71/paddockjs 1.0.0 → 2.0.1

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 (203) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/INSTALL_AND_UPDATE.md +4 -3
  3. package/README.md +219 -12
  4. package/docs/architecture.md +126 -45
  5. package/docs/concepts.md +9 -5
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +317 -22
  8. package/docs/data_model.md +1 -1
  9. package/docs/index.md +10 -0
  10. package/docs/learnings.md +22 -0
  11. package/docs/rules.md +71 -14
  12. package/docs/sense_contract.md +59 -0
  13. package/docs/system_specs.md +75 -13
  14. package/docs/training.md +280 -32
  15. package/examples/python/README.md +62 -0
  16. package/examples/python/base_policy_server.py +191 -0
  17. package/examples/python/requirements.txt +2 -0
  18. package/package.json +12 -4
  19. package/src/api/PaddockSimulatorController.js +6 -33
  20. package/src/app/BrowserExpertAdapter.js +76 -9
  21. package/src/app/BrowserExpertExternalRenderer.js +303 -0
  22. package/src/app/F1SimulatorApp.js +291 -1630
  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 +4 -0
  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 +20 -0
  48. package/src/config/restartOptions.js +34 -0
  49. package/src/environment/actions.js +13 -5
  50. package/src/environment/controllerLoop.js +261 -0
  51. package/src/environment/episode.js +98 -2
  52. package/src/environment/evaluation.js +54 -32
  53. package/src/environment/index.d.ts +376 -10
  54. package/src/environment/index.js +1 -0
  55. package/src/environment/metrics.js +159 -0
  56. package/src/environment/observationOptions.js +22 -1
  57. package/src/environment/observationVector.js +200 -0
  58. package/src/environment/observations.js +199 -141
  59. package/src/environment/options.js +37 -5
  60. package/src/environment/recorder.js +13 -6
  61. package/src/environment/rewards.js +13 -2
  62. package/src/environment/runtime.js +272 -23
  63. package/src/environment/scenarios.js +57 -9
  64. package/src/environment/sensors/bodySenses.js +23 -0
  65. package/src/environment/sensors/boundarySenses.js +17 -0
  66. package/src/environment/sensors/carRays.js +75 -0
  67. package/src/environment/sensors/contactSenses.js +32 -0
  68. package/src/environment/sensors/index.js +157 -0
  69. package/src/environment/sensors/indexedRayBands.js +103 -0
  70. package/src/environment/sensors/nearbyCars.js +52 -0
  71. package/src/environment/sensors/opponentRadar.js +75 -0
  72. package/src/environment/sensors/rayConfig.js +110 -0
  73. package/src/environment/sensors/rayDefaults.js +6 -0
  74. package/src/environment/sensors/rayGeometry.js +60 -0
  75. package/src/environment/sensors/rayGuards.js +19 -0
  76. package/src/environment/sensors/sensorTargets.js +66 -0
  77. package/src/environment/sensors/surfaceRays.js +360 -0
  78. package/src/environment/sensors/trackRays.js +424 -0
  79. package/src/environment/sensors.js +1 -367
  80. package/src/environment/specs.js +159 -26
  81. package/src/environment/workerProtocol.js +13 -1
  82. package/src/index.d.ts +171 -6
  83. package/src/index.js +6 -32
  84. package/src/rendering/proceduralTrackAsset.js +20 -823
  85. package/src/rendering/renderSnapshot.js +38 -12
  86. package/src/rendering/track/finishLineRenderer.js +34 -0
  87. package/src/rendering/track/gridRenderer.js +47 -0
  88. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  89. package/src/rendering/track/pitLaneRenderer.js +248 -0
  90. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  91. package/src/rendering/track/trackRenderConstants.js +38 -0
  92. package/src/rendering/track/trackRenderGeometry.js +94 -0
  93. package/src/simulation/collisionGeometry.js +1 -1
  94. package/src/simulation/driver/driverControlConstants.js +28 -0
  95. package/src/simulation/driver/driverController.js +47 -0
  96. package/src/simulation/driver/driverInput.js +28 -0
  97. package/src/simulation/driver/driverMath.js +6 -0
  98. package/src/simulation/driver/driverPersonality.js +25 -0
  99. package/src/simulation/driver/edgeRecovery.js +48 -0
  100. package/src/simulation/driver/racingControls.js +399 -0
  101. package/src/simulation/driver/racingLinePlan.js +166 -0
  102. package/src/simulation/driver/recoveryDynamics.js +38 -0
  103. package/src/simulation/driver/rejoinControls.js +213 -0
  104. package/src/simulation/driver/safetyCarControls.js +35 -0
  105. package/src/simulation/driver/trafficScan.js +51 -0
  106. package/src/simulation/driverController.js +4 -619
  107. package/src/simulation/participants/participantInteractions.js +120 -0
  108. package/src/simulation/pit/pitFlow.js +239 -0
  109. package/src/simulation/pit/pitIntent.js +92 -0
  110. package/src/simulation/pit/pitKinematics.js +24 -0
  111. package/src/simulation/pit/pitOccupancy.js +47 -0
  112. package/src/simulation/pit/pitPenaltyService.js +71 -0
  113. package/src/simulation/pit/pitQueue.js +60 -0
  114. package/src/simulation/pit/pitRouteMovement.js +167 -0
  115. package/src/simulation/pit/pitRouting.js +319 -0
  116. package/src/simulation/pit/pitService.js +43 -0
  117. package/src/simulation/pit/pitServiceConstants.js +21 -0
  118. package/src/simulation/pit/pitServiceProfile.js +45 -0
  119. package/src/simulation/pit/pitSnapshots.js +61 -0
  120. package/src/simulation/pit/pitState.js +113 -0
  121. package/src/simulation/pit/pitTireService.js +94 -0
  122. package/src/simulation/race/classification.js +104 -0
  123. package/src/simulation/race/gridPenalties.js +37 -0
  124. package/src/simulation/race/raceControlState.js +38 -0
  125. package/src/simulation/race/raceDistance.js +32 -0
  126. package/src/simulation/race/raceFinish.js +65 -0
  127. package/src/simulation/race/raceLifecycle.js +67 -0
  128. package/src/simulation/race/raceOrder.js +113 -0
  129. package/src/simulation/race/raceProgress.js +129 -0
  130. package/src/simulation/race/raceSetup.js +93 -0
  131. package/src/simulation/race/raceStep.js +80 -0
  132. package/src/simulation/race/redFlag.js +15 -0
  133. package/src/simulation/race/retirements.js +44 -0
  134. package/src/simulation/race/safetyCar.js +92 -0
  135. package/src/simulation/race/stalledDnf.js +77 -0
  136. package/src/simulation/raceSimulation.js +231 -2817
  137. package/src/simulation/replay/replayGhosts.js +128 -0
  138. package/src/simulation/rules/moduleConfig.js +72 -0
  139. package/src/simulation/rules/penaltyConfig.js +99 -0
  140. package/src/simulation/rules/penaltyEvents.js +18 -0
  141. package/src/simulation/rules/penaltyStats.js +26 -0
  142. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  143. package/src/simulation/rules/ruleDefaults.js +85 -0
  144. package/src/simulation/rules/rulesReview.js +144 -0
  145. package/src/simulation/rules/rulesetPresets.js +47 -0
  146. package/src/simulation/rules/trackLimitsSteward.js +1 -1
  147. package/src/simulation/rulesConfig.js +5 -323
  148. package/src/simulation/snapshots/raceSnapshots.js +193 -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/trackQueryGrid.js +94 -0
  171. package/src/simulation/track/trackQueryIndex.js +659 -0
  172. package/src/simulation/track/trackQueryPitIndex.js +140 -0
  173. package/src/simulation/track/trackQueryScratch.js +46 -0
  174. package/src/simulation/track/trackQueryStats.js +28 -0
  175. package/src/simulation/track/trackSectors.js +19 -0
  176. package/src/simulation/track/trackStatePolicy.js +24 -0
  177. package/src/simulation/track/trackValidation.js +98 -0
  178. package/src/simulation/trackModel.js +1 -1706
  179. package/src/simulation/vehicle/contactResolution.js +181 -0
  180. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  181. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  182. package/src/simulation/vehicle/runoffResponse.js +87 -0
  183. package/src/simulation/vehicle/surfacePriority.js +28 -0
  184. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  185. package/src/simulation/vehicle/vehicleKinematics.js +37 -0
  186. package/src/simulation/vehicle/vehiclePhysics.js +364 -0
  187. package/src/simulation/vehicle/vehicleSnapshots.js +327 -0
  188. package/src/simulation/vehicle/vehicleState.js +220 -0
  189. package/src/simulation/vehicle/wheelSurface.js +152 -0
  190. package/src/simulation/vehicleGeometry.js +1 -247
  191. package/src/simulation/vehiclePhysics.js +1 -136
  192. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  193. package/src/simulation/wheelSurface.js +1 -338
  194. package/src/styles.css +42 -0
  195. package/src/ui/cameraControlsTemplate.js +20 -0
  196. package/src/ui/carOverviewTemplate.js +33 -0
  197. package/src/ui/componentTemplates.js +14 -360
  198. package/src/ui/raceCanvasTemplate.js +69 -0
  199. package/src/ui/raceControlsTemplate.js +36 -0
  200. package/src/ui/shellTemplate.js +5 -4
  201. package/src/ui/telemetryDrawerTemplate.js +55 -0
  202. package/src/ui/telemetryTemplates.js +136 -0
  203. package/src/ui/timingTowerTemplate.js +34 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Harden the simulator training environment and model-facing sense contract. This patch keeps vector-only observations aligned with full object observations, keeps indexed ray acceleration equivalent to the legacy ray contract for driver-facing senses, preserves stable terminal-car and pit-route state during training loops, and documents the stricter model-sense boundary.
8
+
9
+ ## 2.0.0
10
+
11
+ ### Major Changes
12
+
13
+ - Promote the `v6` simulator work as the next major PaddockJS release. This release keeps the package boundary focused on browser-mounted simulator components, composable simulator surfaces, and browser-free JavaScript environment/control APIs.
14
+ - Add the stricter opt-in `physicsMode: 'simulator'` vehicle model with 2D velocity/yaw dynamics, traction limits, speed-sensitive steering, steering scrub, slip telemetry, reduced off-road grip, and simulator-mode AI tuning. The default remains `physicsMode: 'arcade'` for existing hosts.
15
+ - Add richer procedural track generation with semantic profiles (`race`, `training-short`, `training-medium`, and `training-technical`), validated connected-region centerlines, profile-level generation controls, deterministic `trackGeneration` options, DRS zones, pit-lane access geometry, and cached track query indexes.
16
+ - Add hard barrier-wall consequences and opt-in stalled off-track DNF handling. Barrier contact now marks cars destroyed/DNF in both physics modes, while `rules.modules.stalledDnf` can retire cars that remain stopped off legal surfaces.
17
+ - Expand the headless environment contract for real training loops: compact vector/object/full observation output, optional `Float32Array` vectors, schema caching, no-state/minimal/full result output, per-driver episode info, selected-driver resets, reset-only scenario placements, reward normalization, evaluation helpers, rollout metrics, and a shared driver-controller loop.
18
+ - Add physical-driver observation support with richer body/boundary/contact senses, opponent radar, configurable ray layouts, surface-aware ray channels, driver/debug precision modes, and model-sense visualization that renders the active observation rather than recomputing browser-only values.
19
+ - Add participant interaction profiles and replay ghosts for training/comparison workflows. `batch-training`, `isolated-training`, and related profiles can make real physics participants non-colliding, sensor-hidden, pit-non-blocking, or race-order-excluded, while replay ghosts remain separate trajectory overlays outside car physics/rules.
20
+ - Add Policy Runner support for the three package-supported controller paths: browser-run distilled policy playback, JSON policy-server action loops, and live preview streams for externally rendered snapshots.
21
+ - Add Python bridge examples for policy-server integration through `examples/python/base_policy_server.py` plus a minimal `requirements.txt`.
22
+ - Add UI/runtime controls for simulation speed, external rendering/live preview, replay ghost drawing, no-collision markers, debug-only physics-mode indicators, runtime warmup, visibility-aware frame loops, and reduced high-speed DOM refresh cost.
23
+
24
+ ### Package UI And Showcase
25
+
26
+ - Rework the tracked `local-preview` showcase into release coverage pages for templates, components, API controls, behavior/layout contracts, stewarding, collision lab, Policy Runner, and rules.
27
+ - Add generated showcase coverage cards and hideable example-code panels so each documented mount/control path has an accurate colocated example.
28
+ - Add the Rules page and update showcase navigation across all tracked pages.
29
+ - Clean up showcase composition bugs around loaded simulator heights, placeholder min-heights, section-title wrapping, empty/wrong code examples, and host embed overflow.
30
+ - Remove the old standalone `expert-environment.html` page after folding the supported environment/control coverage into the broader docs and Policy Runner paths.
31
+
32
+ ### Architecture And Maintenance
33
+
34
+ - Split large facade files into feature-owned modules for app banners, camera control, readouts, rendering, runtime lifecycle, sensors, track generation/querying, pit flow, race lifecycle, race order, race finish/classification, rules, timing, driver control, vehicle physics, and snapshots.
35
+ - Keep compatibility barrels thin while moving implementation into canonical feature directories such as `src/simulation/track/`, `src/simulation/vehicle/`, `src/simulation/driver/`, `src/simulation/timing/`, `src/simulation/pit/`, `src/environment/sensors/`, and `src/app/`.
36
+ - Add fast/slow Vitest mode helpers, release browser-smoke coverage, local-preview markup regressions, policy-runner encoder/server tests, warmup tests, physics-mode tests, expert sensor renderer tests, and a track-query-index benchmark script.
37
+ - Update package release gates so `npm run check` runs fast tests, public type checks, dry pack, packed-consumer smoke, tracked showcase build, and quick Chromium smoke; `npm run check:release` adds slow characterization and the full browser smoke matrix.
38
+
39
+ ### Fixes And Behavior Cleanup
40
+
41
+ - Ensure browser expert teardown destroys the expert adapter and detaches external renderer subscriptions.
42
+ - Preserve external renderer driver IDs when incoming live-preview frames already use local simulator driver IDs.
43
+ - Keep browser expert rendering stable when compact environment result options request no returned state payload.
44
+ - Validate missing/non-finite controlled-driver actions instead of silently applying stale or zero controls.
45
+ - Keep environment reset behavior stable when partial reset options omit nested groups or intentionally replace scenario placements.
46
+ - Keep no-collision participants and replay ghosts aligned across rendering, sensors, race order, collision, pit occupancy, and public snapshots.
47
+ - Keep model-facing observations and visual sensor overlays aligned to the active observation contract, with debug precision reserved for labeled diagnostics.
48
+ - Make the debug physics-mode square opt-in through `debug.physicsModeIndicator` and hidden by default for package consumers.
49
+ - Remove the deleted `training-lab` workflow from the package boundary; release artifacts include supported docs/examples only.
50
+
51
+ ### Documentation
52
+
53
+ - Add the Custom Model Controller Guide for wrapping a trained model as a batched controller usable from browser playback or headless loops.
54
+ - Expand README, system specs, data contract, training guide, rules, concepts, architecture notes, learnings, and install/update workflow for the new simulator, environment, Policy Runner, showcase, and release-gate behavior.
55
+ - Document the Python policy-server boundary as an example bridge, not a packaged Python Gymnasium/PettingZoo integration.
56
+
57
+ ### Known Future Scope
58
+
59
+ - Weather effects, reliability failures, fuel-load performance effects, static obstacles, debug mutation APIs, assisted controls, and packaged Python Gymnasium/PettingZoo wrappers remain intentionally out of 2.0.0.
60
+
3
61
  ## 1.0.0
4
62
 
5
63
  ### Major Changes
@@ -112,7 +112,7 @@ npm run check
112
112
  Install a specific version when the host needs a controlled upgrade:
113
113
 
114
114
  ```bash
115
- npm install @inventure71/paddockjs@0.2.0
115
+ npm install @inventure71/paddockjs@2.0.1
116
116
  npm run check
117
117
  ```
118
118
 
@@ -122,9 +122,10 @@ After updating, smoke-test the page that mounts the simulator. Browser behavior
122
122
 
123
123
  The package repo owns its release process:
124
124
 
125
- - `npm run check` runs runtime tests, public type verification, dry-pack verification, packed-consumer install/build verification, the tracked showcase build, and Chromium browser smoke tests.
125
+ - `npm run check` runs fast runtime tests, public type verification, dry-pack verification, packed-consumer install/build verification, the tracked showcase build, and a quick Chromium browser smoke test.
126
+ - `npm run check:release` runs the exhaustive release gate, including slow characterization tests and the full Chromium browser smoke matrix.
126
127
  - `npm run consumer:smoke` packs the package, installs the tarball into a fresh temporary Vite app, and builds that app through public package imports.
127
- - `npm run browser:smoke` builds `local-preview`, starts a local preview server, and checks desktop/mobile canvas rendering, overflow constraints, API buttons, and visual policy-runner stepping in Chromium.
128
+ - `npm run browser:smoke` builds `local-preview`, starts a local preview server, and checks desktop/mobile canvas rendering, overflow constraints, API buttons, and visual policy-runner stepping in Chromium. Use `npm run browser:smoke:quick` for the smaller local browser pass and `npm run browser:smoke:full` for the full matrix.
128
129
  - `npm run changeset` records the next version bump and changelog note.
129
130
  - `npm run version-packages` applies pending Changesets locally.
130
131
  - `.github/workflows/ci.yml` verifies the package on push and pull request.
package/README.md CHANGED
@@ -12,7 +12,7 @@ npm install @inventure71/paddockjs
12
12
 
13
13
  ## Documentation
14
14
 
15
- Start with [docs/index.md](docs/index.md) for system specs, rules, concepts, data contracts, and architecture notes.
15
+ Start with [docs/index.md](docs/index.md) for the full package map: what to install, which API to use, what data to pass, how rules modules work, how the browser and headless runtimes differ, and which verification commands prove a change. If you are integrating the package into a host site, read [System Specs](docs/system_specs.md), [Data Contract](docs/data_contract.md), and [Rules](docs/rules.md) first. If you already trained a driver model and want to run it in PaddockJS, use the [Custom Model Controller Guide](docs/custom_model_controller.md).
16
16
 
17
17
  ## Package Workflow
18
18
 
@@ -28,6 +28,7 @@ Useful commands:
28
28
 
29
29
  ```bash
30
30
  npm run check
31
+ npm run check:release
31
32
  npm run consumer:smoke
32
33
  npm run browser:smoke
33
34
  npm run showcase:dev
@@ -35,10 +36,16 @@ npm run showcase:build
35
36
  npm run changeset
36
37
  ```
37
38
 
38
- `npm run check` verifies runtime tests, public declarations, dry package contents, packed-package consumption in a fresh Vite app, the showcase build, and real Chromium smoke tests against the showcase pages.
39
+ `npm run check` is the normal local gate: fast runtime tests, public declarations, dry package contents, packed-package consumption in a fresh Vite app, the showcase build, and a quick Chromium smoke against the showcase. `npm run check:release` runs the same package gates plus slow characterization tests and the full browser smoke matrix.
39
40
 
40
41
  Local development and showcase builds require Node `20.19.0` or newer. CI currently runs the package check on Node 22 and releases on Node 24.
41
42
 
43
+ ## 2.0 Release Highlights
44
+
45
+ The `2.0.x` release line adds the stricter opt-in simulator physics mode, richer procedural track generation, indexed track/ray queries, barrier destruction and stalled-DNF behavior, batch-training participant profiles, replay ghosts, compact/vectorized environment output, policy-server/live-preview Policy Runner paths, Python policy-server examples, the custom model controller guide, the Rules showcase page, and the full release-gated local preview coverage.
46
+
47
+ The package still does not ship trained model weights, model storage, a Python Gymnasium/PettingZoo package, static obstacles, weather, reliability failures, fuel-load effects, or debug mutation APIs. Those remain host-owned or future scope.
48
+
42
49
  ## Expert Environment API
43
50
 
44
51
  Headless training code imports the environment subpath:
@@ -48,7 +55,9 @@ import { createPaddockEnvironment } from '@inventure71/paddockjs/environment';
48
55
  ```
49
56
 
50
57
  The package root remains the browser component API. The environment subpath is intentionally browser-free and does not import DOM, PixiJS, or package CSS.
51
- PaddockJS is a bring-your-own-model environment. It does not choose an ML framework, store model weights, or ship a trained driver. See [Bring Your Own Model](docs/training.md) for the policy shape and visual playback loop.
58
+ PaddockJS is a bring-your-own-model environment. It does not choose an ML framework, store model weights, or ship a trained driver. The shared `createPaddockDriverControllerLoop()` helper lets a user-owned controller run against either browser expert mode or `createPaddockEnvironment()` with the same batched `decideBatch(context)` call shape.
59
+
60
+ Browser and headless environments default to `physicsMode: 'arcade'` for existing hosts. Opt into the stricter vehicle model with `physicsMode: 'simulator'` when you want 2D velocity/yaw dynamics, traction limits, steering scrub, derived slip telemetry, reduced off-road grip, and simulator-mode AI tuning:
52
61
 
53
62
  ```js
54
63
  const env = createPaddockEnvironment({
@@ -56,6 +65,7 @@ const env = createPaddockEnvironment({
56
65
  entries,
57
66
  controlledDrivers: ['budget'],
58
67
  frameSkip: 2,
68
+ physicsMode: 'simulator',
59
69
  });
60
70
 
61
71
  let result = env.reset();
@@ -64,7 +74,27 @@ result = env.step({
64
74
  });
65
75
  ```
66
76
 
67
- The environment can run with no reward, as above, or with a host-supplied `reward(context)` callback. The repository also includes dependency-free examples that use the same environment contract:
77
+ Action steering is an absolute normalized steering target: `-1` points the wheel to maximum left, `0` points it to center, `1` points it to maximum right, and intermediate values target the same percentage of the maximum angle. Physics still rate-limits how quickly the steering wheel reaches that target.
78
+
79
+ The environment can run with no reward, as above, or with a host-supplied `reward(context)` callback. Reward formulas stay user-owned; PaddockJS only supplies the neutral facts:
80
+
81
+ ```js
82
+ const env = createPaddockEnvironment({
83
+ drivers,
84
+ entries,
85
+ controlledDrivers: ['budget'],
86
+ reward({ metrics, episode }) {
87
+ if (metrics.destroyed) return -200;
88
+ if (metrics.offTrack) return -12;
89
+ if (episode.terminated) return 0;
90
+ return metrics.legalProgressDeltaMeters;
91
+ },
92
+ });
93
+ ```
94
+
95
+ Reward callback results are converted to numbers per controlled driver. Missing, `NaN`, or infinite callback results become a neutral `0` so one bad callback value cannot poison a training batch.
96
+
97
+ The repository also includes dependency-free examples that use the same environment contract:
68
98
 
69
99
  ```bash
70
100
  node examples/train-basic-policy.mjs --generations=4 --candidates=5 --episodes=1 --steps=240
@@ -72,7 +102,9 @@ node examples/train-basic-policy.mjs --generations=4 --candidates=5 --episodes=1
72
102
 
73
103
  The starter script imports the public `@inventure71/paddockjs/environment` subpath and uses self-contained example data from `examples/trainingData.mjs`, so it does not depend on private package source modules for demo drivers. `createProgressReward()` remains available as example/demo reward code only; it is not the official reward and not part of the environment objective.
74
104
 
75
- Each ray reports track-transition distance and car distance. A track hit uses `kind: 'exit'` when the ray leaves the road and `kind: 'entry'` when an off-track ray points back to the road. `observation.object.self.onTrack` follows the simulator's wheel-level legality rules, so track, kerb, and legal pit-lane/box surfaces are on-track for reward and observation purposes. `pitIntent: 0` is always accepted as the no-op clear value, including environments where pit stops are disabled.
105
+ Each default ray reports track-transition distance and car distance. A track hit uses `kind: 'exit'` when the ray leaves the road and `kind: 'entry'` when an off-track ray points back to the road. Richer sensor layouts are opt-in: rays can use per-ray lengths, predefined layouts such as `driver-front-heavy`, channels for `roadEdge`, `kerb`, `illegalSurface`, and `car`, and `precision: 'driver' | 'debug'`. Driver precision is the default model-facing contract; debug precision is only for clearly labeled diagnostics. Surface channels are computed only when requested. Barrier walls are rendered and enforced as hard destruction boundaries in both physics modes, but they are not model-facing ray targets. `observation.object.self.onTrack` follows the simulator's wheel-level legality rules, so track, kerb, and legal pit-lane/box surfaces are on-track for reward and observation purposes. `pitIntent: 0` is always accepted as the no-op clear value, including environments where pit stops are disabled.
106
+
107
+ For realistic local-perception policies, use `physicsMode: 'simulator'` with `observation.profile: 'physical-driver'`. It exposes yaw rate, local boundary distances, contact-patch surface readings, richer opponent radar, and surface-aware ray fields in the versioned vector schema. The profile defaults track lookahead to `[]` so the policy does not receive privileged future curvature unless the host explicitly opts back in.
76
108
 
77
109
  External training code can inspect the environment contract without guessing field ranges:
78
110
 
@@ -81,6 +113,31 @@ const actionSpec = env.getActionSpec();
81
113
  const observationSpec = env.getObservationSpec();
82
114
  ```
83
115
 
116
+ Controller modules own model loading, inference, memory, rewards, and logs. PaddockJS supplies cached specs, stable controlled-driver ordering, compact observations, action validation, and repeated stepping through normal physics controls:
117
+
118
+ ```js
119
+ import { createPaddockDriverControllerLoop } from '@inventure71/paddockjs';
120
+
121
+ const controller = {
122
+ async decideBatch(ctx) {
123
+ return Object.fromEntries(ctx.orderedObservations.map(({ driverId, vector }) => [
124
+ driverId,
125
+ userModelAction(vector),
126
+ ]));
127
+ },
128
+ };
129
+
130
+ const loop = createPaddockDriverControllerLoop({
131
+ runtime: env,
132
+ controller,
133
+ actionRepeat: 4,
134
+ });
135
+ await loop.reset();
136
+ await loop.step();
137
+ ```
138
+
139
+ `loop.start()` may begin from a fresh runtime and will lazily reset before the first scheduled step. In `onStep(ctx)`, `ctx.actions` is the current applied action map and `ctx.previousActions` is the action map applied on the prior physics step. Scheduled playback stops on controller/runtime errors and exposes the value through `loop.stats.lastError`.
140
+
84
141
  The environment also exposes reset-only scenario placement, neutral rollout recording, deterministic evaluation metrics, and a JSON-serializable worker protocol for external bridges:
85
142
 
86
143
  ```js
@@ -97,7 +154,66 @@ const env = createPaddockEnvironment({
97
154
  });
98
155
  ```
99
156
 
100
- Scenario placement is an environment reset feature, not a policy assist. During `step(actions)`, controlled cars still move only through normalized steering, throttle, brake, and pit intent.
157
+ Scenario placement is an environment reset feature, not a policy assist. During `step(actions)`, controlled cars still move only through normalized steering, throttle, brake, and pit intent. `steering`, `throttle`, and `brake` are required for each controlled-driver action; missing or non-finite values fail validation instead of becoming silent zero controls.
158
+
159
+ For same-environment batched learning, controlled cars can use compact vector observations and reset independently at episode boundaries:
160
+
161
+ ```js
162
+ const env = createPaddockEnvironment({
163
+ drivers,
164
+ entries,
165
+ controlledDrivers: agentIds,
166
+ physicsMode: 'simulator',
167
+ participantInteractions: { defaultProfile: 'batch-training' },
168
+ observation: {
169
+ profile: 'physical-driver',
170
+ output: 'vector',
171
+ includeSchema: false,
172
+ vectorType: 'float32',
173
+ },
174
+ result: {
175
+ stateOutput: 'none',
176
+ resetDriversObservationScope: 'reset',
177
+ },
178
+ });
179
+
180
+ const schema = env.getObservationSpec();
181
+ let result = env.reset();
182
+ result = env.step(actionsByDriver);
183
+
184
+ env.resetDrivers({
185
+ [agentIds[0]]: { distanceMeters: 1200, offsetMeters: 3, speedKph: 80 },
186
+ }, {
187
+ stateOutput: 'none',
188
+ observationScope: 'reset',
189
+ });
190
+ ```
191
+
192
+ `batch-training` cars remain real rendered cars in `snapshot.cars`, but they are non-colliding, sensor-hidden, pit-non-blocking, and excluded from race order by default. Step results include `info.drivers[driverId]` episode state and neutral `metrics[driverId]` facts for external logging or user-defined rewards. `stateOutput: 'none'` suppresses repeated `state.snapshot` payloads for high-throughput loops; use `minimal` when the loop still needs the observation snapshot, or omit the option for the full backward-compatible public snapshot. Deterministic evaluation helpers can accept compact no-state base options and internally request the minimal snapshot needed for evaluation metrics. Reset placements are classified against the same runoff/barrier rules before observations are returned: recovery starts stay physical, while cars placed inside terminal barrier space return destroyed metrics and stable miss-valued rays instead of running far-out ray geometry.
193
+
194
+ On the package's local 20-car simulator benchmark with front-heavy physical-driver rays, compact vector/no-state output measured around `3.3ms` per environment action, with a no-ray baseline around `1.5ms`. Those numbers are hardware dependent, but they show the intended usage: keep schema/spec lookup separate, request compact vectors in training loops, and reserve full snapshots for debugging or visualization.
195
+
196
+ For multi-car training and visual comparison, PaddockJS separates real participants from replay overlays:
197
+
198
+ - `participantInteractions` changes how physics-driven cars interact with collisions, sensors, pit occupancy, and race order. Those cars remain in `snapshot.cars` and still move through steering, throttle, brake, pit intent, tire state, timing, and rules.
199
+ - `replayGhosts` are trajectory-driven overlays for reference laps, debugging, or comparison. They appear in `snapshot.replayGhosts`, never in `snapshot.cars`, and do not collide, rank, pit, or trigger penalties. They are sensor-hidden by default and only appear in rays or nearby observations when their own sensor flags opt in.
200
+ - In the browser view, non-colliding participants remain solid clickable cars but get a blue no-collision outline marker. Replay ghosts remain translucent overlays.
201
+
202
+ ```js
203
+ const env = createPaddockEnvironment({
204
+ drivers,
205
+ entries,
206
+ controlledDrivers: ['model-a', 'model-b'],
207
+ participantInteractions: {
208
+ drivers: {
209
+ 'model-a': { profile: 'isolated-training' },
210
+ 'model-b': { profile: 'isolated-training' },
211
+ },
212
+ },
213
+ });
214
+ ```
215
+
216
+ `isolated-training` is the no-collision profile for real cars that should still remain in race order. `batch-training` is the preferred no-collision profile for same-environment learner batches because it is also excluded from race order. Both profiles hide the car from other cars' ray sensors and `nearbyCars` observations by default; use `phantom-race` or explicit `detectableByRays` / `detectableAsNearby` overrides only when sensor visibility is intentional.
101
217
 
102
218
  Browser expert mode is opt-in through the normal mount API. When enabled, the visual simulator advances only when host code calls `simulator.expert.step(actions)`. Expert mode is a mount-time boundary; changing `expert` through `restart(nextOptions)` is rejected so ticker ownership cannot silently change under a mounted simulator.
103
219
  Set `expert.visualizeSensors` to draw expert sensor rays inside the actual race canvas for visual debugging:
@@ -117,8 +233,14 @@ const simulator = await mountF1Simulator(root, {
117
233
  });
118
234
  ```
119
235
 
236
+ The overlay renders ray values from the same active observation returned by `simulator.expert.step(actions)`; it does not recompute a separate sensor model for the browser layer. Each detected ray channel is shown as its own colored marker, so road-edge, kerb, illegal surface, and car hits can be inspected independently. This applies to every model-facing sense: Policy Runner and expert visualizations must show what the policy receives, while extra high-precision diagnostics must be labeled separately.
237
+
238
+ When multiple drivers are controlled, sensor visualization renders the selected controlled driver by default so batch-training previews do not draw every agent's rays every frame. Use `visualizeSensors: { rays: true, drivers: 'all' }` only when you intentionally want the heavier all-controlled-car overlay.
239
+
120
240
  ## API
121
241
 
242
+ The root package exports the browser mounts, composable helper mounts, data/rating helpers, bundled asset and preset constants, procedural-track helper, simulator unit converters, and `createPaddockDriverControllerLoop`. The browser-free training runtime lives under `@inventure71/paddockjs/environment`; see [docs/system_specs.md](docs/system_specs.md) for the full export catalog.
243
+
122
244
  All-in-one mount:
123
245
 
124
246
  ```js
@@ -204,6 +326,23 @@ simulator.mountRaceTelemetryDrawer(document.getElementById('sim-race-workbench')
204
326
  });
205
327
  ```
206
328
 
329
+ When a composable host intends to use pit APIs or automatic pit behavior, include the pit rules in the simulator options before mounting components:
330
+
331
+ ```js
332
+ const simulator = createPaddockSimulator({
333
+ drivers,
334
+ entries,
335
+ rules: {
336
+ ruleset: 'custom',
337
+ modules: {
338
+ pitStops: { enabled: true },
339
+ stalledDnf: { enabled: true },
340
+ tireStrategy: { enabled: true, mandatoryDistinctDryCompounds: 2 },
341
+ },
342
+ },
343
+ });
344
+ ```
345
+
207
346
  `drivers` is the host-owned project/pilot list. `entries` is the optional driver/car/team pairing sheet. The car/driver overview uses the existing driver and vehicle rating components from each entry. Driver and vehicle entries can also include `customFields` as extra label/value metadata. Entries can include `team: { id, name, color, icon, pitCrew }`; the timing tower uses the team icon and defaults team color to the car color when omitted. `team.pitCrew` accepts `speed`, `consistency`, and `reliability` values from `0` to `1` for optional pit-service variability. Assets, including the default car image and generic driver helmet, are bundled by default, so the host website does not need to provide simulator images or textures.
208
347
 
209
348
  The returned object supports:
@@ -220,6 +359,7 @@ The returned object supports:
220
359
  - `setPitIntent(driverId, intent, targetCompound?)`
221
360
  - `getPitIntent(driverId)`
222
361
  - `getPitTargetCompound(driverId)`
362
+ - `getSimulationSpeed()`
223
363
  - `servePenalty(penaltyId)`
224
364
  - `cancelPenalty(penaltyId)`
225
365
  - `getSnapshot()`
@@ -247,14 +387,63 @@ ui: {
247
387
  raceDataBannerSize: 'auto',
248
388
  raceDataTelemetryDetail: true,
249
389
  timingTowerVerticalFit: 'expand-race-view',
390
+ },
391
+ debug: {
392
+ physicsModeIndicator: false,
250
393
  }
251
394
  ```
252
395
 
253
- `preset` is resolved before explicit host options. Available presets are `dashboard`, `timing-overlay`, `compact-race`, and `full-dashboard`; hosts can start from a preset and override any `ui` or `theme` field. `theme` maps to package CSS variables for the stable sizing/color contract: `accentColor`, `greenColor`, `yellowColor`, `timingTowerMaxWidth`, and `raceViewMinHeight`.
396
+ `preset` is resolved before explicit host options. Available presets are `dashboard`, `timing-overlay`, `compact-race`, and `full-dashboard`; hosts can start from a preset and override any `ui`, `debug`, or `theme` field. `debug.physicsModeIndicator: true` renders a small top-left race-canvas square: blue for arcade physics and red for simulator physics. It defaults to `false` for package consumers and is intended only for debug/development use. `theme` maps to package CSS variables for the stable sizing/color contract: `accentColor`, `greenColor`, `yellowColor`, `timingTowerMaxWidth`, and `raceViewMinHeight`.
397
+
398
+ If `trackSeed` is omitted, each mounted browser simulator creates a fresh procedural circuit. Passing `trackSeed` makes the track deterministic so multiple embeds can share the same generated circuit; repeated procedural seeds are cached within the page runtime as immutable track definitions. Treat values returned by `createProceduralTrack()` as read-only and pass custom mutable copies when experimenting with track-definition edits. `restart({ trackSeed })` rebuilds the race on the deterministic circuit for the new seed. Asset URL changes are not restartable; destroy and mount a new simulator when changing assets.
399
+
400
+ Warmup is enabled by default across browser, headless environment, and direct simulation creation. The runtime warms a disposable instance during loading and caches by configuration fingerprint, so identical resets/restarts skip repeated warmup while seed/config changes warm again automatically. Override with `warmup: { enabled, policy: 'config-change' | 'always' | 'never', steps }` or `warmup: false`.
401
+
402
+ Generated circuits are built from seeded connected region boundaries that are smoothed and warped into a validated centerline, so tracks can include concave infield/outfield sections and chicane-like bends instead of simple oval-like fallback shapes. Hosts can pass `trackGeneration` alongside `trackSeed` to choose a profile and override semantic generation controls:
403
+
404
+ ```js
405
+ const simulator = await mountF1Simulator(root, {
406
+ drivers,
407
+ entries,
408
+ trackSeed: 4101,
409
+ trackGeneration: {
410
+ profile: 'training-short',
411
+ length: { minMeters: 900, maxMeters: 1800 },
412
+ startStraight: { gridMeters: 0 },
413
+ pitLane: { enabled: false },
414
+ },
415
+ rules: {
416
+ modules: {
417
+ pitStops: { enabled: false },
418
+ },
419
+ },
420
+ });
421
+
422
+ simulator.restart({
423
+ trackSeed: 5051,
424
+ trackGeneration: { profile: 'race' },
425
+ });
426
+ ```
427
+
428
+ Advanced callers can import `createProceduralTrack(seed, options)` when they need the generated track definition directly instead of mounting a simulator:
254
429
 
255
- If `trackSeed` is omitted, each mounted browser simulator creates a fresh procedural circuit. Passing `trackSeed` makes the track deterministic so multiple embeds can share the same generated circuit; repeated procedural seeds are cached within the page runtime. `restart({ trackSeed })` rebuilds the race on the deterministic circuit for the new seed. Asset URL changes are not restartable; destroy and mount a new simulator when changing assets.
430
+ ```js
431
+ import { createProceduralTrack } from '@inventure71/paddockjs';
432
+
433
+ const trainingTrack = createProceduralTrack(4101, {
434
+ profile: 'training-short',
435
+ length: { minMeters: 900, maxMeters: 1800 },
436
+ startStraight: { gridMeters: 0, exitMeters: 80, blendMeters: 80 },
437
+ pitLane: { enabled: false },
438
+ shape: { scale: 0.2, cornerDensity: 1.3, variation: 0.22 },
439
+ validation: { minClearanceMultiplier: 1, maxLocalTurnRadians: 1.85 },
440
+ attempts: { primary: 80, fallback: 200 },
441
+ });
442
+ ```
443
+
444
+ Profiles are presets, not separate generators. `race` preserves the default full circuit with pit lane; `training-short`, `training-medium`, and `training-technical` generate smaller pitless circuits for training or demos. Resolution is `race` defaults, then the selected profile, then explicit overrides.
256
445
 
257
- Every generated track includes:
446
+ Pit-lane geometry depends on the resolved generation options. The `race` profile includes:
258
447
 
259
448
  - rendered pit lane beside the start/finish straight
260
449
  - lane-aligned procedural entry and exit roads
@@ -263,10 +452,14 @@ Every generated track includes:
263
452
  - 10 shared team service areas
264
453
  - 20 unused garage boxes arranged as 10 team pairs
265
454
 
266
- Pit-lane asphalt, working-lane service areas, and garage boxes are legal drivable surfaces for sensors, runoff handling, and track-limit stewarding. Tire energy degrades down to 1% and affects grip nonlinearly, so badly worn tires are slower and harder to rotate without making the car instantly undrivable.
455
+ Training profiles disable pit-lane generation by default. If a host explicitly sets `pitLane: { enabled: false }`, pit-related rules should also be disabled or left in a no-op configuration.
456
+
457
+ Pit-lane asphalt, working-lane service areas, and garage boxes are legal drivable surfaces for sensors, runoff handling, track-limit stewarding, and stalled-DNF timing. In both physics modes, the rendered barrier wall marks the hard outer runoff boundary; cars whose footprint reaches the wall's inner face are marked `destroyed`, stopped, removed from active collision/sensor participation, and treated as DNF entries at the bottom of the timing order. `physicsMode` changes vehicle integration and grip behavior, not barrier consequences. Environment results expose destruction as neutral `metrics[driverId].destroyed` and a per-driver `endReason: 'destroyed'`, so training loops can assign their own negative reward and then call `resetDrivers()` for a new episode. Opt-in stalled off-track DNF uses `endReason: 'stalled-off-track'` without setting `destroyed`. Tire energy degrades down to 1% and affects grip nonlinearly, so badly worn tires are slower and harder to rotate without making the car instantly undrivable.
267
458
 
268
459
  When `rules.modules.pitStops.enabled` is true, cars automatically form bounded pit trains when lane space is available. They brake to the limiter by the main pit-lane start, drive along the main fast lane, pass through the team queue spot as a rolling gate, roll into the team-colored working-lane service area when it is clear, stop, serve eligible penalties before tire work, show the remaining stationary service time above the car, change to the requested configured tire compound or the default alternate compound, and exit back to the race track.
269
460
 
461
+ `tireStrategy` does not enable automatic pit stops. It controls available compounds, pit-stop target compound choices, and tire-requirement stewarding. Use `rules.modules.pitStops.enabled: true` for automatic pit routing and pit APIs, or choose a preset such as `ruleset: 'fia2025'` / `ruleset: 'grandPrix2025'` that enables both pit stops and the two-compound tire strategy by default.
462
+
270
463
  Team-mates share one service area. Every car enters through the queue spot first, but it only stops there when the active service spot is blocked. A second team car waits in the queue spot until the active service spot is physically clear, including the previous car's first movement out of the box, without blocking the main fast lane.
271
464
 
272
465
  By default, built-in AI cars request an opportunistic `pitIntent: 1` below 50% tire energy and commit `pitIntent: 2` below 30%. Hosts can change those thresholds with `tirePitRequestThresholdPercent` and `tirePitCommitThresholdPercent`. Expert/headless controlled drivers do not receive those automatic tire-threshold calls and must request service with `pitIntent`.
@@ -301,6 +494,18 @@ const simulator = await mountF1Simulator(root, {
301
494
  perfect: false,
302
495
  },
303
496
  },
497
+ tireStrategy: {
498
+ enabled: true,
499
+ mandatoryDistinctDryCompounds: 2,
500
+ },
501
+ tireDegradation: {
502
+ enabled: true,
503
+ },
504
+ stalledDnf: {
505
+ enabled: true,
506
+ maxStoppedSeconds: 12,
507
+ speedThresholdKph: 5,
508
+ },
304
509
  penalties: {
305
510
  trackLimits: { strictness: 0.8 },
306
511
  collision: { strictness: 0.5, consequences: [{ type: 'time', seconds: 5 }] },
@@ -316,7 +521,9 @@ const simulator = await mountF1Simulator(root, {
316
521
  });
317
522
  ```
318
523
 
319
- Supported rulesets are `paddock`, `grandPrix2025`, `fia2025`, and `custom`. Presets only choose defaults; explicit module config wins. Penalty subsections use `strictness` from `0` to `1` instead of plain booleans. Track limits use the white line as the legal edge and require all four wheel contact patches to be fully outside the same side of the line before recording a violation, so normal kerb riding is not punished. Per-car `surface` is resolved from the worst wheel surface, snapshots include `car.wheels` for per-wheel surface and white-line state, and asymmetric left/right wheel resistance adds a small capped yaw tug toward the slower side when only one side is on a worse surface. Collision stewarding is driven by a body collision hull, not transparent sprite bounds or wheel-only overlap, and contact events include shape ids, contact type, depth, and time of impact. It considers impact severity, closing speed, and whether one car clearly hit another from behind; clear rear contact penalizes only the physically trailing car, including lapped traffic cases, while unclear meaningful contact records shared-fault penalties for both cars. Pit-lane speeding is enforced on the main fast lane, working lane, service areas, and garage boxes, but not on pit-entry or pit-exit connector roads. Track-limit warnings are emitted as `track-limits` events, while penalty decisions are exposed through `snapshot.penalties` plus `penalty` events. Penalty consequences support warning, time, drive-through, stop-go, position-drop, grid-drop, and disqualification payloads. Time consequences are additive, drive-through and stop-go penalties are service obligations, and unserved service penalties convert to configured time at final classification.
524
+ For deterministic single-skill training or visual checkpoint comparison, set `rules.modules.tireDegradation.enabled: false` so tyre energy remains fixed while the car still drives through normal steering, throttle, brake, and surface physics. `rules.modules.stalledDnf` defaults to `enabled: false` so base simulator and training environments keep stuck off-track cars live unless a host opts in. Set `enabled: true` to retire cars that are off legal racing or pit surfaces while below `speedThresholdKph` for `maxStoppedSeconds`.
525
+
526
+ Supported rulesets are `paddock`, `grandPrix2025`, `fia2025`, and `custom`. Presets only choose defaults; explicit module config wins. Weather, reliability, and fuel-load performance effects are reserved future modules and are not active `2.0.x` behavior. Penalty subsections use `strictness` from `0` to `1` instead of plain booleans. Track limits use the white line as the legal edge and require all four wheel contact patches to be fully outside the same side of the line before recording a violation, so normal kerb riding is not punished. Per-car `surface` is resolved from the worst wheel surface, snapshots include `car.wheels` for per-wheel surface and white-line state, and asymmetric left/right wheel resistance adds a small capped yaw tug toward the slower side when only one side is on a worse surface. Collision stewarding is driven by a body collision hull, not transparent sprite bounds or wheel-only overlap, and contact events include shape ids, contact type, depth, and time of impact. It considers impact severity, closing speed, and whether one car clearly hit another from behind; clear rear contact penalizes only the physically trailing car, including lapped traffic cases, while unclear meaningful contact records shared-fault penalties for both cars. Stalled off-track DNFs emit `car-dnf` with `reason: 'stalled-off-track'`, freeze the car, remove it from active control/collision/sensor/pit participation, and classify it with the existing DNF metadata. Pit-lane speeding is enforced on the main fast lane, working lane, service areas, and garage boxes, but not on pit-entry or pit-exit connector roads. Track-limit warnings are emitted as `track-limits` events, while penalty decisions are exposed through `snapshot.penalties` plus `penalty` events. Penalty consequences support warning, time, drive-through, stop-go, position-drop, grid-drop, and disqualification payloads. Time consequences are additive, drive-through and stop-go penalties are service obligations, and unserved service penalties convert to configured time at final classification.
320
527
 
321
528
  `initialCameraMode` accepts `'overview'`, `'leader'`, `'selected'`, `'show-all'`, or `'pit'`; invalid values fall back to `'leader'`. The overview camera frames the active generated track bounds, including package-owned track padding and pit-lane extent, instead of using a fixed world-center zoom. Camera mode changes ease from the current camera target to the next target after the initial frame, so switching between leader, selected, overview, show-all, and pit views does not snap the world view.
322
529
 
@@ -371,7 +578,7 @@ Mounted package surfaces include a lightweight red start-light loading overlay.
371
578
 
372
579
  The runtime also pauses its render ticker when the race canvas is offscreen or the browser tab is hidden. This keeps pages with multiple PaddockJS embeds responsive without requiring host code to manually start and stop each simulator. At `5x` and `10x` browser playback, noncritical timing/readout DOM refreshes run at a lower cadence while fixed-step simulation and race events keep using authoritative race state.
373
580
 
374
- Lifecycle callbacks are optional and host-owned. PaddockJS emits `onLoadingChange`, `onReady`, `onError`, `onDriverSelect`, `onRaceEvent`, `onLapChange`, and `onRaceFinish`; callback errors are routed to `onError` when provided and do not stop the simulator loop. Race snapshots include per-car interval timing, leader-gap timing, whole-lap gap counts, calibrated `speedKph`, automatic `track.sectors`, hidden `track.timingLines`, per-car `lapTelemetry`, finish state, `raceControl.winner` after the first finisher, and final `raceControl.classification` only after the whole field finishes. Sector telemetry clears future-sector values so banners and sidebars show completed splits before the active sector plus the active live timer, not stale later-sector entries. Cars that have crossed the line before full race completion expose `raceStatus: 'waved-flag'` / `wavedFlag: true` and stay frozen in provisional finish order. Final classification applies time penalties and unserved service conversions through `adjustedFinishTime`, then applies position-drop and disqualification consequences. The field then circulates in safety-car mode and the race canvas shows a package-owned winner banner.
581
+ Lifecycle callbacks are optional and host-owned. PaddockJS emits `onLoadingChange`, `onReady`, `onError`, `onDriverSelect`, `onRaceEvent`, `onLapChange`, and `onRaceFinish`; lifecycle callback errors are routed to `onError` when provided and do not stop the simulator loop. `onDriverOpen(driver)` is host-owned navigation code and is not wrapped as a lifecycle callback. Race snapshots include per-car interval timing, leader-gap timing, whole-lap gap counts, calibrated `speedKph`, automatic `track.sectors`, hidden `track.timingLines`, per-car `lapTelemetry`, finish state, `raceControl.winner` after the first finisher, and final `raceControl.classification` only after the whole field is finished or DNF. Sector telemetry clears future-sector values so banners and sidebars show completed splits before the active sector plus the active live timer, not stale later-sector entries. Cars that have crossed the line before full race completion expose `raceStatus: 'waved-flag'` / `wavedFlag: true` and stay frozen in provisional finish order; later barrier contact or stalled-off-track detection does not turn that result into DNF. Destroyed/out-of-race cars expose DNF metadata, show `DNF` at the bottom of the timing tower, render faded/gray in the race canvas, and appear after finishers in final classification with no finish time. If a DNF car is restored before final classification, it re-enters live timing and the race waits for it again; once `raceControl.finished` is true, classification stays final. Final classification applies time penalties and unserved service conversions through `adjustedFinishTime`, then applies position-drop and disqualification consequences to finishers before DNF entries. The field then circulates in safety-car mode and the race canvas shows a package-owned winner banner.
375
582
 
376
583
  ## License
377
584