@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/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:
@@ -29,69 +31,173 @@ Users own:
29
31
  The current expert API is a JavaScript environment contract. It supports:
30
32
 
31
33
  - `createPaddockEnvironment()` from `@inventure71/paddockjs/environment`
34
+ - `createPaddockDriverControllerLoop()` for shared browser/headless controller orchestration
32
35
  - explicit `controlledDrivers`
33
36
  - normalized actions: `steering`, `throttle`, `brake`, optional `pitIntent`, and optional `pitCompound`
34
37
  - manual stepping with optional `frameSkip`
35
- - object observations in real units plus a versioned numeric vector and schema, including rays, nearby cars, track lookahead/curvature, pit-lane surface, pit target compound, pit service state, pit-lane open state, and red-flag state
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
36
41
  - full simulator state under `result.state.snapshot`
37
42
  - global and per-controlled-driver events
38
43
  - optional host-owned `reward(context)` callbacks, or no reward
39
44
  - `getActionSpec()` and `getObservationSpec()`
40
45
  - scenarios with `participants: 'all'`, `'controlled-only'`, or an explicit driver-id list
41
- - reset-only scenario placement through `scenario.preset`, `scenario.placements`, and `scenario.traffic`
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
42
49
  - neutral rollout recording through `createRolloutRecorder()`
43
50
  - deterministic evaluation metrics through `runEnvironmentEvaluation()` and `createEvaluationTracker()`
44
51
  - a JSON-serializable worker protocol wrapper through `createEnvironmentWorkerProtocol()`
52
+ - optional frame observers through `externalRenderer` for transport-owned visual streaming (`{ snapshot, observation, meta }`)
45
53
  - built-in AI for non-controlled cars
46
54
  - browser expert mode through `mountF1Simulator(..., { expert })`
47
55
  - opt-in browser ray visualization with `expert.visualizeSensors`
48
56
 
49
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.
50
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. Reward callbacks run only for `step(actions)` transition results; reset/read calls are reward-neutral:
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
+
51
77
  ## Deferred
52
78
 
53
79
  These are not part of the current package API:
54
80
 
55
81
  - Python Gymnasium wrapper
56
- - static obstacle/ghost-car scenario modes
82
+ - static obstacle scenario modes
57
83
  - debug mutation APIs for arbitrary simulator state
58
84
  - assisted-control modes that blend model output with built-in AI
59
85
  - model loading, model storage, model registries, or trained policies
60
86
 
61
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.
62
88
 
63
- ## Policy Shape
89
+ ## Controller Shape
64
90
 
65
- Use this convention for browser playback:
91
+ Use a user-owned controller for browser playback and JavaScript training-style loops:
66
92
 
67
93
  ```js
68
- const policy = {
69
- predict(observation) {
70
- return {
71
- steering: 0,
72
- throttle: 1,
73
- brake: 0,
74
- };
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);
75
119
  },
76
120
  };
121
+
122
+ const loop = createPaddockDriverControllerLoop({
123
+ runtime: env,
124
+ controller,
125
+ actionRepeat: 4,
126
+ });
77
127
  ```
78
128
 
79
- `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:
80
130
 
81
- - `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
82
132
  - `throttle`: `0` to `1`
83
133
  - `brake`: `0` to `1`
84
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`
85
135
  - `pitCompound`: optional tire target such as `'S'`, `'M'`, or `'H'`; it must be one of `rules.modules.tireStrategy.compounds`
86
136
 
87
- 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.
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.
88
144
 
89
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.
90
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. Compact vectors include contact-patch surface code, signed offset, and cross-track error so vector-only loops keep the same contact-patch facts as object observations. 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
+ When a full observation includes both `object` and `vector`, Policy Runner model inputs that need ray geometry use the observation object's actual `angleDegrees` and `lengthMeters`. Compact vector schemas intentionally carry ray channel values, not out-of-band ray geometry, so consumers with per-driver sensor overrides must read the active object observation or the exact per-driver spec instead of reconstructing layout from a default preset.
187
+
188
+ The model-facing sense proof boundary is documented in [Model Sense Contract](sense_contract.md). The executable contract test compares object observations against independent simulator-snapshot oracles, decodes vector entries by schema name, and checks compact vector-only output against full output for the same deterministic state. New model-facing sense fields must extend that oracle instead of relying only on snapshot or shape tests.
189
+
190
+ 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 shape. For normal driver interactions, ray `precision: 'driver'` keeps sampled road-edge, kerb, and illegal-surface distances with or without the index, preserving zero-distance origin surface hits and avoiding debug-only precision in policy inputs. The `batch-training` participant profile keeps its indexed recovery-ray path as a separate high-throughput training contract with characterization coverage. Indexed ray-boundary shortcuts remain available to non-driver/debug precision paths.
191
+
192
+ `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. In multi-driver batches, top-level `done` is true once every reported controlled driver is terminal, including mixed states where one driver is terminated and another is max-step truncated; per-driver `info.drivers[driverId]` keeps the distinct reason.
193
+
91
194
  ## Headless Training Loop
