@inventure71/paddockjs 0.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/INSTALL_AND_UPDATE.md +5 -2
  3. package/README.md +328 -13
  4. package/docs/architecture.md +150 -41
  5. package/docs/concepts.md +22 -3
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +474 -34
  8. package/docs/data_model.md +1 -1
  9. package/docs/index.md +1 -0
  10. package/docs/learnings.md +23 -1
  11. package/docs/rules.md +169 -21
  12. package/docs/system_specs.md +56 -20
  13. package/docs/training.md +369 -25
  14. package/examples/python/README.md +62 -0
  15. package/examples/python/base_policy_server.py +191 -0
  16. package/examples/python/requirements.txt +2 -0
  17. package/examples/train-basic-policy.mjs +5 -6
  18. package/examples/trainingData.mjs +133 -0
  19. package/package.json +15 -4
  20. package/src/api/PaddockSimulatorController.js +36 -2
  21. package/src/app/BrowserExpertAdapter.js +367 -9
  22. package/src/app/F1SimulatorApp.js +615 -940
  23. package/src/app/banners/bannerFormatters.js +55 -0
  24. package/src/app/banners/raceDataBannerController.js +141 -0
  25. package/src/app/banners/stewardMessageController.js +99 -0
  26. package/src/app/camera/cameraConstants.js +21 -0
  27. package/src/app/camera/cameraController.js +265 -0
  28. package/src/app/camera/cameraGeometry.js +176 -0
  29. package/src/app/domBindings.js +14 -1
  30. package/src/app/readouts/carOverviewRenderer.js +109 -0
  31. package/src/app/readouts/controlStateRenderer.js +37 -0
  32. package/src/app/readouts/raceStatusRenderer.js +133 -0
  33. package/src/app/readouts/readoutFormatters.js +58 -0
  34. package/src/app/readouts/telemetryRenderer.js +113 -0
  35. package/src/app/readouts/timingTowerRenderer.js +177 -0
  36. package/src/app/rendering/appAssets.js +38 -0
  37. package/src/app/rendering/carRenderer.js +286 -0
  38. package/src/app/rendering/displayUtils.js +22 -0
  39. package/src/app/rendering/drsTrailRenderer.js +90 -0
  40. package/src/app/rendering/expertSensorRenderer.js +126 -0
  41. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  42. package/src/app/rendering/replayGhostRenderer.js +124 -0
  43. package/src/app/rendering/trackRenderer.js +25 -0
  44. package/src/app/runtime/frameLoop.js +66 -0
  45. package/src/app/runtime/runtimeTiming.js +34 -0
  46. package/src/app/runtime/runtimeVisibility.js +45 -0
  47. package/src/config/defaultOptions.js +24 -4
  48. package/src/environment/actions.js +38 -7
  49. package/src/environment/controllerLoop.js +246 -0
  50. package/src/environment/episode.js +86 -2
  51. package/src/environment/evaluation.js +206 -0
  52. package/src/environment/index.d.ts +638 -6
  53. package/src/environment/index.js +14 -0
  54. package/src/environment/metrics.js +159 -0
  55. package/src/environment/observationOptions.js +33 -0
  56. package/src/environment/observations.js +500 -43
  57. package/src/environment/options.js +41 -2
  58. package/src/environment/recorder.js +28 -0
  59. package/src/environment/runtime.js +260 -24
  60. package/src/environment/scenarios.js +249 -0
  61. package/src/environment/sensors/bodySenses.js +23 -0
  62. package/src/environment/sensors/boundarySenses.js +17 -0
  63. package/src/environment/sensors/carRays.js +75 -0
  64. package/src/environment/sensors/contactSenses.js +32 -0
  65. package/src/environment/sensors/index.js +157 -0
  66. package/src/environment/sensors/indexedRayBands.js +103 -0
  67. package/src/environment/sensors/nearbyCars.js +52 -0
  68. package/src/environment/sensors/opponentRadar.js +65 -0
  69. package/src/environment/sensors/rayConfig.js +110 -0
  70. package/src/environment/sensors/rayDefaults.js +6 -0
  71. package/src/environment/sensors/rayGeometry.js +60 -0
  72. package/src/environment/sensors/rayGuards.js +19 -0
  73. package/src/environment/sensors/sensorTargets.js +60 -0
  74. package/src/environment/sensors/surfaceRays.js +296 -0
  75. package/src/environment/sensors/trackRays.js +261 -0
  76. package/src/environment/sensors.js +1 -257
  77. package/src/environment/specs.js +188 -9
  78. package/src/environment/workerProtocol.js +64 -0
  79. package/src/index.d.ts +533 -7
  80. package/src/index.js +26 -0
  81. package/src/rendering/proceduralTrackAsset.js +20 -510
  82. package/src/rendering/renderSnapshot.js +38 -12
  83. package/src/rendering/track/finishLineRenderer.js +34 -0
  84. package/src/rendering/track/gridRenderer.js +47 -0
  85. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  86. package/src/rendering/track/pitLaneRenderer.js +248 -0
  87. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  88. package/src/rendering/track/trackRenderConstants.js +38 -0
  89. package/src/rendering/track/trackRenderGeometry.js +94 -0
  90. package/src/simulation/collisionGeometry.js +214 -0
  91. package/src/simulation/driver/driverControlConstants.js +28 -0
  92. package/src/simulation/driver/driverController.js +47 -0
  93. package/src/simulation/driver/driverInput.js +28 -0
  94. package/src/simulation/driver/driverMath.js +6 -0
  95. package/src/simulation/driver/driverPersonality.js +25 -0
  96. package/src/simulation/driver/edgeRecovery.js +48 -0
  97. package/src/simulation/driver/racingControls.js +399 -0
  98. package/src/simulation/driver/racingLinePlan.js +166 -0
  99. package/src/simulation/driver/recoveryDynamics.js +38 -0
  100. package/src/simulation/driver/rejoinControls.js +213 -0
  101. package/src/simulation/driver/safetyCarControls.js +35 -0
  102. package/src/simulation/driver/trafficScan.js +51 -0
  103. package/src/simulation/driverController.js +4 -253
  104. package/src/simulation/participants/participantInteractions.js +120 -0
  105. package/src/simulation/pit/pitFlow.js +234 -0
  106. package/src/simulation/pit/pitIntent.js +92 -0
  107. package/src/simulation/pit/pitOccupancy.js +47 -0
  108. package/src/simulation/pit/pitPenaltyService.js +71 -0
  109. package/src/simulation/pit/pitQueue.js +55 -0
  110. package/src/simulation/pit/pitRouteMovement.js +160 -0
  111. package/src/simulation/pit/pitRouting.js +319 -0
  112. package/src/simulation/pit/pitService.js +41 -0
  113. package/src/simulation/pit/pitServiceConstants.js +21 -0
  114. package/src/simulation/pit/pitServiceProfile.js +45 -0
  115. package/src/simulation/pit/pitSnapshots.js +61 -0
  116. package/src/simulation/pit/pitState.js +113 -0
  117. package/src/simulation/pit/pitTireService.js +92 -0
  118. package/src/simulation/race/classification.js +104 -0
  119. package/src/simulation/race/gridPenalties.js +37 -0
  120. package/src/simulation/race/raceControlState.js +38 -0
  121. package/src/simulation/race/raceDistance.js +32 -0
  122. package/src/simulation/race/raceFinish.js +65 -0
  123. package/src/simulation/race/raceLifecycle.js +71 -0
  124. package/src/simulation/race/raceOrder.js +113 -0
  125. package/src/simulation/race/raceProgress.js +124 -0
  126. package/src/simulation/race/raceSetup.js +93 -0
  127. package/src/simulation/race/raceStep.js +80 -0
  128. package/src/simulation/race/redFlag.js +15 -0
  129. package/src/simulation/race/retirements.js +44 -0
  130. package/src/simulation/race/safetyCar.js +86 -0
  131. package/src/simulation/race/stalledDnf.js +86 -0
  132. package/src/simulation/raceSimulation.js +386 -1012
  133. package/src/simulation/replay/replayGhosts.js +128 -0
  134. package/src/simulation/rules/collisionSteward.js +69 -0
  135. package/src/simulation/rules/moduleConfig.js +72 -0
  136. package/src/simulation/rules/penaltyConfig.js +99 -0
  137. package/src/simulation/rules/penaltyEvents.js +18 -0
  138. package/src/simulation/rules/penaltyLedger.js +153 -0
  139. package/src/simulation/rules/penaltyStats.js +26 -0
  140. package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
  141. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  142. package/src/simulation/rules/ruleDefaults.js +85 -0
  143. package/src/simulation/rules/rulesReview.js +141 -0
  144. package/src/simulation/rules/rulesetPresets.js +47 -0
  145. package/src/simulation/rules/tireRequirementSteward.js +29 -0
  146. package/src/simulation/rules/trackLimitsSteward.js +85 -0
  147. package/src/simulation/rulesConfig.js +34 -0
  148. package/src/simulation/snapshots/raceSnapshots.js +192 -0
  149. package/src/simulation/timing/drsTiming.js +87 -0
  150. package/src/simulation/timing/gapEstimation.js +38 -0
  151. package/src/simulation/timing/lapTelemetry.js +182 -0
  152. package/src/simulation/timing/raceTiming.js +15 -0
  153. package/src/simulation/timing/sectorPerformance.js +60 -0
  154. package/src/simulation/timing/timingConstants.js +5 -0
  155. package/src/simulation/timing/timingHistory.js +46 -0
  156. package/src/simulation/timing/timingLines.js +79 -0
  157. package/src/simulation/track/centerlineSpline.js +59 -0
  158. package/src/simulation/track/drsZones.js +67 -0
  159. package/src/simulation/track/pitLaneAccess.js +103 -0
  160. package/src/simulation/track/pitLaneLayout.js +395 -0
  161. package/src/simulation/track/pitLaneState.js +160 -0
  162. package/src/simulation/track/proceduralCenterline.js +237 -0
  163. package/src/simulation/track/sampleGeometry.js +49 -0
  164. package/src/simulation/track/spatialQueries.js +154 -0
  165. package/src/simulation/track/startStraight.js +141 -0
  166. package/src/simulation/track/trackConstants.js +109 -0
  167. package/src/simulation/track/trackGenerationOptions.js +283 -0
  168. package/src/simulation/track/trackMath.js +241 -0
  169. package/src/simulation/track/trackModel.js +240 -0
  170. package/src/simulation/track/trackQueryIndex.js +945 -0
  171. package/src/simulation/track/trackSectors.js +19 -0
  172. package/src/simulation/track/trackStatePolicy.js +24 -0
  173. package/src/simulation/track/trackValidation.js +98 -0
  174. package/src/simulation/trackModel.js +1 -628
  175. package/src/simulation/units.js +6 -4
  176. package/src/simulation/vehicle/contactResolution.js +116 -0
  177. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  178. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  179. package/src/simulation/vehicle/runoffResponse.js +88 -0
  180. package/src/simulation/vehicle/surfacePriority.js +28 -0
  181. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  182. package/src/simulation/vehicle/vehiclePhysics.js +363 -0
  183. package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
  184. package/src/simulation/vehicle/vehicleState.js +186 -0
  185. package/src/simulation/vehicle/wheelSurface.js +150 -0
  186. package/src/simulation/vehicleGeometry.js +1 -0
  187. package/src/simulation/vehiclePhysics.js +1 -104
  188. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  189. package/src/simulation/wheelSurface.js +1 -0
  190. package/src/styles.css +342 -42
  191. package/src/ui/bannerTemplates.js +69 -0
  192. package/src/ui/cameraControlsTemplate.js +20 -0
  193. package/src/ui/carOverviewTemplate.js +33 -0
  194. package/src/ui/componentTemplates.js +20 -426
  195. package/src/ui/raceCanvasTemplate.js +69 -0
  196. package/src/ui/raceControlStatusBanner.js +39 -0
  197. package/src/ui/raceControlsTemplate.js +36 -0
  198. package/src/ui/shellTemplate.js +6 -5
  199. package/src/ui/telemetryDrawerTemplate.js +55 -0
  200. package/src/ui/telemetryTemplates.js +136 -0
  201. package/src/ui/templateUtils.js +23 -0
  202. package/src/ui/timingTowerTemplate.js +34 -0
