@inventure71/paddockjs 0.1.0 → 0.3.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 (40) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/INSTALL_AND_UPDATE.md +163 -0
  3. package/README.md +96 -16
  4. package/docs/architecture.md +55 -11
  5. package/docs/concepts.md +5 -1
  6. package/docs/data_contract.md +324 -8
  7. package/docs/data_model.md +1 -0
  8. package/docs/index.md +1 -0
  9. package/docs/rules.md +2 -0
  10. package/docs/system_specs.md +66 -11
  11. package/docs/training.md +104 -0
  12. package/examples/train-basic-policy.mjs +277 -0
  13. package/package.json +14 -7
  14. package/src/api/PaddockSimulatorController.js +64 -3
  15. package/src/app/BrowserExpertAdapter.js +32 -0
  16. package/src/app/F1SimulatorApp.js +308 -38
  17. package/src/app/domBindings.js +35 -11
  18. package/src/config/defaultOptions.js +33 -0
  19. package/src/data/championship.js +13 -0
  20. package/src/data/normalizeDrivers.js +7 -2
  21. package/src/environment/actions.js +45 -0
  22. package/src/environment/episode.js +17 -0
  23. package/src/environment/events.js +17 -0
  24. package/src/environment/index.d.ts +241 -0
  25. package/src/environment/index.js +2 -0
  26. package/src/environment/observations.js +112 -0
  27. package/src/environment/options.js +150 -0
  28. package/src/environment/rewards.js +69 -0
  29. package/src/environment/runtime.js +140 -0
  30. package/src/environment/sensors.js +257 -0
  31. package/src/environment/specs.js +79 -0
  32. package/src/index.d.ts +97 -4
  33. package/src/index.js +12 -2
  34. package/src/rendering/proceduralTrackAsset.js +7 -1
  35. package/src/simulation/raceSimulation.js +214 -1
  36. package/src/simulation/trackModel.js +21 -0
  37. package/src/styles.css +495 -2
  38. package/src/ui/componentTemplates.js +201 -4
  39. package/AGENTS.md +0 -86
  40. package/src/standalone.js +0 -15
@@ -67,15 +67,282 @@ simulator.mountCameraControls(cameraControlsRoot);
67
67
  simulator.mountSafetyCarControl(safetyCarRoot);
68
68
  simulator.mountTimingTower(timingRoot);
69
69
  simulator.mountTelemetryPanel(telemetryRoot);
70
+ simulator.mountTelemetryCore(coreTelemetryRoot);
71
+ simulator.mountTelemetrySectors(sectorGraphRoot);
72
+ simulator.mountTelemetrySectorBanner(sectorBannerRoot);
73
+ simulator.mountTelemetryLapTimes(lapTimesRoot);
74
+ simulator.mountTelemetrySectorTimes(sectorTimesRoot);
75
+ simulator.mountRaceTelemetryDrawer(raceWorkbenchRoot, {
76
+ timingTowerVerticalFit: 'expand-race-view',
77
+ raceDataTelemetryDetail: true,
78
+ });
70
79
  simulator.mountCarDriverOverview(overviewRoot);
71
80
  simulator.mountRaceDataPanel(raceDataRoot);