92
195
 
93
196
  ```js
94
- import { createPaddockEnvironment } from '@inventure71/paddockjs/environment';
197
+ import {
198
+ createPaddockDriverControllerLoop,
199
+ createPaddockEnvironment,
200
+ } from '@inventure71/paddockjs/environment';
95
201
 
96
202
  const env = createPaddockEnvironment({
97
203
  drivers,
@@ -104,14 +210,114 @@ const env = createPaddockEnvironment({
104
210
  reward: myReward,
105
211
  });
106
212
 
213
+ const loop = createPaddockDriverControllerLoop({
214
+ runtime: env,
215
+ controller,
216
+ actionRepeat: 4,
217
+ });
218
+
219
+ let result = await loop.reset();
220
+ while (!result.done) result = await loop.step();
221
+ ```
222
+
223
+ ## Multi-Car No-Collision Training
224
+
225
+ 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`.
226
+
227
+ ```js
228
+ import { createPaddockEnvironment } from '@inventure71/paddockjs/environment';
229
+
230
+ const env = createPaddockEnvironment({
231
+ drivers,
232
+ entries,
233
+ controlledDrivers: ['model-a', 'model-b'],
234
+ frameSkip: 4,
235
+ scenario: {
236
+ participants: ['model-a', 'model-b'],
237
+ },
238
+ participantInteractions: {
239
+ defaultProfile: 'batch-training',
240
+ },
241
+ observation: {
242
+ profile: 'physical-driver',
243
+ output: 'vector',
244
+ includeSchema: false,
245
+ },
246
+ });
247
+
107
248
  let result = env.reset();