package/docs/training.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  PaddockJS does not train models for you. It provides a simulator environment contract so users can train with any toolchain, then run the resulting policy in the visual simulator.
4
4
 
5
+ If you already have a trained model and need to connect it to PaddockJS, see [Custom Model Controller Guide](custom_model_controller.md).
6
+
5
7
  ## Boundary
6
8
 
7
9
  PaddockJS owns:
@@ -11,6 +13,7 @@ PaddockJS owns:
11
13
  - normalized actions
12
14
  - events
13
15
  - optional reward callback hooks
16
+ - race-rule overrides for the existing simulator rules
14
17
  - browser expert stepping
15
18
  - sensor visualization
16
19
 
@@ -21,55 +24,395 @@ Users own:
21
24
  - model weights
22
25
  - model storage
23
26
  - model loading
24
- - reward design beyond optional starter helpers
27
+ - reward design
28
+
29
+ ## Supported Now
30
+
31
+ The current expert API is a JavaScript environment contract. It supports:
32
+
33
+ - `createPaddockEnvironment()` from `@inventure71/paddockjs/environment`
34
+ - `createPaddockDriverControllerLoop()` for shared browser/headless controller orchestration
35
+ - explicit `controlledDrivers`
36
+ - normalized actions: `steering`, `throttle`, `brake`, optional `pitIntent`, and optional `pitCompound`
37
+ - manual stepping with optional `frameSkip`
38
+ - object observations in real units plus a versioned numeric vector and schema, including rays, nearby cars, track lookahead/curvature, local physical driver senses, pit-lane surface, pit target compound, pit service state, pit-lane open state, and red-flag state
39
+ - `observation.object.self.appliedControls`, which reports the normalized steering, throttle, and brake controls that actually drove the latest physics step
40
+ - compact observation output modes for vector-only or object-only training loops
41
+ - full simulator state under `result.state.snapshot`
42
+ - global and per-controlled-driver events
43
+ - optional host-owned `reward(context)` callbacks, or no reward
44
+ - `getActionSpec()` and `getObservationSpec()`
45
+ - scenarios with `participants: 'all'`, `'controlled-only'`, or an explicit driver-id list
46
+ - reset-only scenario placement through `scenario.preset`, `scenario.placements`, `scenario.traffic`, and episode-boundary `resetDrivers(placements)`
47
+ - multi-car interaction profiles through `participantInteractions`, including no-collision training cars that are hidden from other cars' ray and nearby-car sensors by default
48
+ - per-driver episode state and neutral step metrics for batched training loops
49
+ - neutral rollout recording through `createRolloutRecorder()`
50
+ - deterministic evaluation metrics through `runEnvironmentEvaluation()` and `createEvaluationTracker()`
51
+ - a JSON-serializable worker protocol wrapper through `createEnvironmentWorkerProtocol()`
52
+ - optional frame observers through `externalRenderer` for transport-owned visual streaming (`{ snapshot, observation, meta }`)
53
+ - built-in AI for non-controlled cars
54
+ - browser expert mode through `mountF1Simulator(..., { expert })`
55
+ - opt-in browser ray visualization with `expert.visualizeSensors`
56
+
57
+ The environment also accepts `rules` as a narrow override of the existing race rules, such as `standingStart: false` for training loops. Rule overrides change simulator behavior; keep them fixed when comparing policy runs.
58
+
59
+ `reward(context)` is a user-owned formula over package-owned facts. The context includes the same neutral `metrics` and per-driver `episode` state returned by each step result, so training code does not need to re-derive legality, destruction, or episode termination from raw snapshots:
60
+
61
+ ```js
62
+ const env = createPaddockEnvironment({
63
+ drivers,
64
+ entries,
65
+ controlledDrivers: ['budget'],
66
+ reward({ metrics, episode }) {
67
+ if (metrics.destroyed) return -200;
68
+ if (metrics.offTrack) return -12;
69
+ if (episode.terminated) return 0;
70
+ return metrics.legalProgressDeltaMeters;
71
+ },
72
+ });
73
+ ```
74
+
75
+ PaddockJS does not define an official reward. It exposes neutral simulator facts; users choose the objective.
76
+
77
+ ## Deferred
25
78
 