72
81
  ```
73
82
 
83
+ ## Expert Environment Contract
84
+
85
+ Headless training code imports the browser-free environment subpath:
86
+
87
+ ```js
88
+ import { createPaddockEnvironment, createProgressReward } from '@inventure71/paddockjs/environment';
89
+
90
+ const env = createPaddockEnvironment({
91
+ drivers,
92
+ entries,
93
+ controlledDrivers: ['budget'],
94
+ seed: 71,
95
+ trackSeed: 2026,
96
+ totalLaps: 3,
97
+ frameSkip: 2,
98
+ scenario: {
99
+ participants: 'all',
100
+ nonControlled: 'ai',
101
+ },
102
+ reward: createProgressReward(),
103
+ });
104
+
105
+ let result = env.reset();
106
+ result = env.step({
107
+ budget: { steering: 0, throttle: 1, brake: 0 },
108
+ });
109
+ ```
110
+
111
+ `controlledDrivers` is required. It supports one or many externally controlled cars. Non-controlled participants use the built-in driver AI in the first `0.3.0` environment slice.
112
+
113
+ First-slice scenario support:
114
+
115
+ ```js
116
+ scenario: {
117
+ participants: 'all' | 'controlled-only' | ['budget', 'alpha'],
118
+ nonControlled: 'ai',
119
+ }
120
+ ```
121
+
122
+ Static obstacles, custom placements, ghost cars, debug mutation, assisted controls, and Python Gymnasium wrappers are intentionally deferred.
123
+
124
+ Actions use normalized low-level controls:
125
+
126
+ ```js
127
+ {
128
+ budget: {
129
+ steering: -1, // full left
130
+ throttle: 1,
131
+ brake: 0,
132
+ },
133
+ }
134
+ ```
135
+
136
+ `steering` maps to the simulator's internal steering limit. `throttle` and `brake` are clamped from `0` to `1`.
137
+
138
+ The recommended policy convention is `policy.predict(driverObservation) -> { steering, throttle, brake }`. This is a convention, not a base class. Users can wrap any model or algorithm behind that shape.
139
+
140
+ The environment exposes specs for external training code:
141
+
142
+ ```js
143
+ const actionSpec = env.getActionSpec();
144
+ const observationSpec = env.getObservationSpec();
145
+ ```
146
+
147
+ `actionSpec` describes controlled drivers and normalized action ranges. `observationSpec` describes object observation fields, ray layout, nearby-car limits, and vector schema.
148
+
149
+ `createProgressReward()` is a starter reward helper, not the only reward contract. It returns a callback compatible with `reward(context)` and combines:
150
+
151
+ - forward progress in meters from `state.snapshot.cars[].distanceMeters`
152
+ - on-track speed in `current.object.self.speedKph`
153
+ - off-track penalty from `current.object.self.onTrack`
154
+ - collision penalty from global/per-driver step events
155
+ - steering and brake smoothness penalties from the submitted normalized action
156
+
157
+ Weights are explicit and overridable:
158
+
159
+ ```js
160
+ const reward = createProgressReward({
161
+ weights: {
162
+ progress: 1,
163
+ speed: 0.01,
164
+ offTrack: -2,
165
+ collision: -10,
166
+ steering: -0.03,
167
+ brake: -0.02,
168
+ },
169
+ });
170
+ ```
171
+
172
+ Custom reward functions receive the same context:
173
+
174
+ ```js
175
+ reward({ driverId, previous, current, action, events, state }) {
176
+ return current.object.self.speedKph / 100;
177
+ }
178
+ ```
179
+
180
+ `step(actions)` returns a JavaScript object designed for environment loops:
181
+
182
+ ```js
183
+ {
184
+ observation: {
185
+ budget: {
186
+ object: {
187
+ self: {
188
+ speedKph,
189
+ speedMetersPerSecond,
190
+ headingRadians,
191
+ steeringAngleRadians,
192
+ throttle,
193
+ brake,
194
+ lap,
195
+ completedLaps,
196
+ lapProgressMeters,
197
+ trackOffsetMeters,
198
+ trackHeadingErrorRadians,
199
+ onTrack,
200
+ surface,
201
+ tireEnergy,
202
+ },
203
+ race: {
204
+ position,
205
+ totalCars,
206
+ raceMode,
207
+ totalLaps,
208
+ },
209
+ rays,
210
+ nearbyCars,
211
+ events,
212
+ },
213
+ vector,
214
+ schema,
215
+ events,
216
+ },
217
+ },
218
+ reward,
219
+ terminated,
220
+ truncated,
221
+ done,
222
+ events,
223
+ state: {
224
+ snapshot,
225
+ },
226
+ info: {
227
+ step,
228
+ elapsedSeconds,
229
+ seed,
230
+ trackSeed,
231
+ controlledDrivers,
232
+ actionErrors,
233
+ endReason,
234
+ },
235
+ }
236
+ ```
237
+
238
+ Observation objects use physical units such as kph, meters/second, meters, and radians. Optional `vector` values use fixed documented scaling from `schema`; they do not use hidden per-car normalization. Full simulator truth remains available under `state.snapshot`.
239
+
240
+ Default rays use a compact center-origin set with forward, side, and rear awareness:
241
+
242
+ ```js
243
+ [-135, -60, -20, 0, 20, 60, 135, 180]
244
+ ```
245
+
246
+ Track distances are sampled against the actual track model. When the ray starts inside the track border, `track.kind: 'exit'` means the distance is where the ray leaves the valid road. When the ray starts outside the track border, `track.kind: 'entry'` means the distance is where the ray re-enters the valid road. If no track transition is visible within the ray length, `track.hit` is `false`, `track.distanceMeters` equals `lengthMeters`, and `track.kind` is `null`. Car hits require the ray to intersect the other car's footprint and also return max distance when no car is visible:
247
+
248
+ ```js
249
+ {
250
+ angleDegrees,
251
+ angleRadians,
252
+ lengthMeters,
253
+ track: {
254
+ hit,
255
+ distanceMeters,
256
+ kind, // 'exit' | 'entry' | null
257
+ },
258
+ car: {
259
+ hit,
260
+ distanceMeters,
261
+ driverId,
262
+ relativeSpeedKph,
263
+ },
264
+ }
265
+ ```
266
+
267
+ Nearby-car observations are car-relative:
268
+
269
+ ```js
270
+ {
271
+ id,
272
+ relativeForwardMeters,
273
+ relativeRightMeters,
274
+ relativeDistanceMeters,
275
+ relativeSpeedKph,
276
+ relativeHeadingRadians,
277
+ ahead,
278
+ sameLap,
279
+ }
280
+ ```
281
+
282
+ If no `reward` callback is provided, `result.reward` is `null`. If provided, rewards are returned by controlled driver ID.
283
+
284
+ Browser expert mode is opt-in through the browser mount API:
285
+
286
+ ```js
287
+ const simulator = await mountF1Simulator(root, {
288
+ drivers,
289
+ entries,
290
+ expert: {
291
+ enabled: true,
292
+ controlledDrivers: ['budget'],
293
+ frameSkip: 4,
294
+ visualizeSensors: {
295
+ rays: true,
296
+ },
297
+ },
298
+ });
299
+
300
+ simulator.expert.reset();
301
+ simulator.expert.step({
302
+ budget: { steering: 0, throttle: 1, brake: 0 },
303
+ });
304
+ ```
305
+
306
+ When browser expert mode is enabled, automatic ticker simulation advancement is disabled. The visual canvas advances only when host code calls `simulator.expert.reset()` or `simulator.expert.step(actions)`. Browser expert mode is a mount-time option; `restart(nextOptions)` rejects `expert` changes. Destroy and mount a new simulator to switch between built-in ticker control and expert stepping.
307
+
308
+ `expert.visualizeSensors` is a browser-only visual debugging option. When `visualizeSensors: true` or `visualizeSensors: { rays: true }` is set, the simulator draws the controlled drivers' ray sensors in the Pixi world layer from the cars' current positions. Ray lengths and hit markers are based on the same observation data returned from the expert environment result.
309
+
310
+ ## Lap Telemetry Snapshot
311
+
312
+ Each `car.lapTelemetry` snapshot includes current/last/best lap and sector timing, plus sector performance classes used by package UI:
313
+
314
+ ```js
315
+ {
316
+ currentLap,
317
+ currentSector,
318
+ currentLapTime,
319
+ currentSectorElapsed,
320
+ currentSectorProgress,
321
+ currentSectors,
322
+ lastLapTime,
323
+ bestLapTime,
324
+ lastSectors,
325
+ bestSectors,
326
+ completedLaps,
327
+ sectorPerformance: {
328
+ current: ['overall-best', 'personal-best', null],
329
+ last: [null, 'slower', null],
330
+ best: ['overall-best', null, 'personal-best'],
331
+ },
332
+ }
333
+ ```
334
+
335
+ `overall-best` means fastest sector time currently known across the field, `personal-best` means that driver's own fastest non-overall sector, and `slower` marks a completed sector that is slower than the driver's personal best. Missing or in-progress sector values are `null`.
336
+
74
337
  ## Required Options
75
338
 
76
- `drivers` is required and must be a non-empty array.
339
+ `drivers` is required and must be a non-empty array. Driver `id` values must be unique.
340
+
341
+ `totalLaps` is optional. Values are normalized to a finite positive integer, with invalid or non-positive input falling back to a one-lap race.
77
342
 
78
- `trackSeed` is optional. If omitted, each mounted browser simulator creates a fresh procedural circuit. Passing a `trackSeed` makes the track deterministic; repeated procedural seeds are cached so multiple mounts can reuse the same generated track definition.
343
+ `trackSeed` is optional. If omitted, each mounted browser simulator creates a fresh procedural circuit. Passing a `trackSeed` makes the track deterministic; repeated procedural seeds are cached so multiple mounts can reuse the same generated track definition. Calling `restart({ trackSeed })` rebuilds the simulation on the deterministic circuit for that seed.
344
+
345
+ `initialCameraMode` is optional and accepts `'overview'`, `'leader'`, `'selected'`, or `'show-all'`. Invalid values fall back to `'leader'`.
79
346
 
80
347
  Each driver must have:
81
348
 
@@ -161,7 +428,7 @@ Entries are optional. If omitted, defaults are used.
161
428
  }
162
429
  ```
