@inventure71/paddockjs 2.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 (62) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/INSTALL_AND_UPDATE.md +1 -1
  3. package/README.md +30 -5
  4. package/docs/architecture.md +15 -9
  5. package/docs/concepts.md +6 -4
  6. package/docs/data_contract.md +28 -15
  7. package/docs/data_model.md +1 -1
  8. package/docs/index.md +9 -0
  9. package/docs/rules.md +37 -7
  10. package/docs/sense_contract.md +59 -0
  11. package/docs/system_specs.md +56 -7
  12. package/docs/training.md +16 -10
  13. package/package.json +1 -1
  14. package/src/api/PaddockSimulatorController.js +2 -33
  15. package/src/app/BrowserExpertAdapter.js +9 -300
  16. package/src/app/BrowserExpertExternalRenderer.js +303 -0
  17. package/src/app/F1SimulatorApp.js +1 -42
  18. package/src/config/restartOptions.js +34 -0
  19. package/src/environment/controllerLoop.js +17 -2
  20. package/src/environment/episode.js +14 -2
  21. package/src/environment/index.d.ts +16 -2
  22. package/src/environment/observationVector.js +200 -0
  23. package/src/environment/observations.js +58 -285
  24. package/src/environment/recorder.js +13 -6
  25. package/src/environment/rewards.js +13 -2
  26. package/src/environment/runtime.js +62 -14
  27. package/src/environment/scenarios.js +22 -3
  28. package/src/environment/sensors/index.js +3 -3
  29. package/src/environment/sensors/opponentRadar.js +13 -3
  30. package/src/environment/sensors/sensorTargets.js +8 -2
  31. package/src/environment/sensors/surfaceRays.js +90 -26
  32. package/src/environment/sensors/trackRays.js +185 -22
  33. package/src/environment/specs.js +78 -40
  34. package/src/index.d.ts +2 -0
  35. package/src/index.js +1 -32
  36. package/src/simulation/pit/pitFlow.js +5 -0
  37. package/src/simulation/pit/pitKinematics.js +24 -0
  38. package/src/simulation/pit/pitQueue.js +5 -0
  39. package/src/simulation/pit/pitRouteMovement.js +7 -0
  40. package/src/simulation/pit/pitService.js +2 -0
  41. package/src/simulation/pit/pitTireService.js +2 -0
  42. package/src/simulation/race/raceLifecycle.js +2 -6
  43. package/src/simulation/race/raceProgress.js +6 -1
  44. package/src/simulation/race/raceStep.js +1 -1
  45. package/src/simulation/race/redFlag.js +3 -3
  46. package/src/simulation/race/safetyCar.js +10 -4
  47. package/src/simulation/race/stalledDnf.js +8 -17
  48. package/src/simulation/raceSimulation.js +1 -0
  49. package/src/simulation/rules/rulesReview.js +3 -0
  50. package/src/simulation/snapshots/raceSnapshots.js +1 -0
  51. package/src/simulation/track/trackQueryGrid.js +94 -0
  52. package/src/simulation/track/trackQueryIndex.js +16 -302
  53. package/src/simulation/track/trackQueryPitIndex.js +140 -0
  54. package/src/simulation/track/trackQueryScratch.js +46 -0
  55. package/src/simulation/track/trackQueryStats.js +28 -0
  56. package/src/simulation/vehicle/contactResolution.js +67 -2
  57. package/src/simulation/vehicle/runoffResponse.js +11 -12
  58. package/src/simulation/vehicle/vehicleKinematics.js +37 -0
  59. package/src/simulation/vehicle/vehiclePhysics.js +2 -1
  60. package/src/simulation/vehicle/vehicleSnapshots.js +11 -0
  61. package/src/simulation/vehicle/vehicleState.js +36 -2
  62. package/src/simulation/vehicle/wheelSurface.js +3 -1
package/docs/rules.md CHANGED
@@ -25,6 +25,28 @@ Race rules are normalized before the simulation starts. Hosts can choose a packa
25
25
 
26
26
  The preset only chooses defaults. Explicit `rules.modules` values override the preset.
27
27
 
28
+ Default module state:
29
+
30
+ | Module | Package default | Important default options |
31
+ | --- | --- | --- |
32
+ | `pitStops` | disabled | `pitLaneSpeedLimitKph: 80`, `defaultStopSeconds: 2.8`, `maxConcurrentPitLaneCars: 3`, `minimumPitLaneGapMeters: 20`, `doubleStacking: false`, tire pit thresholds `50%` request / `30%` commit |
33
+ | `tireStrategy` | disabled | `compounds: ['S', 'M', 'H']`, `mandatoryDistinctDryCompounds: null` |
34
+ | `tireDegradation` | enabled | tire energy updates from throttle, braking, and lateral load unless disabled |
35
+ | `stalledDnf` | disabled | `maxStoppedSeconds: 12`, `speedThresholdKph: 5` |
36
+ | `penalties` | disabled | `stewardStrictness: 1`; track-limit, collision, tire-requirement, and pit-lane-speeding subsections default to `strictness: 0` |
37
+ | `weather` | disabled | reserved; no current grip or visibility effect |
38
+ | `reliability` | disabled | reserved; no current failure-risk effect |
39
+ | `fuelLoad` | disabled | reserved; no current mass or pace effect |
40
+
41
+ Ruleset preset deltas:
42
+
43
+ | Ruleset | Module changes from package defaults |
44
+ | --- | --- |
45
+ | `paddock` | no module changes; uses the package defaults above |
46
+ | `custom` | no module changes before explicit host overrides |
47
+ | `grandPrix2025` | enables `pitStops`, enables `tireStrategy` with `mandatoryDistinctDryCompounds: 2`, enables `penalties` with `stewardStrictness: 0.85`, track-limit strictness `0.85`, collision strictness `0.65`, tire-requirement strictness `1`, and pit-lane-speeding strictness `1` |
48
+ | `fia2025` | alias for `grandPrix2025` |
49
+
28
50
  ## Rule Modules
29
51
 
30
52
  Advanced race behavior is organized under `rules.modules` so hosts can enable, disable, or tune each system independently:
@@ -71,14 +93,18 @@ rules: {
71
93
  }