26
- ## Policy Shape
79
+ These are not part of the current package API:
27
80
 
28
- Use this convention for browser playback:
81
+ - Python Gymnasium wrapper
82
+ - static obstacle scenario modes
83
+ - debug mutation APIs for arbitrary simulator state
84
+ - assisted-control modes that blend model output with built-in AI
85
+ - model loading, model storage, model registries, or trained policies
86
+
87
+ Those features need separate design work because they change scenario ownership, reproducibility, and the safety boundary between user experiments and package internals. The worker protocol is intentionally only a message wrapper around the JavaScript environment; it is not a Gymnasium package and does not choose a Python RL stack.
88
+
89
+ ## Controller Shape
90
+
91
+ Use a user-owned controller for browser playback and JavaScript training-style loops:
29
92
 
30
93
  ```js
31
- const policy = {
32
- predict(observation) {
33
- return {
34
- steering: 0,
35
- throttle: 1,
36
- brake: 0,
37
- };
94
+ import {
95
+ createPaddockDriverControllerLoop,
96
+ } from '@inventure71/paddockjs';
97
+
98
+ const controller = {
99
+ async init(ctx) {
100
+ this.model = await loadUserModel(ctx.observationSpec);
101
+ },
102
+ async reset(ctx) {
103
+ this.hidden = resetHidden(ctx.controlledDrivers.length);
104
+ },
105
+ async decideBatch(ctx) {
106
+ const vectors = ctx.orderedObservations.map((item) => item.vector);
107
+ const outputs = await this.model.predictBatch(vectors, this.hidden);
108
+ return Object.fromEntries(ctx.controlledDrivers.map((driverId, index) => [
109
+ driverId,
110
+ {
111
+ steering: outputs[index].steering,
112
+ throttle: Math.max(outputs[index].accel, 0),
113
+ brake: Math.max(-outputs[index].accel, 0),
114
+ },
115
+ ]));
116
+ },
117
+ onStep(ctx) {
118
+ recordUserOwnedRewardOrStats(ctx.metrics, ctx.actions);
38
119
  },
39
120
  };
121
+
122
+ const loop = createPaddockDriverControllerLoop({
123
+ runtime: env,
124
+ controller,
125
+ actionRepeat: 4,
126
+ });
40
127
  ```