163
430
 
164
- Entries match drivers by `driverId`.
431
+ Entries match drivers by `driverId`. Entry `driverId` values must be unique. `driverNumber` is optional; if omitted, PaddockJS falls back to stable grid order. When `driverNumber` is provided, values must be unique.
165
432
  The car/driver overview primarily renders the existing driver and vehicle rating components from `driver` and `vehicle`. `team` is optional team-level metadata for race identity and future pit behavior; `color` defaults to the driver/car color, and `icon` defaults from the team name or timing code. The timing tower uses the team icon in the car/team column. `driver.customFields`, `vehicle.customFields`, and top-level driver `customFields` are accepted as extra metadata after those defined components.
166
433
 
167
434
  ## Rating Rules
@@ -253,12 +520,20 @@ ui: {
253
520
  showFps: true,
254
521
  showTimingTower: true,
255
522
  showTelemetry: true,
523
+ telemetryIncludesOverview: true,
524
+ telemetryModules: {
525
+ core: true,
526
+ sectors: true,
527
+ lapTimes: true,
528
+ sectorTimes: true,
529
+ },
256
530
  showRaceDataPanel: true,
257
531
  raceDataBanners: {
258
532
  initial: 'project',
259
533
  enabled: ['project', 'radio'],
260
534
  },
261
535
  raceDataBannerSize: 'custom',
536
+ raceDataTelemetryDetail: false,
262
537
  timingTowerVerticalFit: 'expand-race-view',
263
538
  }
