@inventure71/paddockjs 2.0.0 → 3.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 (110) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/INSTALL_AND_UPDATE.md +10 -1
  3. package/README.md +92 -17
  4. package/docs/architecture.md +26 -16
  5. package/docs/component_inventory.md +72 -0
  6. package/docs/concepts.md +7 -5
  7. package/docs/custom_model_controller.md +2 -2
  8. package/docs/data_contract.md +121 -33
  9. package/docs/data_model.md +1 -1
  10. package/docs/index.md +10 -0
  11. package/docs/learnings.md +6 -6
  12. package/docs/rules.md +39 -9
  13. package/docs/sense_contract.md +59 -0
  14. package/docs/system_specs.md +76 -20
  15. package/docs/training.md +27 -20
  16. package/package.json +1 -1
  17. package/src/api/PaddockSimulatorController.js +75 -36
  18. package/src/app/BrowserExpertAdapter.js +9 -300
  19. package/src/app/BrowserExpertExternalRenderer.js +303 -0
  20. package/src/app/F1SimulatorApp.js +190 -68
  21. package/src/app/banners/raceDataBannerController.js +71 -36
  22. package/src/app/banners/stewardMessageController.js +5 -4
  23. package/src/app/camera/cameraConstants.js +2 -0
  24. package/src/app/camera/cameraController.js +86 -29
  25. package/src/app/camera/cameraGeometry.js +12 -1
  26. package/src/app/domBindings.js +89 -9
  27. package/src/app/layoutScheduler.js +27 -0
  28. package/src/app/layoutSupport.js +165 -0
  29. package/src/app/raceOverlayClearanceSupport.js +158 -0
  30. package/src/app/readouts/carOverviewRenderer.js +16 -8
  31. package/src/app/readouts/controlStateRenderer.js +1 -1
  32. package/src/app/readouts/raceStatusRenderer.js +30 -21
  33. package/src/app/readouts/readoutFormatters.js +2 -0
  34. package/src/app/readouts/telemetryRenderer.js +17 -8
  35. package/src/app/readouts/timingTowerRenderer.js +62 -10
  36. package/src/app/runtime/runtimeVisibility.js +66 -5
  37. package/src/config/cssValues.js +16 -0
  38. package/src/config/defaultOptions.js +51 -38
  39. package/src/config/restartOptions.js +34 -0
  40. package/src/config/themeOptions.js +825 -0
  41. package/src/config/timingGapMode.js +13 -0
  42. package/src/config/urlOptions.js +25 -0
  43. package/src/data/championship.js +14 -9
  44. package/src/environment/controllerLoop.js +34 -5
  45. package/src/environment/episode.js +14 -2
  46. package/src/environment/index.d.ts +17 -4
  47. package/src/environment/observationVector.js +200 -0
  48. package/src/environment/observations.js +58 -285
  49. package/src/environment/options.js +1 -1
  50. package/src/environment/recorder.js +13 -6
  51. package/src/environment/rewards.js +13 -2
  52. package/src/environment/runtime.js +65 -25
  53. package/src/environment/scenarios.js +22 -3
  54. package/src/environment/sensors/index.js +3 -3
  55. package/src/environment/sensors/opponentRadar.js +13 -3
  56. package/src/environment/sensors/sensorTargets.js +8 -2
  57. package/src/environment/sensors/surfaceRays.js +84 -12
  58. package/src/environment/sensors/trackRays.js +173 -15
  59. package/src/environment/specs.js +78 -40
  60. package/src/index.d.ts +112 -9
  61. package/src/index.js +26 -33
  62. package/src/simulation/driver/driverController.js +1 -1
  63. package/src/simulation/driver/edgeRecovery.js +1 -1
  64. package/src/simulation/driver/racingControls.js +2 -2
  65. package/src/simulation/driver/rejoinControls.js +2 -2
  66. package/src/simulation/pit/pitFlow.js +5 -0
  67. package/src/simulation/pit/pitKinematics.js +24 -0
  68. package/src/simulation/pit/pitQueue.js +5 -0
  69. package/src/simulation/pit/pitRouteMovement.js +7 -0
  70. package/src/simulation/pit/pitRouting.js +2 -4
  71. package/src/simulation/pit/pitService.js +2 -0
  72. package/src/simulation/pit/pitTireService.js +2 -0
  73. package/src/simulation/race/raceLifecycle.js +2 -6
  74. package/src/simulation/race/raceProgress.js +6 -1
  75. package/src/simulation/race/raceSetup.js +2 -6
  76. package/src/simulation/race/raceStep.js +2 -2
  77. package/src/simulation/race/redFlag.js +3 -3
  78. package/src/simulation/race/safetyCar.js +10 -4
  79. package/src/simulation/race/stalledDnf.js +8 -17
  80. package/src/simulation/raceSimulation.js +1 -0
  81. package/src/simulation/rules/rulesReview.js +3 -0
  82. package/src/simulation/snapshots/raceSnapshots.js +1 -0
  83. package/src/simulation/timing/lapTelemetry.js +28 -9
  84. package/src/simulation/track/pitLaneLayout.js +7 -3
  85. package/src/simulation/track/spatialQueries.js +5 -28
  86. package/src/simulation/track/trackQueryGrid.js +94 -0
  87. package/src/simulation/track/trackQueryIndex.js +193 -434
  88. package/src/simulation/track/trackQueryPitIndex.js +140 -0
  89. package/src/simulation/track/trackQueryScratch.js +46 -0
  90. package/src/simulation/track/trackQueryStats.js +28 -0
  91. package/src/simulation/track/trackStatePolicy.js +0 -1
  92. package/src/simulation/vehicle/contactResolution.js +67 -2
  93. package/src/simulation/vehicle/runoffResponse.js +11 -12
  94. package/src/simulation/vehicle/vehicleKinematics.js +37 -0
  95. package/src/simulation/vehicle/vehiclePhysics.js +5 -4
  96. package/src/simulation/vehicle/vehicleSnapshots.js +22 -0
  97. package/src/simulation/vehicle/vehicleState.js +36 -2
  98. package/src/simulation/vehicle/wheelSurface.js +3 -1
  99. package/src/simulation/warmup/runtimeWarmup.js +0 -1
  100. package/src/styles.css +1185 -136
  101. package/src/ui/bannerTemplates.js +38 -28
  102. package/src/ui/cameraControlsTemplate.js +16 -5
  103. package/src/ui/carOverviewTemplate.js +12 -5
  104. package/src/ui/raceCanvasTemplate.js +31 -8
  105. package/src/ui/raceControlsTemplate.js +27 -10
  106. package/src/ui/shellTemplate.js +3 -1
  107. package/src/ui/telemetryDrawerTemplate.js +23 -10
  108. package/src/ui/telemetryTemplates.js +58 -32
  109. package/src/ui/templateUtils.js +48 -0
  110. package/src/ui/timingTowerTemplate.js +29 -12