41
128
 
42
- `predict()` receives one controlled driver's observation. It returns normalized controls:
129
+ `decideBatch()` receives all controlled drivers in a stable order and returns one normalized action map:
43
130
 
44
- - `steering`: `-1` full left, `1` full right
131
+ - `steering`: absolute steering target, where `-1` is maximum left, `0` is centered, `1` is maximum right, and values between are percentages of the maximum steering angle
45
132
  - `throttle`: `0` to `1`
46
133
  - `brake`: `0` to `1`
134
+ - `pitIntent`: optional `0`, `1`, or `2`; `0` means no pit request and is accepted as a no-op even when pit stops are disabled, `1` means keep trying until a free-enough pit-entry window appears, and `2` means commit to entering at the next pit-entry window even if pit-lane capacity or gap checks would block mode `1`
135
+ - `pitCompound`: optional tire target such as `'S'`, `'M'`, or `'H'`; it must be one of `rules.modules.tireStrategy.compounds`
136
+
137
+ `steering`, `throttle`, and `brake` are required for every controlled-driver action. Missing or non-finite vehicle-control fields fail validation instead of silently defaulting to zero, so broken model output does not become a hidden coast or brake command.
138
+
139
+ The controller owns model loading, hidden state, reward/stat logging, and any checkpoint format. PaddockJS owns only cached action/observation specs, compact observation delivery, action validation, batching order, optional action repeat, and normal physics stepping. A one-car `policy.predict(observation)` function can still be wrapped inside `decideBatch()`, but package examples should prefer the batched controller shape.
140
+
141
+ In `onStep(ctx)`, `ctx.actions` is the action map used for the current physics step and `ctx.previousActions` is the last action map actually applied before it. The first frame after reset has no previous per-driver action. Scheduled `loop.start()` playback may begin from a fresh loop without an explicit `reset()`, and controller/runtime errors stop playback while leaving the thrown value in `loop.stats.lastError`.
142
+
143
+ Controlled drivers do not receive tire-threshold automatic pit calls from the built-in strategy. A model must request a stop with `pitIntent`, and may choose the tire with `pitCompound`; then the simulator owns the pit entry, queue, service, penalty hold, tire change, and pit exit sequence until `pitStopStatus` returns to `completed`. For deterministic training, keep `rules.modules.pitStops.variability.enabled` false or set `rules.modules.pitStops.variability.perfect: true`; when variability is enabled without `perfect`, team `pitCrew` speed, consistency, and reliability affect service time from the same seeded simulation RNG. For single-car driving skill work where tyre management is out of scope, set `rules.modules.tireDegradation.enabled: false` so `tireEnergy` stays fixed and the model does not learn a hidden degradation schedule.
144
+
145
+ `observation.object.self.onTrack` follows the simulator's wheel-level legality rules: track, kerb, and legal pit-lane/box surfaces are on-track for reward/observation purposes, while gravel, grass, and barrier surfaces are off-track. Invalid `observation.lookaheadMeters` values fall back to `[20, 50, 100, 150]` so fixed observation schemas stay usable.
146
+
147
+ Use `observation.profile: 'physical-driver'` when training a policy that should rely on local driver-like senses instead of privileged future track data. This profile keeps the normal action contract, exposes yaw rate, local left/right boundary distance, four contact-patch surface readings, richer opponent radar fields, and surface-aware ray channels in the vector schema. Unless `lookaheadMeters` is explicitly provided, the physical-driver profile uses no track lookahead samples.
148
+
149
+ `observation.object.self.appliedControls` is diagnostic/reference data for the
150
+ latest physics step. For controlled cars it mirrors the normalized action after
151
+ environment validation. In `actionPolicy: 'report'` mode, stepping with a
152
+ missing or invalid vehicle action releases stale manual controls, lets the
153
+ built-in driver own that step, and exposes the exact normalized controls the
154
+ built-in driver applied. This is intended for audits or clean local imitation
155
+ datasets; it does not blend built-in AI with a model during normal controlled
156
+ stepping.
157
+
158
+ For model training, prefer `physicsMode: 'simulator'` so the policy learns against the same grip, yaw-rate, contact-patch, kerb, and runoff behavior exposed by those senses. Arcade physics can still be useful as a debugging baseline, but it should not be treated as the main training target for realistic driver policies.
159
+
160
+ Surface-aware rays are opt-in because they are more expensive than the default road-edge/car channels. The environment normalizes both the legacy compact ray shape and the newer per-ray layout:
161
+
162
+ ```js
163
+ const env = createPaddockEnvironment({
164
+ drivers,
165
+ controlledDrivers: ['budget'],
166
+ physicsMode: 'simulator',
167
+ observation: { profile: 'physical-driver' },
168
+ sensors: {
169
+ rays: {
170
+ layout: 'driver-front-heavy',
171
+ channels: ['roadEdge', 'kerb', 'illegalSurface', 'car'],
172
+ precision: 'driver',
173
+ rays: [
174
+ { id: 'front', angleDegrees: 0, lengthMeters: 260 },
175
+ { id: 'right', angleDegrees: 90, lengthMeters: 80 },
176
+ ],
177
+ },
178
+ },
179
+ });
180
+ ```
181
+
182
+ Only requested ray channels are computed. The default compact config still computes the existing road-edge and car readings, so existing policy vectors keep their current shape unless the profile or sensor config opts into richer fields. Barrier walls are not a ray channel; they are visible track geometry and a terminal destruction condition in both physics modes. Active ray objects, vectors, schemas, and visualizations must not expose barrier ray fields.
183
+
184
+ `precision: 'driver'` is the default and the recommended model-facing ray contract. It returns the normal sampled driver-sensor distances without extra refinement. `precision: 'debug'` exists only for clearly labeled diagnostics with additional edge refinement. If the Policy Runner or expert visualization is showing model senses, it must render the active observation values exactly and must not replace them with debug-precision readings.
185
+
186
+ Track-position, wheel-surface, pit-lane, and ray fallback queries can be backed by an internal startup-built track query index. Browser/expert mounts, headless environments, and direct race-simulation construction now default to indexed queries and can opt out with `trackQueryIndex: false` for comparison runs. Compact vector-only, schema-free, no-state environment runs enable that index on the environment-owned simulation, and `batch-training` ray runs keep it enabled even when full state output is returned for inspection. Internal benchmarks can explicitly compare indexed and legacy-disabled modes. This does not change the observation contract; it makes repeated `nearestTrackState()` style queries use small spatial candidate sets instead of scanning the whole track when the indexed path is active. Rays also use indexed boundary intersections for nearby off-track recovery cases, so the same driver-facing sensor values stay fast without switching to a debug-only precision contract.
187
+
188
+ `resetDrivers()` is part of episode control, not policy action. After reset placement, the environment immediately reclassifies the selected cars against the same runoff/barrier rules used during stepping before building observations. Recovery starts inside the legal recovery band remain normal physical starts. Placements that are already inside the barrier destruction boundary return a terminal per-driver episode state and miss-valued rays, so a batch loop can assign its own negative reward and reset that driver without paying for far-out ray geometry.
47
189
 