264
539
  ```
@@ -268,12 +543,15 @@ ui: {
268
543
  - `showFps`: controls whether the race canvas renders the FPS readout.
269
544
  - `showRaceDataPanel`: controls whether the precombined shell includes the project/radio lower-third inside the race window.
270
545
  - `showTimingTower`, `showTelemetry`: reserved component visibility flags for host layout decisions.
546
+ - `telemetryIncludesOverview`: controls whether the telemetry stack template embeds the car/driver overview. Composable hosts can also pass `mountTelemetryPanel(root, { includeOverview: false })`.
547
+ - `telemetryModules`: controls optional telemetry surfaces inside stack/drawer templates. The default object enables `core` scalar readouts, `sectors` progress bars, `lapTimes`, and `sectorTimes`. It can also be `false` to disable all telemetry modules, or an array such as `['sectors', 'lapTimes']` to render only named modules. These modules are also individually mountable with `mountTelemetryCore`, `mountTelemetrySectors`, `mountTelemetryLapTimes`, and `mountTelemetrySectorTimes`.
271
548
  - `raceDataBanners.initial`: `'project'`, `'radio'`, or `'hidden'`. This controls which lower-third appears first in the precombined shell.
272
549
  - `raceDataBanners.enabled`: array containing `'project'` and/or `'radio'`. Disabled banner types never appear, including after driver selection.
273
550
  - `raceDataBannerSize`: `'custom'` preserves the default lower-third geometry and exposes package CSS variables for host tuning. `'auto'` uses the race space to the right of the timing board when there is enough room and falls back to full lower-third overlap when there is not.
551
+ - `raceDataTelemetryDetail`: when `true`, the project lower-third includes compact S1/S2/S3 sector progress and timing readouts. Radio mode keeps the normal quote layout. The separate `mountTelemetrySectorBanner()` component remains available for hosts that explicitly want an independent sector banner.
274
552
  - `timingTowerVerticalFit`: `'expand-race-view'` lets the combined race window grow to contain the timing tower. `'scroll'` keeps the race window height and scrolls the timing list inside the cropped tower. The same values can be passed to `mountRaceCanvas(root, { includeTimingTower: true, timingTowerVerticalFit })` for an embedded composable timing tower.
275
553
 
276
- No UI option exists for raw timing-tower width, max width, or horizontal ratio. The timing tower is capped by the package CSS variable `--timing-board-max-width` because very wide timing boards read poorly. Host pages can scale the whole simulator by changing the mount container, but package-owned layout presets keep their internal proportions inside PaddockJS. For standalone timing towers, give the mount root a fixed height when a fixed vertical footprint is needed; the package keeps the frame inside that height and scrolls only the timing entries.
554
+ No UI option exists for raw timing-tower width, max width, or horizontal ratio. The timing tower is capped by the package CSS variable `--timing-board-max-width` because very wide timing boards read poorly. Host pages can scale the whole simulator by changing the mount container, but package-owned layout presets keep their internal proportions inside PaddockJS. For standalone timing towers, give the mount root a fixed height when a fixed vertical footprint is needed; the package keeps the frame inside that height and scrolls only the timing entries. Narrow hosts are handled internally: side-gutter timing towers become stacked/full-width, embedded timing towers stop behaving like desktop side overlays, and the camera safe area stops reserving a left gutter when the measured timing board is effectively full-width.
277
555
 
278
556
  ## Theme And Sizing Contract
279
557
 
@@ -315,7 +593,39 @@ After every car completes `totalLaps`, `getSnapshot()` returns:
315
593
  }
316
594
  ```
317
595
 
318
- Cars also include `team`, `speedKph`, `distanceMeters`, `gapAheadMeters`, `gapAheadSeconds`, `intervalAheadSeconds`, `leaderGapSeconds`, `finished`, `finishTime`, and `classifiedRank`. `gapAheadSeconds` and `intervalAheadSeconds` are the interval to the car directly ahead. `leaderGapSeconds` is the cumulative gap to P1. The first car to finish sets `raceControl.winner` and receives a `car-finish` event, but the race keeps running until all cars finish. After final classification, the field continues under safety-car behavior.
596
+ Cars also include `team`, `speedKph`, `distanceMeters`, `gapAheadMeters`, `gapAheadSeconds`, `intervalAheadSeconds`, `leaderGapSeconds`, `finished`, `finishTime`, `classifiedRank`, and `lapTelemetry`. `gapAheadSeconds` and `intervalAheadSeconds` are the interval to the car directly ahead. `leaderGapSeconds` is the cumulative gap to P1. The first car to finish sets `raceControl.winner` and receives a `car-finish` event, but the race keeps running until all cars finish. After final classification, the field continues under safety-car behavior.
597
+
598
+ ## Track And Lap Telemetry Snapshot
599
+
600
+ Every built track is automatically divided into three equal sectors. `snapshot.track.sectors` exposes:
601
+
602
+ ```js
603
+ [
604
+ { index: 1, id: 's1', label: 'S1', start, end, startRatio, endRatio, length },
605
+ { index: 2, id: 's2', label: 'S2', start, end, startRatio, endRatio, length },
606
+ { index: 3, id: 's3', label: 'S3', start, end, startRatio, endRatio, length },
607
+ ]
608
+ ```
609
+
610
+ Each car exposes `lapTelemetry`:
611
+
612
+ ```js
613
+ {
614
+ currentLap,
615
+ currentSector,
616
+ currentLapTime,
617
+ currentSectorElapsed,
618
+ currentSectorProgress,
619
+ currentSectors: [s1Time, s2Time, s3Time],
620
+ lastLapTime,
621
+ bestLapTime,
622
+ lastSectors: [s1Time, s2Time, s3Time],
623
+ bestSectors: [s1Time, s2Time, s3Time],
624
+ completedLaps,
625
+ }
626
+ ```
627
+
628
+ Times are seconds or `null` when no timing exists yet. `currentSectorProgress` is `0..1` within the active sector.
319
629
 
320
630
  ## Unit Conversion
321
631
 
@@ -324,6 +634,7 @@ The simulation keeps its internal physics in simulator units. Public speed and d
324
634
  - `simUnitsToMeters(simUnits)`
325
635
  - `metersToSimUnits(meters)`
326
636
  - `simSpeedToKph(simUnitsPerSecond)`
637
+ - `simSpeedToMetersPerSecond(simUnitsPerSecond)`
327
638
  - `kphToSimSpeed(kph)`
328
639
 