72
94
  ```
73
95
 
74
- The current implementation normalizes and exposes all module config, records a penalty ledger, enforces collision penalties, track-limit penalties, tire-requirement penalties, pit-lane speeding penalties, and opt-in stalled off-track DNFs, creates/renders pit-lane geometry for every track, treats pit-lane asphalt, working-lane service areas, and garage boxes as legal drivable surfaces, applies nonlinear tire-energy degradation when `tireDegradation.enabled` is not `false`, and runs automated pit stops when `pitStops.enabled` is true. Weather effects, reliability failures, and fuel-load performance effects are reserved future modules only; the simulation does not change grip, power, retirement risk, mass, or pace from those module keys.
96
+ The current implementation normalizes and exposes all module config, records a penalty ledger, enforces collision penalties, track-limit penalties, tire-requirement penalties, pit-lane speeding penalties, and opt-in stalled off-track DNFs, creates/renders pit-lane geometry when the resolved track generation options enable it, treats pit-lane asphalt, working-lane service areas, and garage boxes as legal drivable surfaces, applies nonlinear tire-energy degradation when `tireDegradation.enabled` is not `false`, and runs automated pit stops when `pitStops.enabled` is true. Weather effects, reliability failures, and fuel-load performance effects are reserved future modules only; the simulation does not change grip, power, retirement risk, mass, or pace from those module keys.
97
+
98
+ `tireStrategy` does not enable automatic pit stops. Enable `rules.modules.pitStops.enabled` separately, or use `ruleset: 'fia2025'` / `ruleset: 'grandPrix2025'`, when cars should use the automatic pit-entry, queue, service, and exit route. `tireStrategy.enabled` controls compounds, tire-requirement stewarding, and pit-stop tire choices.
75
99
 
76
100
  `tireDegradation.enabled: false` freezes each car's current `tireEnergy` instead of updating wear from throttle, braking, and lateral load. This is separate from `tireStrategy.enabled`: tire strategy controls compounds, tire-requirement stewarding, and pit-stop tire choices, while tire degradation controls the physics wear curve.
77
101
 
78
- `stalledDnf` defaults to `enabled: false`. When a host sets `rules.modules.stalledDnf.enabled: true`, a car that is off legal racing or pit surfaces and remains below `speedThresholdKph` for `maxStoppedSeconds` is marked out of race with `dnfReason: 'stalled-off-track'`, emits a `car-dnf` event, freezes controls/motion, and leaves active collision, sensor, pit-blocking, and race-order participation. The timer does not run during pre-start, red flag, after race completion, for already-finished cars, or while pit entry/queue/service/exit owns the car. Returning to legal surface or moving above the threshold resets the timer.
102
+ `stalledDnf` defaults to `enabled: false`. When a host sets `rules.modules.stalledDnf.enabled: true`, a car that is off legal racing or pit surfaces and remains below `speedThresholdKph` for `maxStoppedSeconds` is marked out of race with `dnfReason: 'stalled-off-track'`, emits a `car-dnf` event, freezes controls/motion, and leaves active collision, sensor, pit-blocking, and race-order participation. The freeze clears simulator velocity, acceleration, slip, grip, and manual controls together, so a retired car cannot keep a stale motion vector in snapshots or model observations. The timer does not run during pre-start, red flag, after race completion, for already-finished cars, or while pit entry/queue/service/exit owns the car. Returning to legal surface or moving above the threshold resets the timer.
79
103
 
80
104
  Participant interaction profiles can opt real cars out of specific interaction systems without turning them into replay ghosts. A non-colliding profile skips vehicle collision resolution and collision stewarding for that car pair, but the car still uses normal steering/throttle/brake physics. A non-blocking pit profile is ignored by pit service occupancy and queue-blocking checks. A car with `affectsRaceOrder: false` remains in `snapshot.cars` but is excluded from ranking, DRS references, finish order, and final classification. Replay ghosts are separate trajectory overlays in `snapshot.replayGhosts`; they never participate in rules, collisions, timing, pit stops, or penalties.
81
105
 
106
+ Automatic pit routing owns car motion while a car is entering, queued, servicing, or exiting. During those phases the route/service logic keeps scalar speed, throttle/brake, `appliedControls`, and simulator telemetry synchronized so simulator-mode observations and the next physics-owned step do not inherit stale pre-pit motion, grip, slip, or instability state. `velocityX/Y` is synchronized in simulator mode and cleared from arcade snapshots because arcade physics does not own a world-velocity state.
107
+
82
108
  ## Steward Strictness
83
109
 
84
110
  Penalty subsections use `strictness` from `0` to `1` instead of a boolean:
@@ -104,6 +130,8 @@ Pit-lane surfaces are legal road for track-limit purposes. `pit-entry`, `pit-lan
104
130
 
105
131
  Penalty decisions are recorded in `snapshot.penalties` and emitted as `penalty` events in the same step. Each entry includes the penalty type, driver id, strictness, status, penalty seconds, pending service conversion seconds, lap, timestamp, and rule-specific context. Multiple time penalties for the same driver are additive: two separate +5s entries produce `penaltySeconds: 10` on that car's snapshot and classification adjustment.
106
132
 
133
+ Collision response and collision-steward impact speed use simulator `velocityX/Y` when available. In simulator mode this keeps slip, sideways recovery, pit-adjacent motion, and post-contact damping aligned with actual physical motion instead of assuming every car moves exactly in its heading direction. If a collision pushes against a pit-route-owned car, the pit-owned car stays fixed and the movable car's scalar speed and velocity vector are damped together so the next physics step and model-facing speed senses remain consistent.
134
+
107
135
  Each penalty subsection can define `consequences`. Supported consequences are:
108
136
 
109
137
  - `{ type: 'warning' }`
@@ -166,7 +194,7 @@ The race engine uses simulator units internally. `src/simulation/units.js` conve
166
194
 
167
195
  The default `physicsMode` is `'arcade'`, which preserves the existing forgiving vehicle behavior for current browser hosts and demos.
168
196
 