48
190
  ## Headless Training Loop
49
191
 
50
192
  ```js
51
- import { createPaddockEnvironment } from '@inventure71/paddockjs/environment';
193
+ import {
194
+ createPaddockDriverControllerLoop,
195
+ createPaddockEnvironment,
196
+ } from '@inventure71/paddockjs/environment';
52
197
 
53
198
  const env = createPaddockEnvironment({
54
199
  drivers,
55
200
  entries,
56
201
  controlledDrivers: ['budget'],
57
202
  frameSkip: 4,
203
+ rules: {
204
+ standingStart: false,
205
+ },
58
206
  reward: myReward,
59
207
  });
60
208
 
209
+ const loop = createPaddockDriverControllerLoop({
210
+ runtime: env,
211
+ controller,
212
+ actionRepeat: 4,
213
+ });
214
+
215
+ let result = await loop.reset();
216
+ while (!result.done) result = await loop.step();
217
+ ```
218
+
219
+ ## Multi-Car No-Collision Training
220
+
221
+ Use `participantInteractions` when you want several physics-driven cars in the same environment without having them collide or contaminate each other's sensors. For batched same-environment learning, use `batch-training`.
222
+
223
+ ```js
224
+ import { createPaddockEnvironment } from '@inventure71/paddockjs/environment';
225
+
226
+ const env = createPaddockEnvironment({
227
+ drivers,
228
+ entries,
229
+ controlledDrivers: ['model-a', 'model-b'],
230
+ frameSkip: 4,
231
+ scenario: {
232
+ participants: ['model-a', 'model-b'],
233
+ },
234
+ participantInteractions: {
235
+ defaultProfile: 'batch-training',
236
+ },
237
+ observation: {
238
+ profile: 'physical-driver',
239
+ output: 'vector',
240
+ includeSchema: false,
241
+ },
242
+ });
243
+
61
244
  let result = env.reset();
62
- while (!result.done) {
63
- const observation = result.observation.budget;
64
- const action = policy.predict(observation);
65
- result = env.step({ budget: action });
245
+ result = env.step({
246
+ 'model-a': policyA.predict(result.observation['model-a']),
247
+ 'model-b': policyB.predict(result.observation['model-b']),
248
+ });
249
+ ```
250
+
251
+ `batch-training` is still a real car mode, not a replay ghost and not a teleport path:
252
+
253
+ - the car remains in `result.state.snapshot.cars`
254
+ - the car still moves only through steering, throttle, brake, and optional pit intent
255
+ - the car still has tire, pit, timing, lap, and rules state
256
+ - the car is excluded from race order/classification by default
257
+ - the car does not collide with other cars
258
+ - the car does not block pit-lane service occupancy
259
+ - the car is hidden from other cars' ray sensors by default
260
+ - the car is hidden from other cars' `nearbyCars` observations by default
261
+
262
+ In the browser renderer, no-collision participants still look like solid cars, but get a blue non-collision outline marker. Replay ghosts remain visually separate: translucent trajectory overlays from `snapshot.replayGhosts`, not physics cars from `snapshot.cars`.
263
+
264
+ Sensor hiding is per target car. If `model-b` is `batch-training`, then `model-a`'s rays and `nearbyCars` ignore `model-b`. If every training car should be invisible to every other training car, set `participantInteractions.defaultProfile: 'batch-training'`.
265
+
266
+ Replay ghosts are also sensor-hidden by default. If a replay reference should be visible to policy sensors for comparison experiments, opt it in through its own `sensors.detectableByRays` or `sensors.detectableAsNearby` flags. Sensor results mark those targets as replay ghosts; they still do not collide, rank, pit, or receive controls.
267
+
268
+ If you need a non-colliding car that still appears in sensors, use `phantom-race` instead. If you need a one-off exception, override the flag explicitly:
269
+
270
+ ```js
271
+ participantInteractions: {
272
+ drivers: {
273
+ 'model-b': {
274
+ profile: 'batch-training',
275
+ detectableByRays: true,
276
+ detectableAsNearby: true,
277
+ },
278
+ },
66
279
  }
67
280
  ```