329
640
  The current calibrated speed scale maps `VEHICLE_LIMITS.maxSpeed` to an F1-like `330 km/h`. Rendered car sprite size remains a visual scale and is intentionally larger than physical car length for readability.
@@ -337,7 +648,7 @@ const simulator = await mountF1Simulator(root, options);
337
648
  Controller methods:
338
649
 
339
650
  - `destroy()`: removes listeners, destroys PixiJS runtime, clears the host root.
340
- - `restart(nextOptions)`: restarts the simulation with merged options.
651
+ - `restart(nextOptions)`: restarts the simulation with merged non-asset options. Use `destroy()` and mount a new simulator to change bundled or host-provided asset URLs.
341
652
  - `selectDriver(driverId)`: selects and focuses a driver.
342
653
  - `setSafetyCarDeployed(deployed)`: toggles safety car state.
343
654
  - `callSafetyCar()`: deploys the safety car.
@@ -351,8 +662,13 @@ Composable controllers additionally expose:
351
662
  - `mountCameraControls(root)`: renders package-owned camera mode and zoom controls outside the race canvas.
352
663
  - `mountSafetyCarControl(root)`: renders a package-owned safety-car button that binds to the same race-control state as other safety buttons.
353
664
  - `mountTimingTower(root)`: renders the timing tower component.
354
- - `mountRaceCanvas(root, { includeRaceDataPanel, includeTimingTower, timingTowerVerticalFit })`: renders the PixiJS canvas host, optional FPS, start lights, and optionally embedded camera controls. Pass `includeRaceDataPanel: true` to place the project/radio lower-third inside the race window so it shares race-canvas clipping and layering. Pass `includeTimingTower: true` to place the timing tower inside the race canvas; `timingTowerVerticalFit: 'expand-race-view'` grows the canvas to the tower height, while `'scroll'` keeps the canvas height and scrolls timing rows inside the tower frame. This is required before `start()`.
355
- - `mountTelemetryPanel(root, { includeOverview })`: renders selected-car text telemetry. It includes the car/driver overview by default unless `includeOverview: false` is passed or `ui.telemetryIncludesOverview` is `false`.
665
+ - `mountRaceCanvas(root, { includeRaceDataPanel, includeTimingTower, includeTelemetrySectorBanner, timingTowerVerticalFit })`: renders the PixiJS canvas host, optional FPS, start lights, and optionally embedded camera controls. Pass `includeRaceDataPanel: true` to place the project/radio lower-third inside the race window so it shares race-canvas clipping and layering. Pass `includeTelemetrySectorBanner: true` only when the host intentionally wants the independent sector lower-third in addition to the project/radio banner. Pass `includeTimingTower: true` to place the timing tower inside the race canvas; `timingTowerVerticalFit: 'expand-race-view'` grows the canvas to the tower height, while `'scroll'` keeps the canvas height and scrolls timing rows inside the tower frame. This is required before `start()`.
666
+ - `mountTelemetryPanel(root, { includeOverview })`: renders the package-owned telemetry stack template. The stack is only a composition of detached telemetry surfaces; it includes the car/driver overview by default unless `includeOverview: false` is passed or `ui.telemetryIncludesOverview` is `false`.
667
+ - `mountTelemetryCore(root)`: renders selected-car scalar telemetry only.
668
+ - `mountTelemetrySectors(root)`: renders the live sector progress graph only.
669
+ - `mountTelemetryLapTimes(root)`: renders current, last, and best lap timing only.
670
+ - `mountTelemetrySectorTimes(root)`: renders last and best sector timing only.
671
+ - `mountRaceTelemetryDrawer(root, { timingTowerVerticalFit, drawerInitiallyOpen, raceDataTelemetryDetail })`: renders a template that combines race canvas, embedded timing tower, the project/radio lower-third, safety-car control, and a right-side telemetry drawer. Pass `raceDataTelemetryDetail: true` when this template should put compact S1/S2/S3 detail in the project lower-third instead of mounting a second sector popup. The drawer uses the detached telemetry components and takes layout space from the race window when opened.
356
672
  - `mountCarDriverOverview(root)`: renders the package-owned car/driver overview as a separate component with a Car/Driver toggle, center visual, and linked stat cells from the existing driver/vehicle rating components.
357
673
  - `mountRaceDataPanel(root)`: renders the project/race-data lower-third as a separate component for hosts that intentionally want it outside the race canvas.
358
674
  - `start()`: initializes PixiJS, binds mounted controls, and starts the simulation loop.
@@ -151,6 +151,7 @@ Team metadata is copied onto the same normalized driver object. `team.color` def
151
151
  simUnitsToMeters(simUnits);
152
152
  metersToSimUnits(meters);
153
153
  simSpeedToKph(simUnitsPerSecond);
154
+ simSpeedToMetersPerSecond(simUnitsPerSecond);
154
155
  kphToSimSpeed(kph);