169
- `physicsMode: 'simulator'` is opt-in for more believable F1-style limits. It still accepts only normal steering, throttle, brake, and pit-intent inputs, but the vehicle integrator keeps a real 2D world velocity, derives speed from that velocity, and applies a traction budget between longitudinal and lateral demand. Steering changes yaw through a rate-limited response, while body slip is derived from the difference between the car heading and actual velocity direction instead of being an artificial movement offset. Kerbs remain legal racing surface, but high-load kerb use is less stable and slower than track asphalt. Gravel and grass are averaged per wheel so one wheel over the edge creates partial grip/drag loss, while all-wheel runoff still costs major time. The rendered barrier wall is the hard outer runoff boundary in both physics modes; cars whose footprint reaches the wall's inner face are marked `destroyed`, stopped, removed from active collision and sensor participation, and placed in DNF timing order. `physicsMode` changes vehicle integration and grip behavior, not barrier consequences. Simulator mode does not teleport cars, magnetize them to the track, or bypass physics with hidden pose correction.
197
+ `physicsMode: 'simulator'` is opt-in for more believable F1-style limits. It still accepts only normal steering, throttle, brake, and pit-intent inputs, but the vehicle integrator keeps a real 2D world velocity, derives speed from that velocity, and applies a traction budget between longitudinal and lateral demand. Steering changes yaw through a rate-limited response, while body slip is derived from the difference between the car heading and actual velocity direction instead of being an artificial movement offset. External state placement through simulator APIs rejects non-finite numeric values and resynchronizes `velocityX/Y` whenever speed or heading is changed without an explicit velocity vector, so scenario/reset placement cannot leave stale simulator velocity behind. Kerbs remain legal racing surface, but high-load kerb use is less stable and slower than track asphalt. Gravel and grass are averaged per wheel so one wheel over the edge creates partial grip/drag loss, while all-wheel runoff still costs major time. The rendered barrier wall is the hard outer runoff boundary in both physics modes; cars whose footprint reaches the wall's inner face are marked `destroyed`, stopped, removed from active collision and sensor participation, and placed in DNF timing order. Barrier destruction, stalled DNF, pre-start grid hold, red-flag hold, and pit-queue hold freeze scalar speed and simulator velocity together; clearing a red flag resynchronizes active cars to the release speed and heading before the next observation while DNF/out-of-race cars remain frozen. `physicsMode` changes vehicle integration and grip behavior, not barrier consequences. Simulator mode does not teleport cars, magnetize them to the track, or bypass physics with hidden pose correction.
170
198
 
171
199
  Simulator snapshots and environment observations expose additional vehicle telemetry: `lateralG`, `longitudinalG`, `gripUsage`, `slipAngleRadians`, `tractionLimited`, and `stabilityState`. These fields are optional public telemetry; existing `speedKph`, wheel-surface, steering, throttle, and brake fields remain unchanged.
172
200
 
@@ -194,7 +222,7 @@ DRS behavior:
194
222
 
195
223
  Physics effect:
196
224
 
197
- - Active DRS reduces drag through `src/simulation/vehiclePhysics.js`.
225
+ - Active DRS reduces drag through `src/simulation/vehicle/vehiclePhysics.js`; the root `src/simulation/vehiclePhysics.js` path is only a compatibility re-export.
198
226
  - The current drag multiplier is `0.42`.
199
227
 
200
228
  Visual effect:
@@ -252,11 +280,13 @@ Traffic decisions use real meter-scaled gaps and lateral spacing, not raw simula
252
280
 
253
281
  Rejoin behavior also remains physics-driven. When a car leaves the legal racing surface, the controller enters a short recovery hold and keeps using rejoin controls after the center point first reaches track or kerb until heading and edge margin are stable. That prevents the car from immediately returning to full racing throttle while still pointed across the track. Recovery targets look forward along the racing surface, bias inward from the edge, and request bounded surface-specific speeds for track, kerb, grass, and gravel states. In simulator mode, rejoin controls also watch signed outward velocity relative to the track normal: a car sliding farther into runoff cuts throttle or brakes before trying to accelerate out, while very low-speed gravel/grass recovery limits steering lock and caps throttle when the heading is still outward so steering scrub does not pin the car in place. Barrier contact is not a recovery state; it destroys the car.
254
282
 
283
+ Simulator-mode steering scrub is dissipative in both forward and reverse motion. It opposes the signed forward velocity instead of always applying negative forward acceleration, so a car sliding backward with zero throttle cannot gain speed from steering scrub.
284
+
255
285
  Tire energy can degrade to 1%. The vehicle physics layer converts tire energy into a nonlinear grip factor, so degradation has a visible performance cost across the full 100% to 1% range while still leaving a damaged car controllable enough to return to the pits.
256
286
 
257
287
  ## Vehicle Physics
258
288
 
259
- Vehicle physics live in `src/simulation/vehiclePhysics.js`.
289
+ Vehicle physics live in `src/simulation/vehicle/vehiclePhysics.js`.
260
290
 
261
291
  The model includes:
262
292
 
@@ -299,7 +329,7 @@ When pit stops are enabled, each team is assigned one shared service area in the
299
329
 
300
330
  Collision handling uses:
301
331
 
302
- - Shared vehicle geometry from `src/simulation/vehicleGeometry.js`.
332
+ - Shared vehicle geometry from `src/simulation/vehicle/vehicleGeometry.js`.
303
333
  - A body collision hull for car-vs-car contact.
304
334
  - Four wheel/contact-patch shapes for surface and track-limit state, not for car-vs-car contact.
305
335
  - Track-progress candidate pruning before narrow-phase checks.
@@ -322,7 +352,7 @@ These are not currently implemented:
322
352
 
323
353
  - Strategic pit-call timing beyond the current first automatic stop.
324
354
  - Double stacking and pit-crew conflicts.
325
- - Manual tire strategy selection beyond changing to the first different configured compound.
355
+ - Full race-strategy planning beyond the current automatic pit schedule and host/model pit intents.
326
356
  - Fuel load strategy.
327
357
  - Weather.
328
358
  - Mechanical failures.