package/docs/training.md CHANGED
@@ -46,6 +46,7 @@ The current expert API is a JavaScript environment contract. It supports:
46
46
  - reset-only scenario placement through `scenario.preset`, `scenario.placements`, `scenario.traffic`, and episode-boundary `resetDrivers(placements)`
47
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
48
  - per-driver episode state and neutral step metrics for batched training loops
49
+ - `episode.maxSteps` as a controlled-driver episode safety horizon; the default is high for browser/expert usability, so training and evaluation loops should set an explicit smaller value
49
50
  - neutral rollout recording through `createRolloutRecorder()`
50
51
  - deterministic evaluation metrics through `runEnvironmentEvaluation()` and `createEvaluationTracker()`
51
52
  - a JSON-serializable worker protocol wrapper through `createEnvironmentWorkerProtocol()`
@@ -56,7 +57,7 @@ The current expert API is a JavaScript environment contract. It supports:
56
57
 
57
58
  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
 
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
+ `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
 
61
62
  ```js
62
63
  const env = createPaddockEnvironment({
@@ -144,7 +145,7 @@ Controlled drivers do not receive tire-threshold automatic pit calls from the bu
144
145
 
145
146
  `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
 
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
+ 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
 
149
150
  `observation.object.self.appliedControls` is diagnostic/reference data for the
150
151
  latest physics step. For controlled cars it mirrors the normalized action after
@@ -155,15 +156,15 @@ built-in driver applied. This is intended for audits or clean local imitation
155
156
  datasets; it does not blend built-in AI with a model during normal controlled
156
157
  stepping.
157
158
 
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
+ For model training, prefer `physicsMode: 'advanced'` 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
 
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
+ Surface-aware rays are opt-in because they are more expensive than the default road-edge/car channels. The environment normalizes both the compact ray shape and the newer per-ray layout:
161
162
 
162
163
  ```js
163
164
  const env = createPaddockEnvironment({
164
165
  drivers,
165
166
  controlledDrivers: ['budget'],
166
- physicsMode: 'simulator',
167
+ physicsMode: 'advanced',
167
168
  observation: { profile: 'physical-driver' },
168
169
  sensors: {
169
170
  rays: {
@@ -183,9 +184,13 @@ Only requested ray channels are computed. The default compact config still compu
183
184
 
184
185
  `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
 
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
+ 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
 
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.
189
+ 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.
190
+
191
+ Track-position, wheel-surface, pit-lane, and ray queries use the internal startup-built track query index. Browser/expert mounts, headless environments, direct race-simulation construction, compact vector-only training loops, and `batch-training` ray runs all use that canonical indexed path. This does not change the observation shape. Ray `precision: 'driver'` uses indexed projection plus driver-precision validation for road-edge, kerb, and illegal-surface distances, preserving zero-distance origin surface hits and avoiding debug-only refinement in policy inputs. Debug precision remains available only for explicitly labeled diagnostics.
192
+
193
+ `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.
189
194
 
190
195
  ## Headless Training Loop
191
196
 
@@ -263,7 +268,7 @@ In the browser renderer, no-collision participants still look like solid cars, b
263
268
 
264
269
  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
270
 
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.
271
+ 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.
267
272
 
268
273
  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
274
 
@@ -281,7 +286,7 @@ participantInteractions: {
281
286
 
282
287
  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
288
 
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.
289
+ 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.
285
290
 
286
291
  Use `result.stateOutput` to avoid returning more state than the loop needs:
287
292
 
@@ -298,16 +303,18 @@ const env = createPaddockEnvironment({
298
303
  });
299
304
  ```
300
305
 
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.
306
+ `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.
307
+
308
+ Advanced-mode velocity senses are authoritative only when the simulator owns a real world velocity. Arcade snapshots intentionally expose `velocityX/Y` as `null`. When advanced-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.
302
309
 
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:
310
+ On the local 20-car batch-training benchmark used for the original index work (`physicsMode: 'advanced'`, `frameSkip: 4`, `physical-driver`, `driver-front-heavy` rays), the measured environment action cost after compact output and indexed track queries was approximately:
304
311
 
305
312
  ```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
313
+ nearest track states 1.52x faster than the pre-indexed path
314
+ pit lane states 2.02x faster than the pre-indexed path
315
+ sampled high-curvature off-track rays 14.81x faster than the pre-indexed path
316
+ wheel surface near pit connector 1.12x faster than the pre-indexed path
317
+ 20-car batch recovery environment 1.03x faster than the pre-indexed path
311
318
  ```
312
319
 
313
320
  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.
@@ -362,7 +369,7 @@ env.resetDrivers({
362
369
  });
363
370
  ```
364
371
 
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.
372
+ `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.
366
373
 
367
374
  The worker protocol mirrors this with a `resetDrivers` message:
368
375
 
@@ -396,15 +403,15 @@ const next = env.step(action);
396
403
  recorder.recordStep(result, action, next);
397
404
  ```
398
405
 
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.
406
+ 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.
400
407
 
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`.
408
+ 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 older `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
409
 
403
410
  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
411
 
405
412
  ## Example Rewards
406
413
 
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.
414
+ `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.
408
415
 
409
416
  ## Visual Playback Loop
410
417
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inventure71/paddockjs",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "description": "A data-driven browser F1 simulator component for host websites.",
6
6
  "license": "Apache-2.0",
@@ -1,4 +1,5 @@
1
1
  import { F1SimulatorApp } from '../app/F1SimulatorApp.js';
2
+ import { installLayoutSupport } from '../app/layoutSupport.js';
2
3
  import {
3
4
  createRaceDataPanelMarkup,
4
5
  createTelemetrySectorBannerMarkup,
@@ -18,6 +19,9 @@ import {
18
19
  createTimingTowerMarkup,
19
20
  } from '../ui/componentTemplates.js';
20
21
  import { applyPaddockThemeCssVariables, resolveF1SimulatorOptions } from '../config/defaultOptions.js';
22
+ import { formatCssUrl } from '../config/cssValues.js';
23
+ import { mergeRestartOptions } from '../config/restartOptions.js';
24
+ import { getNextTimingGapMode, normalizeTimingGapMode } from '../config/timingGapMode.js';
21
25
 
22
26
  function assertMountTarget(root, label) {
23
27
  if (!root || typeof root !== 'object' || !('innerHTML' in root)) {
@@ -27,42 +31,10 @@ function assertMountTarget(root, label) {
27
31
 
28
32
  function setPackageCssVariables(root, assets, theme) {
29
33
  root.classList?.add?.('f1-sim-component');
30
- root.style?.setProperty?.('--broadcast-panel-surface', `url('${assets.broadcastPanel}')`);
34
+ root.style?.setProperty?.('--broadcast-panel-surface', formatCssUrl(assets.broadcastPanel));
31
35
  applyPaddockThemeCssVariables(root, theme);
32
36
  }
33
37
 
34
- function mergeResolvedOptions(previousOptions, nextOptions) {
35
- const resetFromPreset = Object.hasOwn(nextOptions, 'preset');
36
- const previousUi = resetFromPreset ? {} : previousOptions.ui;
37
- const previousTheme = resetFromPreset ? {} : previousOptions.theme;
38
- return {
39
- ...previousOptions,
40
- ...nextOptions,
41
- ui: {
42
- ...previousUi,
43
- ...(nextOptions.ui ?? {}),
44
- raceDataBanners: {
45
- ...(previousUi.raceDataBanners ?? {}),
46
- ...(nextOptions.ui?.raceDataBanners ?? {}),
47
- },
48
- },
49
- theme: {
50
- ...previousTheme,
51
- ...(nextOptions.theme ?? {}),
52
- },
53
- assets: {
54
- ...previousOptions.assets,
55
- ...(nextOptions.assets ?? {}),
56
- trackTextures: {
57
- ...previousOptions.assets.trackTextures,
58
- ...(nextOptions.assets?.trackTextures ?? {}),
59
- },
60
- },
61
- drivers: nextOptions.drivers ?? previousOptions.drivers,
62
- entries: nextOptions.entries ?? previousOptions.entries,
63
- };
64
- }
65
-
66
38
  function createCompositeRoot(getRoots, getOptions) {
67
39
  return {
68
40
  style: {
@@ -80,6 +52,12 @@ function createCompositeRoot(getRoots, getOptions) {
80
52
  querySelectorAll(selector) {
81
53
  return getRoots().flatMap((root) => [...(root.querySelectorAll?.(selector) ?? [])]);
82
54
  },
55
+ setAttribute(name, value) {
56
+ getRoots().forEach((root) => root.setAttribute?.(name, value));
57
+ },
58
+ removeAttribute(name) {
59
+ getRoots().forEach((root) => root.removeAttribute?.(name));
60
+ },
83
61
  applyCssVariables() {
84
62
  const options = getOptions();
85
63
  getRoots().forEach((root) => setPackageCssVariables(root, options.assets, options.theme));
@@ -91,6 +69,7 @@ export class PaddockSimulatorController {
91
69
  constructor(options = {}) {
92
70
  this.options = resolveF1SimulatorOptions(options);
93
71
  this.roots = new Map();
72
+ this.layoutSupportCleanups = new Map();
94
73
  this.app = null;
95
74
  this.compositeRoot = createCompositeRoot(() => [...this.roots.values()], () => this.options);
96
75
  }
@@ -102,6 +81,8 @@ export class PaddockSimulatorController {
102
81
  }
103
82
  root.innerHTML = markup;
104
83
  setPackageCssVariables(root, this.options.assets, this.options.theme);
84
+ this.layoutSupportCleanups.get(key)?.();
85
+ this.layoutSupportCleanups.set(key, installLayoutSupport(root));
105
86
  this.roots.set(key, root);
106
87
  return root;
107
88
  }
@@ -127,6 +108,7 @@ export class PaddockSimulatorController {
127
108
  includeTimingTower = false,
128
109
  includeTelemetrySectorBanner = false,
129
110
  timingTowerVerticalFit,
111
+ responsiveNarrowLayout,
130
112
  } = {}) {
131
113
  return this.mountComponent(root, 'race-canvas', createRaceCanvasMarkup({
132
114
  ...this.options,
@@ -134,6 +116,7 @@ export class PaddockSimulatorController {
134
116
  includeTimingTower,
135
117
  includeTelemetrySectorBanner,
136
118
  timingTowerVerticalFit,
119
+ responsiveNarrowLayout,
137
120
  }));
138
121
  }
139
122
 
@@ -170,7 +153,10 @@ export class PaddockSimulatorController {
170
153
  }
171
154
 
172
155
  mountRaceDataPanel(root) {
173
- return this.mountComponent(root, 'race-data-panel', createRaceDataPanelMarkup(this.options));
156
+ return this.mountComponent(root, 'race-data-panel', createRaceDataPanelMarkup({
157
+ ...this.options,
158
+ standalone: true,
159
+ }));
174
160
  }
175
161
 
176
162
  querySelector(selector) {
@@ -196,15 +182,35 @@ export class PaddockSimulatorController {
196
182
  destroy() {
197
183
  this.app?.destroy();
198
184
  this.app = null;
185
+ this.layoutSupportCleanups.forEach((cleanup) => cleanup());
186
+ this.layoutSupportCleanups.clear();
199
187
  this.roots.forEach((root) => {
200
188
  root.innerHTML = '';
201
189
  });
202
190
  this.roots.clear();
203
191
  }
204
192
 
193
+ syncOptionsFromRunningApp() {
194
+ const timingGapMode = this.app?.getTimingGapMode?.();
195
+ if (!timingGapMode) return this.options;
196
+ this.options = {
197
+ ...this.options,
198
+ ui: {
199
+ ...this.options.ui,
200
+ timingGapMode: normalizeTimingGapMode(timingGapMode),
201
+ },
202
+ };
203
+ return this.options;
204
+ }
205
+
205
206
  restart(nextOptions = {}) {
206
- const nextResolvedOptions = resolveF1SimulatorOptions(mergeResolvedOptions(this.options, nextOptions));
207
- this.app?.restart(nextResolvedOptions);
207
+ const currentOptions = this.app ? this.syncOptionsFromRunningApp() : this.options;
208
+ const nextResolvedOptions = resolveF1SimulatorOptions(mergeRestartOptions(currentOptions, nextOptions));
209
+ if (this.app) {
210
+ this.app.restart(nextResolvedOptions);
211
+ this.options = nextResolvedOptions;
212
+ return;
213
+ }
208
214
  this.options = nextResolvedOptions;
209
215
  this.compositeRoot.applyCssVariables();
210
216
  }
@@ -254,6 +260,39 @@ export class PaddockSimulatorController {
254
260
  return this.app?.simulationSpeed ?? 1;
255
261
  }
256
262
 
263
+ setTimingGapMode(mode) {
264
+ const timingGapMode = this.app
265
+ ? this.app.setTimingGapMode(mode)
266
+ : normalizeTimingGapMode(mode);
267
+ this.options = {
268
+ ...this.options,
269
+ ui: {
270
+ ...this.options.ui,
271
+ timingGapMode,
272
+ },
273
+ };
274
+ return this.options.ui.timingGapMode;
275
+ }
276
+
277
+ getTimingGapMode() {
278
+ return this.app?.getTimingGapMode?.() ?? normalizeTimingGapMode(this.options.ui?.timingGapMode);
279
+ }
280
+
281
+ toggleTimingGapMode() {
282
+ if (this.app) {
283
+ const timingGapMode = this.app.toggleTimingGapMode();
284
+ this.options = {
285
+ ...this.options,
286
+ ui: {
287
+ ...this.options.ui,
288
+ timingGapMode,
289
+ },
290
+ };
291
+ return timingGapMode;
292
+ }
293
+ return this.setTimingGapMode(getNextTimingGapMode(this.options.ui?.timingGapMode));
294
+ }
295
+
257
296
  servePenalty(penaltyId) {
258
297
  return this.app?.servePenalty(penaltyId) ?? null;
259
298
  }