108
- while (!result.done) {
109
- const observation = result.observation.budget;
110
- const action = policy.predict(observation);
111
- result = env.step({ budget: action });
249
+ result = env.step({
250
+ 'model-a': policyA.predict(result.observation['model-a']),
251
+ 'model-b': policyB.predict(result.observation['model-b']),
252
+ });
253
+ ```
254
+
255
+ `batch-training` is still a real car mode, not a replay ghost and not a teleport path:
256
+
257
+ - the car remains in `result.state.snapshot.cars`
258
+ - the car still moves only through steering, throttle, brake, and optional pit intent
259
+ - the car still has tire, pit, timing, lap, and rules state
260
+ - the car is excluded from race order/classification by default
261
+ - the car does not collide with other cars
262
+ - the car does not block pit-lane service occupancy
263
+ - the car is hidden from other cars' ray sensors by default
264
+ - the car is hidden from other cars' `nearbyCars` observations by default
265
+
266
+ 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`.
267
+
268
+ 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'`.
269
+
270
+ 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. Nearby-car radar enrichment keys targets by both entity type and id, so an opt-in ghost with the same id as a real car cannot overwrite the real car's velocity-derived radar facts.
271
+
272
+ 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:
273
+
274
+ ```js
275
+ participantInteractions: {
276
+ drivers: {
277
+ 'model-b': {
278
+ profile: 'batch-training',
279
+ detectableByRays: true,
280
+ detectableAsNearby: true,
281
+ },
282
+ },
112
283
  }
113
284
  ```
114
285
 
286
+ 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.
287
+
288
+ 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. If `sensorsByDriver` changes ray or nearby-car shape for specific drivers, read `observationSpec.perDriver[driverId].vector.schema` for that driver's exact compact schema. 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.
289
+
290
+ Use `result.stateOutput` to avoid returning more state than the loop needs:
291
+
292
+ ```js
293
+ const env = createPaddockEnvironment({
294
+ drivers,
295
+ entries,
296
+ controlledDrivers: agentIds,
297
+ observation: { profile: 'physical-driver', output: 'vector', includeSchema: false },
298
+ result: {
299
+ stateOutput: 'none',
300
+ resetDriversObservationScope: 'reset',
301
+ },
302
+ });
303
+ ```
304
+
305
+ `stateOutput: 'minimal'` returns the lean public observation snapshot. `stateOutput: 'none'` returns `state: null`. The default is still `full` for existing callers. `resetDriversObservationScope: 'reset'` also scopes `info.controlledDrivers` to the drivers present in that reset result's `observation` and `metrics`; use `info.drivers` if you need all configured drivers' episode bookkeeping. 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. Reward callbacks still receive the documented reward context on `step(actions)`, and reset/read calls remain reward-neutral.
306
+
307
+ Simulator-mode velocity senses are authoritative only when the simulator owns a real world velocity. Arcade snapshots intentionally expose `velocityX/Y` as `null`. When simulator-mode cars are terminal or held by race control, grid logic, or pit queue logic, the environment freezes scalar speed and `velocityX/Y` together at zero instead of carrying old motion into observations or recorded transitions.
308
+
309
+ 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:
310
+
311
+ ```txt
312
+ nearest track states 1.52x faster than legacy-disabled
313
+ pit lane states 2.02x faster than legacy-disabled
314
+ sampled high-curvature off-track rays 14.81x faster than legacy-disabled
315
+ wheel surface near pit connector 1.12x faster than legacy-disabled
316
+ 20-car batch recovery environment 1.03x faster than legacy-disabled
317
+ ```
318
+
319
+ 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.
320
+
115
321
  ## Scenario Reset Control
116
322
 
117
323
  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:
@@ -146,6 +352,42 @@ const env = createPaddockEnvironment({
146
352
 
147
353
  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.
148
354
 
355
+ For batched training, individual controlled cars can be reset between episodes without recreating the whole simulation:
356
+
357
+ ```js
358
+ env.resetDrivers({
359
+ 'model-a': {
360
+ distanceMeters: 1200,
361
+ offsetMeters: 3,
362
+ speedKph: 80,
363
+ headingErrorRadians: 0.2,
364
+ },
365
+ }, {
366
+ observationScope: 'reset',
367
+ stateOutput: 'none',
368
+ });
369
+ ```
370
+
371
+ `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. Even with reset-only observation scope, `info.drivers` remains per-driver: a reset driver's terminal placement does not mark non-reset drivers as terminated or truncated. The controller loop keeps its last full observation cache and merges reset-only observations into it before the next decision, so batched controllers still receive an observation for every controlled driver after a partial reset.
372
+
373
+ The worker protocol mirrors this with a `resetDrivers` message:
374
+
375
+ ```js
376
+ protocol.handle({
377
+ id: 'reset-agent-3',
378
+ type: 'resetDrivers',
379
+ placements: {
380
+ 'agent-3': { distanceMeters: 1200, offsetMeters: 0, speedKph: 80 },
381
+ },
382
+ resultOptions: {
383
+ observationScope: 'reset',
384
+ stateOutput: 'none',
385
+ },
386
+ });
387
+ ```
388
+
389
+ For state polling, worker `getState` messages accept `stateOptions: { output: 'full' | 'minimal' | 'none' }`; `resultOptions.stateOutput` is accepted as a compatibility alias on that message type.
390
+
149
391
  ## Rollouts And Evaluation
150
392
 
151
393
  PaddockJS can export neutral transition data without owning a trainer:
@@ -155,23 +397,28 @@ import { createRolloutRecorder } from '@inventure71/paddockjs/environment';
155
397
 
156
398
  const recorder = createRolloutRecorder();
157
399
  let result = env.reset();
158
- const action = { budget: policy.predict(result.observation.budget) };
400
+ const action = { budget: { steering: 0, throttle: 1, brake: 0 } };
159
401
  const next = env.step(action);
160
402
  recorder.recordStep(result, action, next);
161
403
  ```
162
404
 
163
- 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.
405
+ 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. Recorded transitions snapshot arrays, typed vectors, actions, observations, rewards, and info at record time, so later policy-loop mutations do not rewrite previous rollout rows.
164
406
 
165
- 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 do not update a model or compute rewards.
407
+ 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`.
408
+
409
+ 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.
166
410
 
167
411
  ## Example Rewards
168
412
 
169
- `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.
413
+ `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. It derives speed and legal-surface facts from the reward snapshot and metrics when object observations are omitted, so compact vector-only runs do not change the demo reward for the same transition. Real training code should pass a domain-specific `reward(context)` or omit rewards entirely when collecting observations or imitation data.
170
414
 
171
415
  ## Visual Playback Loop
172
416
 
173
417
  ```js
174
- import { mountF1Simulator } from '@inventure71/paddockjs';
418
+ import {
419
+ createPaddockDriverControllerLoop,
420
+ mountF1Simulator,
421
+ } from '@inventure71/paddockjs';
175
422
 
176
423
  const simulator = await mountF1Simulator(root, {
177
424
  drivers,
@@ -184,14 +431,15 @@ const simulator = await mountF1Simulator(root, {
184
431
  },
185
432
  });
186
433
 
187
- function frame() {
188
- const observation = simulator.expert.getObservation().budget;
189
- const action = policy.predict(observation);
190
- simulator.expert.step({ budget: action });
191
- requestAnimationFrame(frame);
192
- }
434
+ const loop = createPaddockDriverControllerLoop({
435
+ runtime: simulator.expert,
436
+ controller,
437
+ actionRepeat: 4,
438
+ mode: 'browser',
439
+ });
193
440
 
194
- frame();
441
+ await loop.reset();
442
+ loop.start();
195
443
  ```
196
444
 
197
445
  ## 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.