@@ -0,0 +1,59 @@
1
+ # Model Sense Contract
2
+
3
+ This document defines the proof boundary for model-facing PaddockJS senses. The
4
+ active environment observation contract is the source of truth for policies,
5
+ Policy Runner inputs, expert visualization, and rollout recording.
6
+
7
+ ## What Is Proven In Code
8
+
9
+ `src/__tests__/environmentSenseContract.test.js` is the executable contract for
10
+ core model-facing senses. It checks three layers:
11
+
12
+ 1. Object observations are compared against independent formulas from the
13
+ returned simulator snapshot. This covers body telemetry, local track
14
+ relation, race state, lookahead samples, wheel contact patches, nearby-car
15
+ geometry, and opponent radar fields.
16
+ 2. Vector observations are decoded by schema name and compared against the
17
+ object observation oracle. A vector entry is not accepted unless a matching
18
+ schema oracle exists.
19
+ 3. Compact vector-only output is compared against full output for the same
20
+ deterministic state, and public per-driver observation specs are compared
21
+ against the schemas produced in observations.
22
+
23
+ This is intentionally stronger than testing current implementation against
24
+ itself: the object oracle uses simulator snapshot facts and unit conversions,
25
+ while the vector oracle uses schema names and object fields rather than calling
26
+ `buildObservationVector()`.
27
+
28
+ ## Model-Facing Sense Rules
29
+
30
+ - Public distance fields with a `Meters` suffix are meters, not simulator
31
+ units.
32
+ - Public angle fields with a `Radians` suffix are radians.
33
+ - Speed is exposed both as `speedKph` and `speedMetersPerSecond`; the latter is
34
+ derived from the simulator speed unit conversion.
35
+ - `self.onTrack` and `trackRelation.onLegalSurface` follow wheel-level legal
36
+ surface rules when wheel contact patches exist.
37
+ - `contactPatches` always uses the stable order `front-left`, `front-right`,
38
+ `rear-left`, `rear-right`.
39
+ - `nearbyCars` are sorted by car-relative distance, with deterministic ordering
40
+ for equal distances.
41
+ - Opponent radar fields use actual simulator velocity vectors when present.
42
+ - Ray object values, vector values, schemas, and visualizations must describe
43
+ the same active observation values. Debug-only precision may exist only when
44
+ clearly labeled and must not be presented as model input.
45
+ - `getObservationSpec()` is the canonical schema for compact loops, including
46
+ per-driver sensor overrides.
47
+
48
+ ## Remaining Formal Limits
49
+
50
+ The test suite proves conformance to the executable contract above for the
51
+ deterministic scenarios it enumerates. It is not a mathematical proof over every
52
+ possible JavaScript object a caller could construct or every physically possible
53
+ track state. Extending the proof boundary requires adding more oracle cases for
54
+ the specific state family, for example replay ghosts, pit-service movement, or a
55
+ new sensor channel.
56
+
57
+ Any new model-facing sense must add or extend an oracle in
58
+ `environmentSenseContract.test.js`. Adding a vector schema field without an
59
+ oracle must fail the test.
@@ -35,7 +35,31 @@ import {
35
35
  ```
36
36
 
37
37
  The environment subpath is the only public headless training import. It must stay free of DOM, PixiJS, CSS, and browser app dependencies.
38
- `createProgressReward()` is published from the same subpath only as non-canonical demo reward code for examples and smoke tests. It must remain optional and replaceable; environment stepping must continue to work with a custom `reward(context)` callback or no reward callback. Reward callbacks are user-owned formulas over package-owned facts, including neutral per-driver `metrics` and `info.drivers[driverId]` episode state. Non-finite or missing callback results are normalized to a neutral `0` reward for that controlled driver.
38
+
39
+ Root package exports:
40
+
41
+ | Export group | Public exports | Use |
42
+ | --- | --- | --- |
43
+ | Browser mounts | `mountF1Simulator`, `createPaddockSimulator` | all-in-one shell or composable package-owned UI surfaces |
44
+ | Composable helper mounts | `mountRaceControls`, `mountCameraControls`, `mountSafetyCarControl`, `mountTimingTower`, `mountRaceCanvas`, `mountTelemetryPanel`, `mountTelemetryCore`, `mountTelemetrySectors`, `mountTelemetrySectorBanner`, `mountTelemetryLapTimes`, `mountTelemetrySectorTimes`, `mountRaceTelemetryDrawer`, `mountCarDriverOverview`, `mountRaceDataPanel` | function-call wrappers around a composable simulator controller |
45
+ | Data helpers | `DriverData`, `VehicleData`, `CHAMPIONSHIP_ENTRY_BLUEPRINTS`, `DEMO_PROJECT_DRIVERS`, `buildChampionshipDriverGrid`, `formatDriverNumber`, `normalizeSimulatorDrivers` | demo data, host entry normalization, and rating helper construction |
46
+ | Package constants | `DEFAULT_F1_SIMULATOR_ASSETS`, `PADDOCK_SIMULATOR_PRESETS` | inspect bundled asset mapping and UI preset defaults |
47
+ | Track and units | `createProceduralTrack`, `REAL_F1_CAR_LENGTH_METERS`, `VISUAL_CAR_LENGTH_METERS`, `SIM_UNITS_PER_METER`, `TARGET_F1_TOP_SPEED_KPH`, `metersToSimUnits`, `simUnitsToMeters`, `kphToSimSpeed`, `simSpeedToKph`, `simSpeedToMetersPerSecond` | generated track definitions and physical/display unit conversion |
48
+ | Controller loop | `createPaddockDriverControllerLoop` | shared browser/headless batched model-controller orchestration |
49
+
50
+ Environment subpath exports:
51
+
52
+ | Export | Use |
53
+ | --- | --- |
54
+ | `createPaddockEnvironment` | browser-free step/reset runtime for JavaScript training and evaluation loops |
55
+ | `createPaddockDriverControllerLoop` | shared controller-loop helper for browser expert mode or headless environments |
56
+ | `createProgressReward` | optional demo reward used by examples; not the package objective |
57
+ | `createRolloutRecorder`, `createRolloutTransition` | neutral rollout transition recording helpers |
58
+ | `DEFAULT_EVALUATION_CASES`, `createEvaluationTracker`, `runEnvironmentEvaluation` | deterministic evaluation cases and metric aggregation |
59
+ | `ENVIRONMENT_SCENARIO_PRESETS` | reset-only scenario placement presets |
60
+ | `createEnvironmentWorkerProtocol`, `handleEnvironmentMessage` | JSON-serializable worker bridge helpers |
61
+
62
+ `createProgressReward()` is published from the same subpath only as non-canonical demo reward code for examples and smoke tests. It must remain optional and replaceable; environment stepping must continue to work with a custom `reward(context)` callback or no reward callback. Reward callbacks are user-owned formulas over package-owned facts, including neutral per-driver `metrics` and `info.drivers[driverId]` episode state. They run only for `step(actions)` transition results; reset/read paths are reward-neutral. Non-finite or missing callback results are normalized to a neutral `0` reward for that controlled driver.
39
63
  Ray observations expose track-transition distance and car distance per ray. Track transitions use `kind: 'exit'` when the ray starts on track and reaches the border, `kind: 'entry'` when the ray starts off track and reaches the road again, and `kind: null` with max distance when no transition is visible. Track-position, surface, pit-lane, and ray fallback queries can use an internal non-enumerable track query index built with the track model. Browser/expert mounts, headless environments, and direct race-simulation construction now default to the indexed path (opt-out with `trackQueryIndex: false`); compact training-style environment runs (including `batch-training` rays with full state output) still auto-enable it. This changes runtime cost, not public observation shapes.
40
64
  The package must not own model training, model persistence, model registries, or trained policy behavior. The supported contract is: external code reads observations, returns normalized actions, and advances either the headless environment or browser expert mode.
41
65
  The shared expert runtime exposes `getActionSpec()` and `getObservationSpec()` so external code can inspect controlled drivers, action ranges, sensor layout, nearby-car limits, track lookahead fields, and the versioned vector schema before connecting a model. Environment reset may apply deterministic scenario placement presets or explicit placement/traffic layouts; model steps may not mutate position and still use normalized actions only. Neutral rollout recording, deterministic evaluation metrics, and the JSON worker protocol are environment utilities, not training algorithms.
@@ -50,9 +74,14 @@ const simulator = await mountF1Simulator(root, {
50
74
  onDriverOpen,
51
75
  seed,
52
76
  trackSeed,
77
+ trackGeneration,
78
+ trackQueryIndex,
53
79
  warmup,
54
80
  totalLaps,
55
81
  physicsMode,
82
+ rules,
83
+ participantInteractions,
84
+ replayGhosts,
56
85
  initialCameraMode,
57
86
  preset,
58
87
  title,
@@ -62,6 +91,7 @@ const simulator = await mountF1Simulator(root, {
62
91
  showBackLink,
63
92
  ui,
64
93
  theme,
94
+ debug,
65
95
  assets,
66
96
  onLoadingChange,
67
97
  onReady,
@@ -83,8 +113,22 @@ const simulator = createPaddockSimulator({
83
113
  onDriverOpen,
84
114
  seed,
85
115
  trackSeed,
116
+ trackGeneration,
117
+ trackQueryIndex,
118
+ warmup,
86
119
  totalLaps,
120
+ physicsMode,
87
121
  initialCameraMode,
122
+ participantInteractions,
123
+ replayGhosts,
124
+ rules: {
125
+ ruleset: 'custom',
126
+ modules: {
127
+ pitStops: { enabled: true },
128
+ stalledDnf: { enabled: true },
129
+ tireStrategy: { enabled: true, mandatoryDistinctDryCompounds: 2 },
130
+ },
131
+ },
88
132
  });
89
133
 
90
134
  simulator.mountRaceControls(controlsRoot);
@@ -115,6 +159,8 @@ simulator.setPitLaneOpen(false);
115
159
  simulator.setRedFlagDeployed(true);
116
160
  ```
117
161
 
162
+ Pit APIs are rule-gated. `setPitIntent()` returns `false` until the active rules enable `rules.modules.pitStops.enabled` and the active track has pit-lane geometry. `tireStrategy` does not enable automatic pit routing by itself; it only controls available compounds, tire requirement stewarding, and pit target choices.
163
+
118
164
  Standalone helper functions are also exported for host code that prefers function calls:
119
165
 
120
166
  ```js
@@ -157,7 +203,10 @@ Returned controller:
157
203
  mountTelemetryLapTimes(root),
158
204
  mountTelemetrySectorTimes(root),
159
205
  mountRaceTelemetryDrawer(root, { timingTowerVerticalFit, drawerInitiallyOpen, raceDataTelemetryDetail }),
206
+ mountCarDriverOverview(root),
160
207
  mountRaceDataPanel(root),
208
+ querySelector(selector),
209
+ querySelectorAll(selector),
161
210
  start(),
162
211
 
163
212
  // Included on both APIs:
@@ -216,9 +265,9 @@ Returned controller:
216
265
  - `physicsMode` accepts `'arcade'` and `'simulator'`. The default is `'arcade'` to preserve existing hosts. `'simulator'` is opt-in and keeps cars controlled only through steering, throttle, brake, and pit intent while enabling traction-budget limits, steering scrub, velocity-heading slip, surface-specific grip/drag, and simulator telemetry. Snapshots expose `physicsMode` plus per-car `lateralG`, `longitudinalG`, `gripUsage`, `slipAngleRadians`, `tractionLimited`, `stabilityState`, and the latest `appliedControls`.
217
266
  - `restart(nextOptions)` can change race data and deterministic seeds such as `trackSeed`, but it does not support changing asset URLs. Asset changes require `destroy()` and a fresh mount because PixiJS texture loading is an initialization boundary.
218
267
  - `onDriverOpen(driver)` is the navigation boundary.
219
- - Lifecycle callbacks are optional: `onLoadingChange`, `onReady`, `onError`, `onDriverSelect`, `onRaceEvent`, `onLapChange`, and `onRaceFinish`. Host callback failures are routed to `onError` when possible and must not stop the simulator loop.
268
+ - Lifecycle callbacks are optional: `onLoadingChange`, `onReady`, `onError`, `onDriverSelect`, `onRaceEvent`, `onLapChange`, and `onRaceFinish`. Lifecycle callback failures are routed to `onError` when possible and must not stop the simulator loop. `onDriverOpen(driver)` is host-owned navigation code and is not wrapped as a lifecycle callback.
220
269
  - Race completion is part of the simulation snapshot. Cars receive individual `finished`, `finishTime`, `finishRank`, `status: 'waved-flag'`, `raceStatus: 'waved-flag'`, `wavedFlag`, `penaltySeconds`, `adjustedFinishTime`, and `classifiedRank` values as they cross the finish distance. Destroyed/out-of-race cars expose `dnf`, `dnfReason`, `dnfAt`, and `dnfOrder`, appear below active cars in timing, render faded/gray in the race canvas, and do not block race completion while they remain DNF. The first finisher sets a provisional `raceControl.winner`; already-finished cars remain frozen in provisional finish order while the remaining cars complete the distance. Final `raceControl.classification` and `raceControl.finished` are set only after every race participant has finished or is DNF. DNF entries are included after finishers with no finish time. A DNF car restored before final classification re-enters live timing and must finish; after final classification, resurrection does not reopen the race. Final classification converts unserved drive-through and stop-go penalties into configured time, sorts finishers by `finishTime + penaltySeconds`, applies position-drop and disqualification consequences, then race control switches to `safety-car`, the final order freezes to the classified result, and the field keeps circulating under safety-car behavior.
221
- - Race rules support package presets and custom module config. Supported rulesets are `paddock`, `grandPrix2025`, `fia2025`, and `custom`. Advanced modules include pit stops, tire strategy, tire degradation, stalled off-track DNF, penalties, weather, reliability, and fuel load. The current simulator normalizes all module config, creates/renders track-owned pit-lane geometry on an explicit start/finish straight, treats pit-lane asphalt, working-lane service areas, and garage boxes as legal drivable surfaces, runs automatic bounded pit-train entry/service/exit through the main fast lane and shared team service areas when pit stops are enabled, lets host/expert pit calls choose a target tire compound, uses the team queue point as a rolling gate when the service area is free, queues a second team car behind the active service area and moves it forward through a queue-release route only after the active service area is physically clear, keeps pit-route approach speed bounded until the final queue/service capture instead of crawling through open route, optionally varies pit-service time from team pit-crew stats with a perfect-training override, supports pit-lane open/closed state plus red flags, requests pit stops from configurable tire-energy thresholds, applies nonlinear tire-grip degradation down to 1% unless `rules.modules.tireDegradation.enabled` is `false`, can retire cars that remain off legal racing/pit surfaces below the configured stalled-DNF speed threshold for the configured duration when `rules.modules.stalledDnf.enabled` is `true`, applies the pit speed limiter only on the main pit lane/working lane, enforces pit-lane speeding on speed-limited pit-lane parts but not on entry/exit connectors, keeps the safety car about `55m` ahead of the leader with compact `22m` frozen-order queue slots, lets the built-in driver AI ride kerbs, attack, defend, and recover through normal steering/throttle/brake decisions with short controller-state commitments for rejoin and attack decisions, and enforces stewarded penalties for collisions, track limits, pit-lane speeding, and tire requirements.
270
+ - Race rules support package presets and custom module config. Supported rulesets are `paddock`, `grandPrix2025`, `fia2025`, and `custom`. Active advanced modules include pit stops, tire strategy, tire degradation, stalled off-track DNF, and penalties. Weather, reliability, and fuel load are reserved module placeholders only; they do not currently change grip, power, retirement risk, mass, or pace. The current simulator normalizes all module config, creates/renders track-owned pit-lane geometry on tracks whose resolved generation options enable it, treats pit-lane asphalt, working-lane service areas, and garage boxes as legal drivable surfaces, runs automatic bounded pit-train entry/service/exit through the main fast lane and shared team service areas when pit stops are enabled, lets host/expert pit calls choose a target tire compound, uses the team queue point as a rolling gate when the service area is free, queues a second team car behind the active service area and moves it forward through a queue-release route only after the active service area is physically clear, keeps pit-route approach speed bounded until the final queue/service capture instead of crawling through open route, optionally varies pit-service time from team pit-crew stats with a perfect-training override, supports pit-lane open/closed state plus red flags, requests pit stops from configurable tire-energy thresholds, applies nonlinear tire-grip degradation down to 1% unless `rules.modules.tireDegradation.enabled` is `false`, can retire cars that remain off legal racing/pit surfaces below the configured stalled-DNF speed threshold for the configured duration when `rules.modules.stalledDnf.enabled` is `true`, applies the pit speed limiter only on the main pit lane/working lane, enforces pit-lane speeding on speed-limited pit-lane parts but not on entry/exit connectors, keeps the safety car about `55m` ahead of the leader with compact `22m` frozen-order queue slots, lets the built-in driver AI ride kerbs, attack, defend, and recover through normal steering/throttle/brake decisions with short controller-state commitments for rejoin and attack decisions, and enforces stewarded penalties for collisions, track limits, pit-lane speeding, and tire requirements. `tireStrategy` does not enable automatic pit stops; `pitStops.enabled` is required for pit routing and non-clear pit intent requests.
222
271
  - Penalty subsections use `strictness` from `0` to `1`, not only boolean enablement. `strictness: 0` means the subsection is not enforced; `strictness: 1` applies the configured rule margin. Steward decisions are exposed as `penalty` events and as top-level `snapshot.penalties`. Penalty entries include normalized consequences and lifecycle status. Immediate consequences apply time, grid, position, or disqualification effects directly; drive-through and stop-go consequences remain issued until served, cancelled, or converted at final classification.
223
272
  - The browser UI can opt into top steward messages with `ui.penaltyBanners` and timing-row penalty badges with `ui.timingPenaltyBadges`. Steward messages render track-limit warning events and penalty decisions from the simulation; time-penalty messages put the penalty seconds in a large left chip and use penalty-colored backgrounds, while warnings use warning-colored backgrounds. Timing-row `!` badges are rendered only from `snapshot.penalties`; warning events do not count. UI code must not recalculate steward decisions.
224
273
  - The simulator must stay interactive after being installed through `npm install @inventure71/paddockjs`.
@@ -230,10 +279,10 @@ Returned controller:
230
279
  - The render loop should pause while the race canvas is offscreen or the document is hidden, then resume without catching up the elapsed hidden time. Layout measurements needed for overlay camera safe areas should be cached between resize/layout invalidations. Runtime DOM updates should skip unchanged text/markup so visible embeds do not rewrite stable readouts every frame.
231
280
  - Restart and rerender paths must destroy replaced PixiJS display children while preserving shared loaded textures.
232
281
  - Restart supports race/data/seed changes but does not support asset URL changes or browser expert mode changes. Texture loading and ticker ownership are mount-time boundaries; hosts must destroy and mount again to change either boundary.
233
- - Expert environment code can create a headless `createPaddockEnvironment()` from the `@inventure71/paddockjs/environment` subpath. It requires explicit `controlledDrivers`, accepts normalized actions `{ steering, throttle, brake }` plus optional `pitIntent` and `pitCompound`, advances only through `step(actions)`, and returns environment-loop JavaScript results with `observation`, `reward`, `metrics`, `terminated`, `truncated`, `done`, `events`, `state`, and `info`. `steering` is an absolute normalized steering target: `-1` points at maximum left, `0` points at center, `1` points at maximum right, and intermediate values are percentages of the maximum steering angle; the vehicle integrator rate-limits motion toward that target instead of snapping the wheel. `steering`, `throttle`, and `brake` are required for each controlled-driver action. Strict action policy throws on missing or non-finite vehicle controls, while report policy records the error and releases stale manual controls so the built-in driver owns that physics step. `info.drivers[driverId]` exposes per-driver `terminated`, `truncated`, `endReason`, `episodeStep`, and `episodeId` for batched loops; selected-driver resets clear only selected driver truncation/termination and do not rewind global `info.step`. Controlled drivers do not receive tire-threshold automatic pit calls; they request automatic pit service through `pitIntent`, may choose the target tire through `pitCompound`, and observe pit-lane/service/race-control state in `observation[driverId].object.self` and `.race`. `pitIntent: 0` is a no-op clear request and does not fail when pit stops are disabled. `self.onTrack` follows wheel-level legality, so track, kerb, and legal pit-lane/box surfaces are on-track while gravel/grass/barrier are off-track. Barrier-wall contact in both physics modes is measured against the rendered wall's inner face; contact marks the car destroyed/DNF, removes it from active collision/sensor participation, emits a `car-destroyed` event, and terminates that driver's episode with `endReason: 'destroyed'`. Opt-in stalled off-track DNF removes the car from the same active participation systems while keeping `destroyed: false` and terminates controlled-driver episodes with `endReason: 'stalled-off-track'` when `rules.modules.stalledDnf.enabled` is `true`. Reset is still an episode-boundary `resetDrivers()` call. Environment scenarios may set reset positions with `preset`, `placements`, and relative `traffic`; `resetDrivers(placements)` may reset selected controlled drivers between episodes without recreating the whole simulation. Partial `reset(options)` calls preserve omitted nested option groups while replacing arrays and explicit scenario placement maps. After placement, selected cars are classified by the same runoff/barrier rules before observations are returned, so cars already inside terminal barrier space report destroyed metrics and stable miss-valued rays instead of doing alive-car far-out ray scans. Both scenario placement and `resetDrivers()` are setup data, not policy actions. `participantInteractions` may make real physics cars non-colliding, sensor-hidden, non-blocking for pit occupancy, or excluded from race order, but those cars still live in `snapshot.cars` and move only through normal physics/control APIs. The `batch-training` profile is non-colliding, sensor-hidden, pit-non-blocking, excluded from race order, and still rendered. `replayGhosts` are separate trajectory-driven entities in `snapshot.replayGhosts`; they are visual/reference overlays and never enter car physics, timing, pit, order, or penalty systems. Replay ghosts are sensor-hidden unless their own sensor flags explicitly opt them into ray or nearby observations.
282
+ - Expert environment code can create a headless `createPaddockEnvironment()` from the `@inventure71/paddockjs/environment` subpath. It requires explicit `controlledDrivers`, accepts normalized actions `{ steering, throttle, brake }` plus optional `pitIntent` and `pitCompound`, advances only through `step(actions)`, and returns environment-loop JavaScript results with `observation`, `reward`, `metrics`, `terminated`, `truncated`, `done`, `events`, `state`, and `info`. `steering` is an absolute normalized steering target: `-1` points at maximum left, `0` points at center, `1` points at maximum right, and intermediate values are percentages of the maximum steering angle; the vehicle integrator rate-limits motion toward that target instead of snapping the wheel. `steering`, `throttle`, and `brake` are required for each controlled-driver action. Strict action policy throws on missing or non-finite vehicle controls, while report policy records the error and releases stale manual controls so the built-in driver owns that physics step. `info.drivers[driverId]` exposes per-driver `terminated`, `truncated`, `endReason`, `episodeStep`, and `episodeId` for batched loops; selected-driver resets clear only selected driver truncation/termination and do not rewind global `info.step`. For scoped reset results, `info.controlledDrivers` matches the drivers present in `observation` and `metrics`, while `info.drivers` preserves episode state for all configured controlled drivers. Controlled drivers do not receive tire-threshold automatic pit calls; they request automatic pit service through `pitIntent`, may choose the target tire through `pitCompound`, and observe pit-lane/service/race-control state in `observation[driverId].object.self` and `.race`. `pitIntent: 0` is a no-op clear request and does not fail when pit stops are disabled. `self.onTrack` follows wheel-level legality, so track, kerb, and legal pit-lane/box surfaces are on-track while gravel/grass/barrier are off-track. Barrier-wall contact in both physics modes is measured against the rendered wall's inner face; contact marks the car destroyed/DNF, removes it from active collision/sensor participation, emits a `car-destroyed` event, and terminates that driver's episode with `endReason: 'destroyed'`. Opt-in stalled off-track DNF removes the car from the same active participation systems while keeping `destroyed: false` and terminates controlled-driver episodes with `endReason: 'stalled-off-track'` when `rules.modules.stalledDnf.enabled` is `true`. Reset is still an episode-boundary `resetDrivers()` call. Environment scenarios may set reset positions with `preset`, `placements`, and relative `traffic`; `resetDrivers(placements)` may reset selected controlled drivers between episodes without recreating the whole simulation. Partial `reset(options)` calls preserve omitted nested option groups while replacing arrays and explicit scenario placement maps. After placement, selected cars are classified by the same runoff/barrier rules before observations are returned, so cars already inside terminal barrier space report destroyed metrics and stable miss-valued rays instead of doing alive-car far-out ray scans. Both scenario placement and `resetDrivers()` are setup data, not policy actions. `participantInteractions` may make real physics cars non-colliding, sensor-hidden, non-blocking for pit occupancy, or excluded from race order, but those cars still live in `snapshot.cars` and move only through normal physics/control APIs. The `batch-training` profile is non-colliding, sensor-hidden, pit-non-blocking, excluded from race order, and still rendered. `replayGhosts` are separate trajectory-driven entities in `snapshot.replayGhosts`; they are visual/reference overlays and never enter car physics, timing, pit, order, or penalty systems. Replay ghosts are sensor-hidden unless their own sensor flags explicitly opt them into ray or nearby observations.
234
283
  - Headless environments also accept an optional `externalRenderer` observer hook. It receives `{ snapshot, observation, meta }` on `reset`, `step`, and `resetDrivers`, so external tooling can stream authoritative frames to visualizers without letting that transport mutate simulation state.
235
284
  - Controller loop code should use `createPaddockDriverControllerLoop()` instead of duplicating browser/headless stepping. Its controller context groups observations by controlled driver id, includes `orderedObservations` for direct batch tensor assembly, exposes metrics, events, cached specs, and reset-driver ids, and keeps per-driver resets as controller reset hooks when the runtime supports `resetDrivers()`. `previousActions` is the last action map actually applied before the current `actions`; on the first controlled frame after reset it is empty, and after repeated frames it reflects the prior physics step rather than pre-copying the current decision.
236
- - Expert ray sensors originate from the controlled car center. The default compact set is `[-135, -60, -20, 0, 20, 60, 135, 180]`, giving forward, side, and rear awareness while staying small. Rays detect track edges against the actual track geometry and detect car hits by ray-to-car-footprint intersection. Hosts may opt into per-ray lengths, predefined layouts such as `driver-front-heavy`, and surface-aware channels for kerb and illegal surface detection; those channels are computed only when requested and use analytic acceleration plus indexed ray-boundary intersections across the legal surface and nearby off-track recovery band before falling back to indexed sampled geometry where needed. Batch-training ray runs keep the same driver-facing road-edge, kerb, and illegal-surface distances by using the indexed geometry path rather than a local strip approximation. Barrier walls are rendered/physical terminal boundaries, not model-facing ray targets; active ray objects, vectors, schemas, and visualizations must not expose a `barrier` ray channel. Ray `precision` defaults to `driver`, which is the active model-facing sensor contract; `debug` precision is only for explicitly labeled diagnostics. `observation.profile: 'physical-driver'` exposes local driver-like yaw, contact-patch, boundary, richer ray, and opponent-radar senses while omitting default lookahead samples unless explicitly configured. `observation.output` can be `full`, `vector`, or `object`, `includeSchema: false` omits repeated schema payloads, and `vectorType: 'float32'` returns typed vector buffers for high-throughput JavaScript loops. `result.stateOutput` can be `full`, `minimal`, or `none`; `none` returns `state: null` and is intended for loops that rely on observations, metrics, and info instead of full snapshots.
285
+ - Expert ray sensors originate from the controlled car center. The default compact set is `[-135, -60, -20, 0, 20, 60, 135, 180]`, giving forward, side, and rear awareness while staying small. Rays detect track edges against the actual track geometry and detect car hits by ray-to-car-footprint intersection. Hosts may opt into per-ray lengths, predefined layouts such as `driver-front-heavy`, and surface-aware channels for kerb and illegal surface detection; those channels are computed only when requested. For normal driver interactions, `precision: 'driver'` road-edge, kerb, and illegal-surface channels keep the sampled model-facing distances whether the track query index is enabled or disabled. The `batch-training` participant profile keeps its indexed recovery-ray path as its high-throughput training contract; other indexed ray-boundary shortcuts are reserved for non-driver/debug diagnostics. Barrier walls are rendered/physical terminal boundaries, not model-facing ray targets; active ray objects, vectors, schemas, and visualizations must not expose a `barrier` ray channel. Ray `precision` defaults to `driver`, which is the active model-facing sensor contract; `debug` precision is only for explicitly labeled diagnostics. `observation.profile: 'physical-driver'` exposes local driver-like yaw, contact-patch, boundary, richer ray, and opponent-radar senses while omitting default lookahead samples unless explicitly configured. `observation.output` can be `full`, `vector`, or `object`, `includeSchema: false` omits repeated schema payloads, and `vectorType: 'float32'` returns typed vector buffers for high-throughput JavaScript loops. `result.stateOutput` can be `full`, `minimal`, or `none`; `none` returns `state: null` and is intended for loops that rely on observations, metrics, and info instead of full snapshots.
237
286
  - Track surface bands are single-source geometry. Rendering derives visible gravel, runoff, and barrier offsets from the same track widths used by wheel surfaces and ray surface hits: road edge, kerb outer edge, gravel outer edge, runoff outer edge, and barrier wall width. New code must not add independent magic offsets for these bands.
238
287
  - Browser expert mode is opt-in with `expert: { enabled: true, controlledDrivers, frameSkip }`. When enabled, the returned controller exposes `expert.reset()`, `expert.step(actions)`, `expert.getObservation()`, and `expert.getState()`.
239
288
  - Browser expert mode also exposes `attachExternalRenderer(source)`, `detachExternalRenderer()`, and `getExternalRendererState()` for render-only external-frame playback. The `source` callback contract is `subscribe(onFrame) => unsubscribe`, where each frame is `{ snapshot, observation, meta? }`.
@@ -249,8 +298,8 @@ Returned controller:
249
298
  - Race canvas rendered with PixiJS.
250
299
  - Procedural track rendering with asphalt texture and DRS overlays.
251
300
  - Driver selection from cars and timing tower rows.
252
- - Camera modes: overview, leader, selected, show all.
253
- Overview is a closer static circuit view centered on the world; show all is the mode that dynamically fits the active pack.
301
+ - Camera modes: overview, leader, selected, show all, and pit.
302
+ Overview frames the active generated track bounds with package-owned padding and pit-lane extent; show all dynamically fits the active pack; pit frames the operational pit-lane work area when the track has one.
254
303
  - Zoom controls.
255
304
  - FPS readout.
256
305
  - Start lights.
package/docs/training.md CHANGED
@@ -56,7 +56,7 @@ The current expert API is a JavaScript environment contract. It supports:
56
56
 
57
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
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:
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
60
 
61
61
  ```js
62
62
  const env = createPaddockEnvironment({
@@ -144,7 +144,7 @@ Controlled drivers do not receive tire-threshold automatic pit calls from the bu
144
144
 
145
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
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.
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
148
 
149
149
  `observation.object.self.appliedControls` is diagnostic/reference data for the
150
150
  latest physics step. For controlled cars it mirrors the normalized action after
@@ -183,9 +183,13 @@ Only requested ray channels are computed. The default compact config still compu
183
183
 
184
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
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.
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
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.
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.
189
193
 
190
194
  ## Headless Training Loop
191
195
 
@@ -263,7 +267,7 @@ In the browser renderer, no-collision participants still look like solid cars, b
263
267
 
264
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'`.
265
269
 
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.
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.
267
271
 
268
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:
269
273
 
@@ -281,7 +285,7 @@ participantInteractions: {
281
285
 
282
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.
283
287
 
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.
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.
285
289
 
286
290
  Use `result.stateOutput` to avoid returning more state than the loop needs:
287
291
 
@@ -298,7 +302,9 @@ const env = createPaddockEnvironment({
298
302
  });
299
303
  ```
300
304
 
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.
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.
302
308
 
303
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:
304
310
 
@@ -362,7 +368,7 @@ env.resetDrivers({
362
368
  });
363
369
  ```
364
370
 
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.
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.
366
372
 
367
373
  The worker protocol mirrors this with a `resetDrivers` message:
368
374
 
@@ -396,7 +402,7 @@ const next = env.step(action);
396
402
  recorder.recordStep(result, action, next);
397
403
  ```
398
404
 
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.
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.
400
406
 
401
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`.
402
408
 
@@ -404,7 +410,7 @@ Deterministic evaluation helpers report simulator quality metrics such as distan
404
410
 
405
411
  ## Example Rewards
406
412
 
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.
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.
408
414
 
409
415
  ## Visual Playback Loop
410
416
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inventure71/paddockjs",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "type": "module",
5
5
  "description": "A data-driven browser F1 simulator component for host websites.",
6
6
  "license": "Apache-2.0",
@@ -18,6 +18,7 @@ import {
18
18
  createTimingTowerMarkup,
19
19
  } from '../ui/componentTemplates.js';
20
20
  import { applyPaddockThemeCssVariables, resolveF1SimulatorOptions } from '../config/defaultOptions.js';
21
+ import { mergeRestartOptions } from '../config/restartOptions.js';
21
22
 
22
23
  function assertMountTarget(root, label) {
23
24
  if (!root || typeof root !== 'object' || !('innerHTML' in root)) {
@@ -31,38 +32,6 @@ function setPackageCssVariables(root, assets, theme) {
31
32
  applyPaddockThemeCssVariables(root, theme);
32
33
  }
33
34
 
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
35
  function createCompositeRoot(getRoots, getOptions) {
67
36
  return {
68
37
  style: {
@@ -203,7 +172,7 @@ export class PaddockSimulatorController {
203
172
  }
204
173
 
205
174
  restart(nextOptions = {}) {
206
- const nextResolvedOptions = resolveF1SimulatorOptions(mergeResolvedOptions(this.options, nextOptions));
175
+ const nextResolvedOptions = resolveF1SimulatorOptions(mergeRestartOptions(this.options, nextOptions));
207
176
  this.app?.restart(nextResolvedOptions);
208
177
  this.options = nextResolvedOptions;
209
178
  this.compositeRoot.applyCssVariables();