68
281
 
282
+ Use that override deliberately. The default no-collision training profile is sensor-hidden because parallel training rollouts usually should not alter each other's observation tensors.
283
+
284
+ Compact vector mode is intended for high-throughput loops. `env.getObservationSpec()` remains the canonical schema source, so external code can request `output: 'vector'` and `includeSchema: false` without serializing object observations and schema data on every step. JavaScript training loops can also request `vectorType: 'float32'` for typed numeric buffers. Keep the default array output for JSON-only bridges unless the bridge explicitly packs typed arrays.
285
+
286
+ Use `result.stateOutput` to avoid returning more state than the loop needs:
287
+
288
+ ```js
289
+ const env = createPaddockEnvironment({
290
+ drivers,
291
+ entries,
292
+ controlledDrivers: agentIds,
293
+ observation: { profile: 'physical-driver', output: 'vector', includeSchema: false },
294
+ result: {
295
+ stateOutput: 'none',
296
+ resetDriversObservationScope: 'reset',
297
+ },
298
+ });
299
+ ```
300
+
301
+ `stateOutput: 'minimal'` returns the lean public observation snapshot. `stateOutput: 'none'` returns `state: null`. The default is still `full` for existing callers. When `stateOutput: 'none'`, `observation.output: 'vector'`, and `includeSchema: false`, the environment uses an internal compact training snapshot before building observations and metrics. That optimization is not exposed as policy state, and reward callbacks still receive the documented reward context rather than a browser/render-only snapshot.
302
+
303
+ On the local 20-car batch-training benchmark used for this package work (`physicsMode: 'simulator'`, `frameSkip: 4`, `physical-driver`, `driver-front-heavy` rays), the measured environment action cost after compact output and indexed track queries was approximately:
304
+
305
+ ```txt
306
+ nearest track states 1.52x faster than legacy-disabled
307
+ pit lane states 2.02x faster than legacy-disabled
308
+ sampled high-curvature off-track rays 14.81x faster than legacy-disabled
309
+ wheel surface near pit connector 1.12x faster than legacy-disabled
310
+ 20-car batch recovery environment 1.03x faster than legacy-disabled
311
+ ```
312
+
313
+ These numbers are hardware- and track-dependent, but the relative result is the useful guidance: compact state/observation output reduces serialization/allocation overhead, and indexed track queries prevent off-track recovery rays from becoming the dominant cost center.
314
+
315
+ ## Scenario Reset Control
316
+
317
+ Scenario placement belongs to environment reset, not to model control. It is useful for deterministic training or evaluation starts such as cornering, recovery, overtaking packs, and pit entry:
318
+
319
+ ```js
320
+ const env = createPaddockEnvironment({
321
+ drivers,
322
+ entries,
323
+ controlledDrivers: ['budget'],
324
+ scenario: {
325
+ preset: 'off-track-recovery',
326
+ placements: {
327
+ budget: {
328
+ distanceMeters: 420,
329
+ offsetMeters: 16,
330
+ speedKph: 65,
331
+ headingErrorRadians: 0.4,
332
+ },
333
+ },
334
+ traffic: [
335
+ {
336
+ driverId: 'alpha',
337
+ relativeTo: 'budget',
338
+ deltaDistanceMeters: 24,
339
+ offsetMeters: -1.5,
340
+ speedKph: 68,
341
+ },
342
+ ],
343
+ },
344
+ });
345
+ ```
346
+
347
+ Supported fixed presets are `cornering`, `off-track-recovery`, `overtaking-pack`, and `pit-entry`. Explicit `placements` override preset placement for the same driver. `traffic` places cars relative to another placed or existing car. After reset, `step(actions)` still advances controlled cars only through the normal action contract.
348
+
349
+ For batched training, individual controlled cars can be reset between episodes without recreating the whole simulation:
350
+
351
+ ```js
352
+ env.resetDrivers({
353
+ 'model-a': {
354
+ distanceMeters: 1200,
355
+ offsetMeters: 3,
356
+ speedKph: 80,
357
+ headingErrorRadians: 0.2,
358
+ },
359
+ }, {
360
+ observationScope: 'reset',
361
+ stateOutput: 'none',
362
+ });
363
+ ```
364
+
365
+ `resetDrivers()` is an episode-boundary API. It increments that driver's `episodeId`, resets its `episodeStep`, clears manual controls and pit intent, and places the car through the same simulator state-reset path used by scenarios. It does not rewind global `info.step`; max-step truncation is evaluated from the selected driver's own `episodeStep`, so one truncated driver can be reset while the rest of the batch keeps its current episode state. It is not available through `step(actions)` and is not a movement shortcut during an episode. Passing `{ observationScope: 'reset', stateOutput: 'none' }` keeps reset responses small for batched runners.
366
+
367
+ The worker protocol mirrors this with a `resetDrivers` message:
368
+
369
+ ```js
370
+ protocol.handle({
371
+ id: 'reset-agent-3',
372
+ type: 'resetDrivers',
373
+ placements: {
374
+ 'agent-3': { distanceMeters: 1200, offsetMeters: 0, speedKph: 80 },
375
+ },
376
+ resultOptions: {
377
+ observationScope: 'reset',
378
+ stateOutput: 'none',
379
+ },
380
+ });
381
+ ```
382
+
383
+ For state polling, worker `getState` messages accept `stateOptions: { output: 'full' | 'minimal' | 'none' }`; `resultOptions.stateOutput` is accepted as a compatibility alias on that message type.
384
+
385
+ ## Rollouts And Evaluation
386
+
387
+ PaddockJS can export neutral transition data without owning a trainer:
388
+
389
+ ```js
390
+ import { createRolloutRecorder } from '@inventure71/paddockjs/environment';
391
+
392
+ const recorder = createRolloutRecorder();
393
+ let result = env.reset();
394
+ const action = { budget: { steering: 0, throttle: 1, brake: 0 } };
395
+ const next = env.step(action);
396
+ recorder.recordStep(result, action, next);
397
+ ```
398
+
399
+ Each transition has `{ observation, action, reward, nextObservation, terminated, truncated, info }`. If the environment has no reward callback, `reward` is still `null`; the recorder does not invent one.
400
+
401
+ Environment step results also include reward-neutral `metrics[driverId]` facts such as progress delta, legal progress delta, kerb use, illegal/off-track state, severe cuts, destruction state, under-30-kph state, spin/backwards/instability state, lap completion, lap time, and contact count. `spinOrBackwards` is true for large heading-error backwards cases and for active simulator instability labels such as `spin-risk`, `understeer`, `oversteer`, and `destroyed`. `contactCount` counts each normalized physical contact event once per involved driver even when the event carries both legacy `carId`/`otherCarId` fields and normalized `driverIds`. These metrics are facts for logging, evaluation, and user-defined reward functions; PaddockJS does not turn them into a built-in objective. If a car touches the rendered barrier wall's inner face in either physics mode, that driver receives `metrics[driverId].destroyed: true` and `info.drivers[driverId].endReason: 'destroyed'`; external training code should assign any super-negative crash reward itself and then call `resetDrivers()` for the next episode. If opt-in `rules.modules.stalledDnf.enabled: true` retires a controlled car for being stuck off legal surfaces, the episode terminates with `endReason: 'stalled-off-track'` while `metrics[driverId].destroyed` stays `false`.
402
+
403
+ Deterministic evaluation helpers report simulator quality metrics such as distance, off-track steps, contacts, recovery success, pass count, and first lap time when available. They use the same wheel-level legal-surface and contact-event semantics as environment metrics. `runEnvironmentEvaluation()` can accept compact base options with `result.stateOutput: 'none'`; it internally requests the minimal snapshot needed for evaluation bookkeeping. Direct `createEvaluationTracker(result)` calls require a result with `state.snapshot`, so use `stateOutput: 'minimal'` or `full` for manual tracker wiring. Evaluation helpers do not update a model or compute rewards.
404
+
405
+ ## Example Rewards
406
+
407
+ `createProgressReward()` is exported for examples and quick smoke tests, but it is non-canonical demo code. It is not the official reward function and should not be treated as the recommended objective for every user. Real training code should pass a domain-specific `reward(context)` or omit rewards entirely when collecting observations or imitation data.
408
+
69
409
  ## Visual Playback Loop