155
156
  ```
156
157
 
package/docs/index.md CHANGED
@@ -8,6 +8,7 @@ Use these docs as the package source of truth:
8
8
  - [Rules](rules.md): race-control behavior, DRS, safety car, starts, ordering, contact handling, and simulation rules.
9
9
  - [Concepts](concepts.md): vocabulary used by the simulator.
10
10
  - [Data Contract](data_contract.md): shape of host-provided drivers, car pairings, ratings, assets, and callbacks.
11
+ - [Bring Your Own Model](training.md): environment contract, policy convention, and visual playback loop.
11
12
  - [Architecture](architecture.md): module ownership and data/control flow.
12
13
  - [Data Model Notes](data_model.md): lower-level simulator data model notes kept with package docs.
13
14
  - [Learnings](learnings.md): implementation lessons and layout verification rules learned from simulator integration issues.
package/docs/rules.md CHANGED
@@ -59,6 +59,8 @@ The race engine uses simulator units internally. `src/simulation/units.js` conve
59
59
 
60
60
  Lap is computed from each car's cumulative race distance over the track length. Total laps are provided by mount options and default to `10`.
61
61
 
62
+ Each track is automatically divided into three equal sectors. Sector and lap telemetry is recorded when a car's cumulative race distance crosses a sector boundary or start/finish boundary. Timing values are stored in seconds and exposed on each car snapshot as `lapTelemetry`.
63
+
62
64
  Each car is marked `finished` when it reaches `track.length * totalLaps`. The first finisher becomes `winner`, receives classified rank `1`, and emits a `car-finish` event, but the race keeps running until every car has crossed the finish distance.
63
65
 
64
66
  When all cars have finished, the simulator records `finishedAt` and final `classification`, emits a `race-finish` event, freezes order to the classified result, deploys the safety car, and switches race mode to `safety-car`. Finished cars keep circulating under safety-car behavior instead of hard-stopping. The current implementation does not yet implement pit stops, penalties, or championship scoring.
@@ -23,6 +23,18 @@ import {
23
23
  } from '@inventure71/paddockjs';
24
24
  ```
25
25
 
26
+ Headless expert environment import:
27
+
28
+ ```js
29
+ import { createPaddockEnvironment, createProgressReward } from '@inventure71/paddockjs/environment';
30
+ ```
31
+
32
+ The environment subpath is the only public headless training import. It must stay free of DOM, PixiJS, CSS, and browser app dependencies.
33
+ `createProgressReward()` is published from the same subpath as a starter callback for JavaScript training loops. It must remain optional and replaceable; environment stepping must continue to work with a custom `reward(context)` callback or no reward callback.
34
+ 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.
35
+ 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.
36
+ The shared expert runtime exposes `getActionSpec()` and `getObservationSpec()` so external code can inspect controlled drivers, action ranges, sensor layout, nearby-car limits, and vector schema before connecting a model.
37
+
26
38
  All-in-one mount call:
27
39
 
28
40
  ```js
@@ -50,6 +62,7 @@ const simulator = await mountF1Simulator(root, {
50
62
  onRaceEvent,
51
63
  onLapChange,
52
64
  onRaceFinish,
65
+ expert,
53
66
  });
54
67
  ```
55
68
 
@@ -76,6 +89,15 @@ simulator.mountRaceCanvas(canvasRoot, {
76
89
  timingTowerVerticalFit: 'scroll',
77
90
  });
78
91
  simulator.mountTelemetryPanel(telemetryRoot);
92
+ simulator.mountTelemetryCore(coreTelemetryRoot);
93
+ simulator.mountTelemetrySectors(sectorGraphRoot);
94
+ simulator.mountTelemetrySectorBanner(sectorBannerRoot);
95
+ simulator.mountTelemetryLapTimes(lapTimesRoot);
96
+ simulator.mountTelemetrySectorTimes(sectorTimesRoot);
97
+ simulator.mountRaceTelemetryDrawer(raceWorkbenchRoot, {
98
+ timingTowerVerticalFit: 'expand-race-view',
99
+ raceDataTelemetryDetail: true,
100
+ });
79
101
 
80
102
  await simulator.start();
81
103
  ```
@@ -84,6 +106,8 @@ Standalone helper functions are also exported for host code that prefers functio
84
106
 
85
107
  ```js
86
108
  mountRaceControls(root, simulator);
109
+ mountCameraControls(root, simulator);
110
+ mountSafetyCarControl(root, simulator);
87
111
  mountTimingTower(root, simulator);
88
112
  mountRaceCanvas(root, simulator, {
89
113
  includeRaceDataPanel: true,
@@ -91,6 +115,15 @@ mountRaceCanvas(root, simulator, {
91
115
  timingTowerVerticalFit: 'scroll',
92
116
  });
93
117
  mountTelemetryPanel(root, simulator);
118
+ mountTelemetryCore(root, simulator);
119
+ mountTelemetrySectors(root, simulator);
120
+ mountTelemetrySectorBanner(root, simulator);
121
+ mountTelemetryLapTimes(root, simulator);
122
+ mountTelemetrySectorTimes(root, simulator);
123
+ mountRaceTelemetryDrawer(root, simulator, {
124
+ raceDataTelemetryDetail: true,
125
+ });
126
+ mountCarDriverOverview(root, simulator);
94
127
  mountRaceDataPanel(root, simulator);
95
128
  ```
96
129
 
@@ -103,8 +136,14 @@ Returned controller:
103
136
  mountCameraControls(root),
104
137
  mountSafetyCarControl(root),
105
138
  mountTimingTower(root),
106
- mountRaceCanvas(root, { includeRaceDataPanel, includeTimingTower, timingTowerVerticalFit }),
139
+ mountRaceCanvas(root, { includeRaceDataPanel, includeTimingTower, includeTelemetrySectorBanner, timingTowerVerticalFit }),
107
140
  mountTelemetryPanel(root),
141
+ mountTelemetryCore(root),
142
+ mountTelemetrySectors(root),
143
+ mountTelemetrySectorBanner(root),
144
+ mountTelemetryLapTimes(root),
145
+ mountTelemetrySectorTimes(root),
146
+ mountRaceTelemetryDrawer(root, { timingTowerVerticalFit, drawerInitiallyOpen, raceDataTelemetryDetail }),
108
147
  mountRaceDataPanel(root),
109
148
  start(),
110
149
 
@@ -117,6 +156,7 @@ Returned controller:
117
156
  clearSafetyCar(),
118
157
  toggleSafetyCar(),
119
158
  getSnapshot(),
159
+ expert,
120
160
  }
121
161
  ```
122
162
 
@@ -131,29 +171,44 @@ Returned controller:
131
171
  - Timing tower entries are fixed rows stacked from the top of the timing list. Row vertical position must be based on rank/index, never distributed by available height or total entry count.
132
172
  - Timing tower entries display team icons. The tower can switch at runtime between interval-to-car-ahead timing and cumulative gap-to-leader timing.
133
173
  - Snapshots expose calibrated display units through `speedKph`, `distanceMeters`, and `gapMeters`; internal physics remains in simulator units.
174
+ - Snapshots expose automatic three-sector track metadata through `track.sectors` and per-car lap/sector timing through `car.lapTelemetry`, including sector performance classes for overall-best, personal-best, and slower completed sector times.
134
175
  - Entries can include optional `team` metadata. Team color defaults to car color when omitted.
135
176
  - Mounted package surfaces show a package-owned red start-light loading overlay until `start()` finishes PixiJS, asset, control, and initial readout initialization.
136
177
  - `preset` is a preset-first API. Presets are resolved before explicit host overrides so hosts can use `dashboard`, `timing-overlay`, `compact-race`, or `full-dashboard` as a starting point and still override specific `ui` or `theme` fields.
137
178
  - `theme` is the public sizing/color contract. It maps to package CSS variables for `accentColor`, `greenColor`, `yellowColor`, `timingTowerMaxWidth`, and `raceViewMinHeight`.
179
+ - `initialCameraMode` accepts `'overview'`, `'leader'`, `'selected'`, or `'show-all'`; invalid values fall back to `'leader'`.
138
180
  - Camera controls can be embedded in the race canvas, externally mounted, or omitted by `ui.cameraControls`.
139
- - Telemetry can include the car/driver overview directly or stay text-only through `ui.telemetryIncludesOverview` / `mountTelemetryPanel(root, { includeOverview: false })`.
181
+ - Telemetry surfaces are detached package components: core scalar readouts, sector graph, broadcast sector banner, lap-time table, and sector-time table. The broadcast sector banner shows the selected car identity, uses the selected car color for its frame/label, and keeps sector performance colors inside the sector bars. It is an explicitly mounted independent surface, not the default telemetry-drawer lower-third. `mountTelemetryPanel()` is only a stack template around those detached pieces, and `ui.telemetryModules` controls which pieces appear in stack/drawer templates.
182
+ - `mountRaceTelemetryDrawer()` creates a package-owned race workbench: race canvas, embedded timing tower, lower-third banner, safety-car control, and a right-side telemetry drawer. Pass `{ raceDataTelemetryDetail: true }` when the drawer lower-third should include compact project telemetry detail. The drawer opens smoothly, takes width from the race view instead of overlaying it, and is inert/hidden to interaction when closed.
140
183
  - The FPS readout can be shown or hidden with `ui.showFps`.
141
184
  - `ui.layoutPreset: 'left-tower-overlay'` is a package-owned preset that creates a left broadcast gutter inside the race view, places the timing tower there at the same width as the default timing-board column, frames the PixiJS camera around the remaining usable race area, and keeps camera controls and start lights out of the tower area. In the combined shell, project and radio lower-thirds stay inside the race window while being allowed to cover the timing sidebar.
142
185
  - `ui.raceDataBanners.initial` controls the starting lower-third (`'project'`, `'radio'`, or `'hidden'`), and `ui.raceDataBanners.enabled` controls which lower-third types can appear.
143
186
  - `ui.raceDataBannerSize` controls lower-third sizing: `'custom'` keeps the default CSS-variable-driven banner size for host tuning, while `'auto'` uses the race space to the right of the timing board when wide enough and overlaps the timing board only when space is constrained.
187
+ - `ui.raceDataTelemetryDetail` adds compact S1/S2/S3 sector detail to the project lower-third while keeping radio mode unchanged.
144
188
  - `ui.timingTowerVerticalFit` controls vertical tower behavior in the combined overlay preset: `'expand-race-view'` grows the race window to fit the tower, while `'scroll'` crops the tower area and scrolls timing rows inside it. The same values are accepted as `mountRaceCanvas()` options when `includeTimingTower` embeds the tower in the race canvas.
145
- - Hosts may scale the whole mounted simulator through the container. The horizontal proportions inside package-owned presets are not public API and should not be configurable through raw width or ratio options. The camera reads the current canvas dimensions so wider or taller host windows reveal more of the race view without needing host-owned camera math. The timing tower has a package-owned max width because overly wide timing boards degrade readability; standalone hosts can constrain vertical height through the mount container and let the timing entries scroll internally.
189
+ - Hosts may scale the whole mounted simulator through the container. The horizontal proportions inside package-owned presets are not public API and should not be configurable through raw width or ratio options. The camera reads the current canvas dimensions so wider or taller host windows reveal more of the race view without needing host-owned camera math. The timing tower has a package-owned max width because overly wide timing boards degrade readability; standalone hosts can constrain vertical height through the mount container and let the timing entries scroll internally. Mobile and narrow embeds are handled by package CSS: timing boards stack full-width when they no longer work as side gutters, camera controls are repositioned into the remaining race area, and full-width timing boards do not reserve horizontal camera gutter space.
146
190
  - The host does not need to provide simulator assets.
147
191
  - The host passes data, not internal DOM.
192
+ - Host driver IDs and entry `driverId` values must be unique. Entries may omit `driverNumber`; provided numbers must be unique.
193
+ - `totalLaps` is normalized to a finite positive integer before simulation so invalid input cannot produce zero-lap, negative-lap, or non-finite snapshots.
194
+ - `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.
148
195
  - `onDriverOpen(driver)` is the navigation boundary.
149
196
  - 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.
150
197
  - Race completion is part of the simulation snapshot. Cars receive individual `finished`, `finishTime`, and `classifiedRank` values as they cross the finish distance. The first finisher sets `raceControl.winner`; final `raceControl.classification` and `raceControl.finished` are set only after every car finishes. At that point race control switches to `safety-car`, the final order freezes, and the field keeps circulating under safety-car behavior.
151
- - The simulator must stay interactive after being installed through `npm install ../PaddockJS`.
198
+ - The simulator must stay interactive after being installed through `npm install @inventure71/paddockjs`.
152
199
  - The package must build correctly through a browser bundler that supports JavaScript modules, CSS imports, and image imports.
153
200
  - The simulation should remain deterministic for the same seed, track seed, drivers, entries, and rules.
154
201
  - When `trackSeed` is omitted in a browser mount, the simulator creates a fresh procedural circuit for that mount. Explicit `trackSeed` values are deterministic and cached by seed for repeated mounts.
155
202
  - The renderer should target a paced 60 FPS simulation/render loop.
156
- - 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.
203
+ - 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.
204
+ - Restart and rerender paths must destroy replaced PixiJS display children while preserving shared loaded textures.
205
+ - 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.
206
+ - Expert environment code can create a headless `createPaddockEnvironment()` from the `@inventure71/paddockjs/environment` subpath. It requires explicit `controlledDrivers`, accepts normalized actions `{ steering, throttle, brake }`, advances only through `step(actions)`, and returns environment-loop JavaScript results with `observation`, `reward`, `terminated`, `truncated`, `done`, `events`, `state`, and `info`.
207
+ - 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.
208
+ - 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()`.
209
+ - Browser expert mode wraps the same `RaceSimulation` instance that the visual canvas renders. It must not create a parallel simulation for the same mount.
210
+ - Browser expert mode disables automatic ticker-driven simulation advancement. The visual canvas updates only after explicit expert `reset()` or `step(actions)` calls.
211
+ - Browser expert mode may opt into `expert.visualizeSensors: true` or `expert.visualizeSensors: { rays: true }`. When enabled, ray sensors render in the race canvas world layer from the controlled cars, using the same observation result produced by explicit expert steps.
157
212
 
158
213
  ## Current Visible Features
159
214
 
@@ -169,7 +224,7 @@ Returned controller:
169
224
  - Safety car toggle.
170
225
  - External safety-car control through controller methods and optional mounted button.
171
226
  - Restart button.
172
- - Selected-car text telemetry.
227
+ - Detached selected-car telemetry components for scalar readouts, sector progress graph, lap timing table, and sector timing table.
173
228
  - Car/driver overview panel with a center visual, linked stat cells, and a Car/Driver toggle.
174
229
  - Project/race data lower-third.
175
230
  - Intermittent project radio quotes.
@@ -182,7 +237,8 @@ Returned controller:
182
237
  - A host build tool that handles CSS and image imports. Vite is the currently verified bundler.
183
238
  - `pixi.js` available through the package dependency graph.
184
239
 
185
- Raw Node imports are not a supported runtime check because the package imports CSS and image assets. Verify through a browser bundler and host page instead.
240
+ Raw Node imports of the package root are not a supported runtime check because the browser component entry imports CSS and image assets. The `@inventure71/paddockjs/environment` subpath is the supported browser-free import path for headless JavaScript training.
241
+ The repository starter loop is executable with `node examples/train-basic-policy.mjs`. It is a dependency-free example that trains/evaluates a tiny policy against the environment contract; it is not a packaged Gymnasium bridge or a recommended final RL algorithm.
186
242
 
187
243
  ## Verification
188
244
 
@@ -197,15 +253,14 @@ Expected:
197
253
  - All Vitest tests pass.
198
254
  - `npm pack --dry-run` succeeds and includes source files plus bundled assets.
199
255
 
200
- Run from the portfolio host:
256
+ Run from a browser host that consumes the published package:
201
257
 
202
258
  ```bash
203
- npm install ../PaddockJS
259
+ npm install @inventure71/paddockjs@latest
204
260
  npm run check
205
261
  ```
206
262
 
207
263
  Expected:
208
264
 
209
- - PaddockJS tests pass through the host script.
210
- - The host F1 bundle builds.
265
+ - The host bundle builds with PaddockJS resolved from npm.
211
266
  - Browser smoke shows shell, canvas, driver rows, FPS readout, and working `Open project` navigation.