70
410
 
71
411
  ```js
72
- import { mountF1Simulator } from '@inventure71/paddockjs';
412
+ import {
413
+ createPaddockDriverControllerLoop,
414
+ mountF1Simulator,
415
+ } from '@inventure71/paddockjs';
73
416
 
74
417
  const simulator = await mountF1Simulator(root, {
75
418
  drivers,
@@ -82,14 +425,15 @@ const simulator = await mountF1Simulator(root, {
82
425
  },
83
426
  });
84
427
 
85
- function frame() {
86
- const observation = simulator.expert.getObservation().budget;
87
- const action = policy.predict(observation);
88
- simulator.expert.step({ budget: action });
89
- requestAnimationFrame(frame);
90
- }
428
+ const loop = createPaddockDriverControllerLoop({
429
+ runtime: simulator.expert,
430
+ controller,
431
+ actionRepeat: 4,
432
+ mode: 'browser',
433
+ });
91
434
 
92
- frame();
435
+ await loop.reset();
436
+ loop.start();
93
437
  ```
94
438
 
95
439
  ## Specs
@@ -0,0 +1,62 @@
1
+ # Python Base Policy Server Example
2
+
3
+ This folder provides a neutral extension point for browser Policy Runner integrations.
4
+
5
+ It is not a trainer and not a model/checkpoint format owner. It only shows transport contracts:
6
+
7
+ - HTTP inference stubs
8
+ - `POST /policy/reset`
9
+ - `POST /policy/reset-state`
10
+ - `POST /policy/decide-batch`
11
+ - Preview WebSocket stream
12
+ - `ws://127.0.0.1:8787/preview`
13
+ - packet shape: `{ type: "preview:snapshot", snapshot, observation, meta }`
14
+ - Preview latest-frame HTTP endpoint (for polling visualizers)
15
+ - `http://127.0.0.1:8787/preview/frame`
16
+
17
+ ## Run
18
+
19
+ ```bash
20
+ python -m pip install -r examples/python/requirements.txt
21
+ python examples/python/base_policy_server.py
22
+ ```
23
+
24
+ Then in Policy Runner:
25
+
26
+ For browser-owned stepping:
27
+
28
+ 1. Select controller `Policy server`
29
+ 2. Use `http://127.0.0.1:8787`
30
+
31
+ For external render-only preview:
32
+
33
+ 1. Select controller `Live preview stream`
34
+ 2. Use either:
35
+ - `ws://127.0.0.1:8787/preview` (WebSocket push)
36
+ - `http://127.0.0.1:8787/preview/frame` (HTTP polling)
37
+ 3. Connect
38
+
39
+ The base server enables CORS for local browser previews so `policy-runner.html`
40
+ can call it from the Vite preview origin.
41
+
42
+ Direct launch URL example (when local preview runs on `5174`):
43
+
44
+ `http://127.0.0.1:5174/policy-runner.html?controller=live-node-view&liveUrl=ws://127.0.0.1:8787/preview`
45
+
46
+ ## Extend
47
+
48
+ Subclass `BasePolicyServer` and override:
49
+
50
+ - `init_policy(ctx)`
51
+ - `reset_policy(ctx)`
52
+ - `reset_policy_state(driver_ids)`
53
+ - `decide_batch(ctx)`
54
+ - `publish_preview_frame(frame)` (optional)
55
+
56
+ Use `broadcast_preview_frame(snapshot, observation, meta)` to push authoritative frames to the browser renderer.
57
+
58
+ ## Important Boundary
59
+
60
+ - This server does not step the simulator by itself.
61
+ - Your training/runtime bridge (Python orchestration + JS environment) must provide authoritative frames by calling `broadcast_preview_frame(...)` after environment `reset/step/resetDrivers`.
62
+ - In browser `Live preview stream`, the simulator is render-only while attached; local browser stepping controls are intentionally disabled.