@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - eabe042: Prepare the major 0.3.0 feature release with published-package docs, runtime restart hardening, and the first expert environment API. Public install docs now target npm consumption, package dry-run contents exclude repo-agent files and unexported standalone code, lockfiles are synchronized to the package version, host driver/entry validation rejects duplicate IDs, invalid lap counts normalize to one-lap races, restart can rebuild deterministic tracks from a new `trackSeed`, asset URL changes are explicitly remount-only, and rerender paths destroy replaced PixiJS display children. The release also adds a browser-free JavaScript environment contract through the `@inventure71/paddockjs/environment` subpath and an opt-in browser expert wrapper that reuses the visual simulator's race state, including opt-in in-canvas ray sensor visualization for controlled expert drivers, center-origin geometry-based ray detection for track exit/re-entry transitions and car footprints, a starter `createProgressReward()` callback, and a dependency-free headless training/evaluation example. It also documents the bring-your-own-model boundary, adds environment action/observation specs, and includes a visual policy-runner example that demonstrates `policy.predict(observation)` driving browser expert mode without adding ML dependencies or model persistence.
8
+
9
+ ## 0.2.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 846f388: Add detached telemetry surfaces, sector timing status colors, a broadcast sector banner, and a race telemetry drawer template with embedded timing tower, safety-car control, and lower-third race data.
14
+
15
+ ### Patch Changes
16
+
17
+ - 21cf4e5: Document the official release workflow and switch automated publishing to npm trusted publishing through GitHub Actions.
18
+
3
19
  All notable changes to this project will be documented in this file.
4
20
 
5
21
  The npm release workflow updates this changelog from committed Changesets.
@@ -0,0 +1,163 @@
1
+ # PaddockJS Install and Update Guide
2
+
3
+ This guide explains how host websites consume the published PaddockJS npm package.
4
+
5
+ ## Install
6
+
7
+ Install the package from npm:
8
+
9
+ ```bash
10
+ npm install @inventure71/paddockjs
11
+ ```
12
+
13
+ The host website imports PaddockJS by package name:
14
+
15
+ ```js
16
+ import { mountF1Simulator } from '@inventure71/paddockjs';
17
+ ```
18
+
19
+ Host applications should not copy simulator assets into their own source tree. PaddockJS owns its bundled car, safety-car, logo, panel, and track texture assets.
20
+
21
+ ## Host Website Setup
22
+
23
+ Create a host entry file in the consuming website:
24
+
25
+ ```js
26
+ import {
27
+ CHAMPIONSHIP_ENTRY_BLUEPRINTS,
28
+ DEMO_PROJECT_DRIVERS,
29
+ mountF1Simulator,
30
+ } from '@inventure71/paddockjs';
31
+
32
+ const root = document.getElementById('f1-simulator-root');
33
+
34
+ if (root) {
35
+ await mountF1Simulator(root, {
36
+ drivers: DEMO_PROJECT_DRIVERS,
37
+ entries: CHAMPIONSHIP_ENTRY_BLUEPRINTS,
38
+ title: 'F1 Simulator Lab',
39
+ kicker: 'Race Control',
40
+ backLinkHref: '/projects',
41
+ backLinkLabel: 'Projects',
42
+ onDriverOpen(driver) {
43
+ if (driver.link) window.location.href = driver.link;
44
+ },
45
+ });
46
+ }
47
+ ```
48
+
49
+ The host page needs a root element and the bundled JavaScript/CSS produced by the host build:
50
+
51
+ ```html
52
+ <div id="f1-simulator-root"></div>
53
+ <script type="module" src="/dist/f1-simulator.js"></script>
54
+ ```
55
+
56
+ If the host bundler does not extract package CSS automatically, import the stylesheet explicitly:
57
+
58
+ ```js
59
+ import '@inventure71/paddockjs/styles.css';
60
+ ```
61
+
62
+ ## Build Setup
63
+
64
+ Use a browser bundler that understands JavaScript module imports, CSS imports, and image imports. Vite, Rollup, Webpack, and similar bundlers are valid fits.
65
+
66
+ Use Node `20.19.0` or newer for local package development, showcase builds, and host builds based on the current Vite toolchain.
67
+
68
+ Example Vite entry config:
69
+
70
+ ```js
71
+ import { defineConfig } from 'vite';
72
+
73
+ export default defineConfig({
74
+ build: {
75
+ outDir: 'dist',
76
+ rollupOptions: {
77
+ input: 'src/paddockjs-entry.js',
78
+ output: {
79
+ entryFileNames: 'f1-simulator.js',
80
+ chunkFileNames: 'chunks/[name]-[hash].js',
81
+ assetFileNames: 'assets/[name]-[hash][extname]',
82
+ },
83
+ },
84
+ },
85
+ });
86
+ ```
87
+
88
+ Run the host's normal install and build checks:
89
+
90
+ ```bash
91
+ npm install
92
+ npm run check
93
+ ```
94
+
95
+ Do not use raw Node as the main import test:
96
+
97
+ ```bash
98
+ node -e "import('@inventure71/paddockjs')"
99
+ ```
100
+
101
+ That can fail because PaddockJS imports CSS and image assets. Verify through a browser-oriented host build instead.
102
+
103
+ ## Updating The Package
104
+
105
+ Install the latest published version:
106
+
107
+ ```bash
108
+ npm install @inventure71/paddockjs@latest
109
+ npm run check
110
+ ```
111
+
112
+ Install a specific version when the host needs a controlled upgrade:
113
+
114
+ ```bash
115
+ npm install @inventure71/paddockjs@0.2.0
116
+ npm run check
117
+ ```
118
+
119
+ After updating, smoke-test the page that mounts the simulator. Browser behavior changes should be checked in the consuming host, because host CSS, container size, and route handling are outside the package.
120
+
121
+ ## Package Release Workflow
122
+
123
+ The package repo owns its release process:
124
+
125
+ - `npm run check` runs runtime tests, public type verification, dry-pack verification, and the tracked showcase build.
126
+ - `npm run changeset` records the next version bump and changelog note.
127
+ - `npm run version-packages` applies pending Changesets locally.
128
+ - `.github/workflows/ci.yml` verifies the package on push and pull request.
129
+ - `.github/workflows/release.yml` opens a release PR from Changesets and publishes to npm after merge through npm trusted publishing.
130
+
131
+ For trusted publishing on npm, configure the package settings to trust:
132
+
133
+ - organization or user: `Inventure71`
134
+ - repository: `PaddockJS`
135
+ - workflow filename: `release.yml`
136
+
137
+ No long-lived `NPM_TOKEN` secret is required once trusted publishing is enabled.
138
+
139
+ The release PR must include synchronized `package.json`, `package-lock.json`, `CHANGELOG.md`, and Changesets output for the version being published.
140
+
141
+ ## When Something Breaks
142
+
143
+ If the host cannot resolve the package, reinstall from npm:
144
+
145
+ ```bash
146
+ npm install @inventure71/paddockjs@latest
147
+ ```
148
+
149
+ If the host bundler cannot resolve an asset, verify the package contents from the package repo:
150
+
151
+ ```bash
152
+ npm run pack:dry
153
+ ```
154
+
155
+ Check that the asset is listed in the dry-pack output.
156
+
157
+ If simulator tests fail after a package change:
158
+
159
+ ```bash
160
+ npm test
161
+ ```
162
+
163
+ Fix the package first, then update and rebuild the host.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # PaddockJS
2
2
 
3
- This folder is the extractable F1 simulator component. It owns the simulator source, bundled simulator assets, CSS, demo data, and the public mount API.
3
+ PaddockJS is an installable F1-style simulator component for browser host websites. It owns the simulator source, bundled simulator assets, CSS, demo data, and public mount API.
4
4
 
5
5
  ## Install
6
6
 
@@ -10,24 +10,19 @@ From npm:
10
10
  npm install @inventure71/paddockjs
11
11
  ```
12
12
 
13
- For local sibling-repo development:
14
-
15
- ```bash
16
- npm install ../PaddockJS
17
- ```
18
-
19
13
  ## Documentation
20
14
 
21
15
  Start with [docs/index.md](docs/index.md) for system specs, rules, concepts, data contracts, and architecture notes.
22
16
 
23
17
  ## Package Workflow
24
18
 
25
- The repo now includes the full package-release boundary:
19
+ The repo includes the full package-release boundary:
26
20
 
27
21
  - a tracked showcase host in `local-preview/`
28
22
  - public TypeScript declarations in `src/index.d.ts`
29
23
  - GitHub Actions CI in `.github/workflows/ci.yml`
30
24
  - Changesets release automation in `.github/workflows/release.yml`
25
+ - npm trusted publishing support through GitHub Actions OIDC
31
26
 
32
27
  Useful commands:
33
28
 
@@ -40,6 +35,67 @@ npm run changeset
40
35
 
41
36
  `npm run check` verifies the runtime tests, public declaration file, dry package contents, and showcase build.
42
37
 
38
+ Local development and showcase builds require Node `20.19.0` or newer. CI currently runs the package check on Node 22 and releases on Node 24.
39
+
40
+ ## Expert Environment API
41
+
42
+ Headless training code imports the environment subpath:
43
+
44
+ ```js
45
+ import { createPaddockEnvironment, createProgressReward } from '@inventure71/paddockjs/environment';
46
+ ```
47
+
48
+ The package root remains the browser component API. The environment subpath is intentionally browser-free and does not import DOM, PixiJS, or package CSS.
49
+ PaddockJS is a bring-your-own-model environment. It does not choose an ML framework, store model weights, or ship a trained driver. See [Bring Your Own Model](docs/training.md) for the policy shape and visual playback loop.
50
+
51
+ ```js
52
+ const env = createPaddockEnvironment({
53
+ drivers,
54
+ entries,
55
+ controlledDrivers: ['budget'],
56
+ frameSkip: 2,
57
+ reward: createProgressReward(),
58
+ });
59
+
60
+ let result = env.reset();
61
+ result = env.step({
62
+ budget: { steering: 0, throttle: 1, brake: 0 },
63
+ });
64
+ ```
65
+
66
+ The repository includes a dependency-free starter training loop that uses the same environment contract:
67
+
68
+ ```bash
69
+ node examples/train-basic-policy.mjs --generations=4 --candidates=5 --episodes=1 --steps=240
70
+ ```
71
+
72
+ Each ray reports track-transition distance and car distance. A track hit uses `kind: 'exit'` when the ray leaves the road and `kind: 'entry'` when an off-track ray points back to the road.
73
+
74
+ External training code can inspect the environment contract without guessing field ranges:
75
+
76
+ ```js
77
+ const actionSpec = env.getActionSpec();
78
+ const observationSpec = env.getObservationSpec();
79
+ ```
80
+
81
+ Browser expert mode is opt-in through the normal mount API. When enabled, the visual simulator advances only when host code calls `simulator.expert.step(actions)`. Expert mode is a mount-time boundary; changing `expert` through `restart(nextOptions)` is rejected so ticker ownership cannot silently change under a mounted simulator.
82
+ Set `expert.visualizeSensors` to draw expert sensor rays inside the actual race canvas for visual debugging:
83
+
84
+ ```js
85
+ const simulator = await mountF1Simulator(root, {
86
+ drivers,
87
+ entries,
88
+ expert: {
89
+ enabled: true,
90
+ controlledDrivers: ['budget'],
91
+ frameSkip: 4,
92
+ visualizeSensors: {
93
+ rays: true,
94
+ },
95
+ },
96
+ });
97
+ ```
98
+
43
99
  ## API
44
100
 
45
101
  All-in-one mount:
@@ -67,12 +123,14 @@ const simulator = await mountF1Simulator(document.getElementById('sim-root'), {
67
123
  vehicle: { id: 'budget-bb01', name: 'BB-01 Ledger', power: 48, braking: 72, aero: 55, dragEfficiency: 66, mechanicalGrip: 63, weightControl: 58, tireCare: 82 },
68
124
  },
69
125
  ],
126
+ initialCameraMode: 'show-all',
127
+ expert: {
128
+ enabled: true,
129
+ controlledDrivers: ['budget'],
130
+ },
70
131
  onDriverOpen(driver) {
71
132
  window.location.href = driver.link;
72
133
  },
73
- onRaceFinish({ winner, classification }) {
74
- console.log('Race winner:', winner.name, classification);
75
- },
76
134
  });
77
135
  ```
78
136
 
@@ -96,7 +154,11 @@ simulator.mountTimingTower(document.getElementById('sim-timing'));
96
154
  simulator.mountRaceCanvas(document.getElementById('sim-race'), {
97
155
  includeRaceDataPanel: true,
98
156
  });
99
- simulator.mountTelemetryPanel(document.getElementById('sim-telemetry'));
157
+ simulator.mountTelemetryCore(document.getElementById('sim-telemetry-core'));
158
+ simulator.mountTelemetrySectors(document.getElementById('sim-telemetry-sectors'));
159
+ simulator.mountTelemetrySectorBanner(document.getElementById('sim-telemetry-sector-banner'));
160
+ simulator.mountTelemetryLapTimes(document.getElementById('sim-telemetry-laps'));
161
+ simulator.mountTelemetrySectorTimes(document.getElementById('sim-telemetry-sector-times'));
100
162
  simulator.mountCarDriverOverview(document.getElementById('sim-overview'));
101
163
 
102
164
  await simulator.start();
@@ -112,18 +174,28 @@ simulator.mountRaceCanvas(document.getElementById('sim-race'), {
112
174
  });
113
175
  ```
114
176
 
177
+ For a packaged race-window template with a right-side telemetry drawer:
178
+
179
+ ```js
180
+ simulator.mountRaceTelemetryDrawer(document.getElementById('sim-race-workbench'), {
181
+ timingTowerVerticalFit: 'expand-race-view',
182
+ raceDataTelemetryDetail: true,
183
+ });
184
+ ```
185
+
115
186
  `drivers` is the host-owned project/pilot list. `entries` is the optional driver/car/team pairing sheet. The car/driver overview uses the existing driver and vehicle rating components from each entry. Driver and vehicle entries can also include `customFields` as extra label/value metadata. Entries can include `team: { id, name, color, icon }`; the timing tower uses the team icon and defaults team color to the car color when omitted. Assets, including the default car image and generic driver helmet, are bundled by default, so the host website does not need to provide simulator images or textures.
116
187
 
117
188
  The returned object supports:
118
189
 
119
190
  - `destroy()`
120
- - `restart(nextOptions)`
191
+ - `restart(nextOptions)` for non-asset, non-expert race/data/seed changes
121
192
  - `selectDriver(driverId)`
122
193
  - `setSafetyCarDeployed(deployed)`
123
194
  - `callSafetyCar()`
124
195
  - `clearSafetyCar()`
125
196
  - `toggleSafetyCar()`
126
197
  - `getSnapshot()`
198
+ - `expert` when explicitly enabled, otherwise `null`
127
199
 
128
200
  Useful UI options:
129
201
 
@@ -139,26 +211,34 @@ ui: {
139
211
  cameraControls: 'external',
140
212
  showFps: false,
141
213
  telemetryIncludesOverview: false,
214
+ telemetryModules: ['core', 'sectors', 'lapTimes', 'sectorTimes'],
142
215
  raceDataBanners: {
143
216
  initial: 'project',
144
217
  enabled: ['project', 'radio'],
145
218
  },
146
219
  raceDataBannerSize: 'auto',
220
+ raceDataTelemetryDetail: true,
147
221
  timingTowerVerticalFit: 'expand-race-view',
148
222
  }
149
223
  ```
150
224
 
151
225
  `preset` is resolved before explicit host options. Available presets are `dashboard`, `timing-overlay`, `compact-race`, and `full-dashboard`; hosts can start from a preset and override any `ui` or `theme` field. `theme` maps to package CSS variables for the stable sizing/color contract: `accentColor`, `greenColor`, `yellowColor`, `timingTowerMaxWidth`, and `raceViewMinHeight`.
152
226
 
153
- If `trackSeed` is omitted, each mounted browser simulator creates a fresh procedural circuit. Passing `trackSeed` makes the track deterministic so multiple embeds can share the same generated circuit; repeated procedural seeds are cached within the page runtime.
227
+ If `trackSeed` is omitted, each mounted browser simulator creates a fresh procedural circuit. Passing `trackSeed` makes the track deterministic so multiple embeds can share the same generated circuit; repeated procedural seeds are cached within the page runtime. `restart({ trackSeed })` rebuilds the race on the deterministic circuit for the new seed. Asset URL changes are not restartable; destroy and mount a new simulator when changing assets.
228
+
229
+ `initialCameraMode` accepts `'overview'`, `'leader'`, `'selected'`, or `'show-all'`; invalid values fall back to `'leader'`. `layoutPreset: 'left-tower-overlay'` creates a left broadcast gutter inside the race view, frames the camera around the remaining race area, and places the timing tower in that gutter without covering camera controls. The project/radio lower-third stays inside the race window and can intentionally render over the timing sidebar instead of shrinking around it. Composable hosts should pass `{ includeRaceDataPanel: true }` to `mountRaceCanvas()` when they want that lower-third clipped and layered by the race window; `mountRaceDataPanel()` remains available for hosts that intentionally want the banner as a standalone surface. `raceDataTelemetryDetail: true` makes the project lower-third include a compact S1/S2/S3 sector strip; radio mode stays the same and the standalone `mountTelemetrySectorBanner()` surface remains available only when a host explicitly mounts it. `includeTimingTower: true` embeds the timing tower directly inside the race canvas and reserves camera space from the measured tower gutter when the tower is a side overlay. In narrow mobile hosts, package CSS stacks the embedded timing tower full-width and the camera stops reserving a fake left gutter. `mountRaceTelemetryDrawer()` is a higher-level template that mounts the race canvas, embedded timing tower, lower-third, safety-car control, and a right telemetry drawer together; pass `{ raceDataTelemetryDetail: true }` when the drawer lower-third should include compact sector detail. Opening the drawer reduces the race area instead of overlaying it, and closing it removes the drawer from interaction. `raceDataBanners.initial` selects the starting banner state (`'project'`, `'radio'`, or `'hidden'`), while `raceDataBanners.enabled` chooses which banner types may appear. `raceDataBannerSize: 'auto'` uses the race space to the right of the timing board when it is wide enough and falls back to the full lower-third overlap when it is not; `'custom'` preserves the default CSS-variable-driven lower-third size for hosts that want to tune their own banner geometry. `timingTowerVerticalFit: 'expand-race-view'` lets the race window grow tall enough for the tower; `'scroll'` keeps the race window height and scrolls the timing list inside the cropped tower. The same fit values can be passed to `mountRaceCanvas()` when `includeTimingTower` is enabled. Standalone timing towers are capped by `--timing-board-max-width` and fill their mount root height; placing the root in a fixed-height container makes only the timing entries scroll. Timing entries always stack from the top as fixed rows, so P1/P2 occupy the same vertical positions whether the race has 2 cars or 20. The timing tower includes an `Int`/`Gap` broadcast switch: `Int` shows the interval to the car ahead, while `Gap` shows total gap to the leader. `cameraControls: 'external'` moves view controls out of the canvas so hosts can mount them with `mountCameraControls()`. `showFps: false` hides the FPS readout. `telemetryIncludesOverview: false` keeps the car/driver overview out of the telemetry stack so hosts can mount `mountCarDriverOverview()` separately. `telemetryModules` controls optional package-owned telemetry surfaces: `core`, `sectors`, `lapTimes`, and `sectorTimes`; those same surfaces are also available as fully detached mount methods.
230
+
231
+ ### Composable overlay layout contract
232
+
233
+ `ui.layoutPreset: 'left-tower-overlay'` is package-owned. Hosts should not recreate or depend on internal shell classes such as `.sim-shell--left-tower-overlay` or `.sim-grid` as their public integration contract.
154
234
 
155
- `layoutPreset: 'left-tower-overlay'` creates a left broadcast gutter inside the race view, frames the camera around the remaining race area, and places the timing tower in that gutter without covering camera controls. The project/radio lower-third stays inside the race window and can intentionally render over the timing sidebar instead of shrinking around it. Composable hosts should pass `{ includeRaceDataPanel: true }` to `mountRaceCanvas()` when they want that lower-third clipped and layered by the race window; `mountRaceDataPanel()` remains available for hosts that intentionally want the banner as a standalone surface. `includeTimingTower: true` embeds the timing tower directly inside the race canvas and reserves camera space from the measured tower gutter. `raceDataBanners.initial` selects the starting banner state (`'project'`, `'radio'`, or `'hidden'`), while `raceDataBanners.enabled` chooses which banner types may appear. `raceDataBannerSize: 'auto'` uses the race space to the right of the timing board when it is wide enough and falls back to the full lower-third overlap when it is not; `'custom'` preserves the default CSS-variable-driven lower-third size for hosts that want to tune their own banner geometry. `timingTowerVerticalFit: 'expand-race-view'` lets the race window grow tall enough for the tower; `'scroll'` keeps the race window height and scrolls the timing list inside the cropped tower. The same fit values can be passed to `mountRaceCanvas()` when `includeTimingTower` is enabled. Standalone timing towers are capped by `--timing-board-max-width` and fill their mount root height; placing the root in a fixed-height container makes only the timing entries scroll. Timing entries always stack from the top as fixed rows, so P1/P2 occupy the same vertical positions whether the race has 2 cars or 20. The timing tower includes an `Int`/`Gap` broadcast switch: `Int` shows the interval to the car ahead, while `Gap` shows total gap to the leader. `cameraControls: 'external'` moves view controls out of the canvas so hosts can mount them with `mountCameraControls()`. `showFps: false` hides the FPS readout. `telemetryIncludesOverview: false` keeps telemetry text-only so hosts can mount `mountCarDriverOverview()` separately.
235
+ For the all-in-one simulator, call `mountF1Simulator()` and let PaddockJS generate the shell. For composable layouts, hosts should provide mount roots and call package mount methods such as `mountRaceCanvas(root, { includeTimingTower: true, includeRaceDataPanel: true })` or `mountRaceTelemetryDrawer(root)`. Host CSS may size the outer container, but timing-tower placement, gutter measurement, camera safe area, and narrow-screen stacking are owned by PaddockJS.
156
236
 
157
237
  Mounted package surfaces include a lightweight red start-light loading overlay. The runtime removes each overlay after PixiJS, assets, controls, and the initial readouts are ready.
158
238
 
159
239
  The runtime also pauses its render ticker when the race canvas is offscreen or the browser tab is hidden. This keeps pages with multiple PaddockJS embeds responsive without requiring host code to manually start and stop each simulator.
160
240
 
161
- Lifecycle callbacks are optional and host-owned. PaddockJS emits `onLoadingChange`, `onReady`, `onError`, `onDriverSelect`, `onRaceEvent`, `onLapChange`, and `onRaceFinish`; callback errors are routed to `onError` when provided and do not stop the simulator loop. Race snapshots include per-car interval timing, leader-gap timing, calibrated `speedKph`, finish state, `raceControl.winner` after the first finisher, and final `raceControl.classification` only after the whole field finishes. The field then circulates in safety-car mode and the race canvas shows a package-owned winner banner.
241
+ Lifecycle callbacks are optional and host-owned. PaddockJS emits `onLoadingChange`, `onReady`, `onError`, `onDriverSelect`, `onRaceEvent`, `onLapChange`, and `onRaceFinish`; callback errors are routed to `onError` when provided and do not stop the simulator loop. Race snapshots include per-car interval timing, leader-gap timing, calibrated `speedKph`, automatic `track.sectors`, per-car `lapTelemetry`, finish state, `raceControl.winner` after the first finisher, and final `raceControl.classification` only after the whole field finishes. The field then circulates in safety-car mode and the race canvas shows a package-owned winner banner.
162
242
 
163
243
  ## License
164
244
 
@@ -48,13 +48,36 @@ host page
48
48
  -> simulator.mountSafetyCarControl(safetyCarRoot)
49
49
  -> simulator.mountTimingTower(timingRoot)
50
50
  -> simulator.mountRaceCanvas(canvasRoot, { includeRaceDataPanel })
51
- -> simulator.mountTelemetryPanel(telemetryRoot)
51
+ -> simulator.mountTelemetryCore(coreTelemetryRoot)
52
+ -> simulator.mountTelemetrySectors(sectorGraphRoot)
53
+ -> simulator.mountTelemetrySectorBanner(sectorBannerRoot)
54
+ -> simulator.mountTelemetryLapTimes(lapTimesRoot)
55
+ -> simulator.mountTelemetrySectorTimes(sectorTimesRoot)
56
+ -> simulator.mountRaceTelemetryDrawer(raceWorkbenchRoot) [optional template alternative]
52
57
  -> simulator.mountRaceDataPanel(raceDataRoot) [optional standalone alternative]
53
58
  -> simulator.start()
54
59
  -> new F1SimulatorApp(compositeRoot, resolvedOptions)
55
60
  -> PixiJS render loop + DOM readout updates
56
61
  ```
57
62
 
63
+ Headless environment flow:
64
+
65
+ ```txt
66
+ training script
67
+ -> import from @inventure71/paddockjs/environment
68
+ -> createPaddockEnvironment(options)
69
+ -> resolveEnvironmentOptions(options)
70
+ -> createRaceSimulation(...)
71
+ -> env.reset()
72
+ -> env.step(actions)
73
+ -> resolveActionMap(actions)
74
+ -> RaceSimulation.step(...) repeated by frameSkip
75
+ -> buildEnvironmentObservation(...)
76
+ -> optional reward(context)
77
+ ```
78
+
79
+ Browser expert mode reuses the same environment runtime around the visual app's existing `RaceSimulation` instance. When `expert.enabled` is true, the visual ticker does not advance simulation time automatically; `simulator.expert.reset()` and `simulator.expert.step(actions)` are the only simulation-advance entry points.
80
+
58
81
  ## Public Entry
59
82
 
60
83
  `src/index.js` is the public package entry.
@@ -70,6 +93,18 @@ Responsibilities:
70
93
  - Return the controller methods.
71
94
  - Re-export the composable mount API.
72
95
 
96
+ `src/index.js` does not export the headless environment API. Training and automation code imports `@inventure71/paddockjs/environment`, which resolves to `src/environment/index.js`.
97
+
98
+ `src/environment/` owns the browser-free expert environment:
99
+
100
+ - `index.js` and `index.d.ts` expose the public subpath API.
101
+ - `runtime.js` owns environment-loop `reset()`, `step(actions)`, `getObservation()`, and `getState()` around an injected race-simulation host.
102
+ - `options.js` validates `controlledDrivers`, scenario participants, frame skip, action policy, sensor config, and episode limits.
103
+ - `actions.js` maps normalized public controls onto simulator steering/throttle/brake controls.
104
+ - `observations.js`, `sensors.js`, and `events.js` build sensor-style observations, fixed-schema vectors, ray/nearby-car readings, and global/per-driver events.
105
+
106
+ The environment subpath must not import `src/index.js`, package CSS, PixiJS, `F1SimulatorApp`, or DOM-specific code.
107
+
73
108
  `src/api/PaddockSimulatorController.js` owns composable mounting.
74
109
 
75
110
  Responsibilities:
@@ -99,8 +134,12 @@ Responsibilities:
99
134
  - Race data panel rendering.
100
135
  - Safety car button.
101
136
  - Multiple package-rendered safety-car buttons bound to one simulation state.
102
- - Restart behavior.
103
- - Lifecycle cleanup.
137
+ - Restart behavior for race data, seed, and track changes. Asset URL changes and browser expert mode changes are intentionally outside restart because texture loading and ticker ownership are part of initialization.
138
+ - Lifecycle cleanup, including partial-init failure cleanup and destruction of replaced PixiJS display children without destroying shared textures.
139
+
140
+ When `options.expert.enabled` is true, `F1SimulatorApp` creates a narrow browser expert adapter around its existing `this.sim` race simulation. The adapter uses the same shared environment runtime as the headless API, but it must not create a second race simulation for the visual mount. Expert browser mounts disable automatic ticker-driven simulation advancement; the canvas updates only after `simulator.expert.reset()` or `simulator.expert.step(actions)` renders the new snapshot. Expert mode is a mount-time boundary; runtime restart rejects `expert` changes instead of attempting to rewire ticker ownership in place.
141
+
142
+ Expert sensor visualization belongs to the browser app layer, not the headless environment. The environment result owns the observation contract; `BrowserExpertAdapter` passes that observation into `F1SimulatorApp.renderExpertFrame()`, and `F1SimulatorApp` draws opt-in sensor rays in a Pixi world layer so they share the same camera transform as the track and cars.
104
143
 
105
144
  This file is still large. When changing it substantially, prefer extracting cohesive modules rather than adding unrelated responsibilities.
106
145
 
@@ -118,6 +157,7 @@ Responsibilities:
118
157
  - Car creation.
119
158
  - Race ordering.
120
159
  - Lap calculation.
160
+ - Per-car lap and sector telemetry.
121
161
  - Timing history.
122
162
  - DRS eligibility.
123
163
  - Collision response.
@@ -127,13 +167,13 @@ Responsibilities:
127
167
 
128
168
  `src/simulation/vehiclePhysics.js` owns vehicle integration and surface physics.
129
169
 
130
- `src/simulation/trackModel.js` owns track construction, procedural track generation, DRS zones, and nearest-track queries. Browser mounts generate a fresh procedural seed when `trackSeed` is omitted; explicit procedural track seeds are cached so repeated mounts with the same seed do not regenerate or rebuild the same model.
170
+ `src/simulation/trackModel.js` owns track construction, procedural track generation, automatic three-sector metadata, DRS zones, and nearest-track queries. Browser mounts generate a fresh procedural seed when `trackSeed` is omitted; explicit procedural track seeds are cached so repeated mounts with the same seed do not regenerate or rebuild the same model.
131
171
 
132
172
  `src/simulation/units.js` owns conversion between simulator units and public meter/km/h display values. Physics stays in simulator units; snapshots expose calibrated display fields such as `speedKph`, `distanceMeters`, and `gapMeters`.
133
173
 
134
174
  `src/rendering/renderSnapshot.js` owns interpolation for rendering.
135
175
 
136
- The app runtime pauses its PixiJS ticker when the race canvas is outside the viewport or the document is hidden, then resets the frame clock before resuming. This prevents host pages with several simulator embeds from running every race while only one is visible, and avoids a large simulation catch-up step when the canvas re-enters view. Camera safe-area layout measurements are cached and invalidated by resize observation so the 60 FPS render path does not force repeated DOM geometry reads. Long project-radio delays are also treated as stale schedule state instead of replaying every missed lower-third transition.
176
+ The app runtime pauses its PixiJS ticker when the race canvas is outside the viewport or the document is hidden, then resets the frame clock before resuming. This prevents host pages with several simulator embeds from running every race while only one is visible, and avoids a large simulation catch-up step when the canvas re-enters view. Camera safe-area layout measurements are cached and invalidated by resize observation so the 60 FPS render path does not force repeated DOM geometry reads. Runtime readouts avoid redundant DOM writes for unchanged text, timing markup, finish classification, and static selected-car overview data. Long project-radio delays are also treated as stale schedule state instead of replaying every missed lower-third transition.
137
177
 
138
178
  ## Data Layer
139
179
 
@@ -141,9 +181,9 @@ The app runtime pauses its PixiJS ticker when the race canvas is outside the vie
141
181
 
142
182
  `src/data/vehicleData.js` converts vehicle rating sheets into physical setup values.
143
183
 
144
- `src/data/championship.js` pairs drivers with entries and generates timing codes, numbers, team metadata, and converted constructor data.
184
+ `src/data/championship.js` pairs drivers with entries and generates timing codes, numbers, team metadata, and converted constructor data. It rejects duplicate entry driver IDs, ignores omitted driver numbers during uniqueness checks, and falls back to stable grid-order numbers when host entries do not provide numbers.
145
185
 
146
- `src/data/normalizeDrivers.js` validates host driver data and invokes championship pairing.
186
+ `src/data/normalizeDrivers.js` validates host driver data, rejects duplicate driver IDs before runtime maps are created, and invokes championship pairing.
147
187
 
148
188
  `src/data/demoDrivers.js` is demo/portfolio-flavored sample data. It should not become the only supported data path.
149
189
 
@@ -156,13 +196,16 @@ The app runtime pauses its PixiJS ticker when the race canvas is outside the vie
156
196
  - Safety-car control.
157
197
  - Timing tower.
158
198
  - Race canvas.
159
- - Race-data panel.
160
- - Telemetry panel.
199
+ - Race-data panel, including the optional project telemetry detail variant.
200
+ - Telemetry stack template.
201
+ - Detached telemetry core, sector graph, lap-time table, and sector-time table.
202
+ - Broadcast sector banner.
203
+ - Race telemetry drawer template.
161
204
  - Car/driver overview panel.
162
205
 
163
- `src/ui/shellTemplate.js` composes those component templates into the default all-in-one simulator DOM and package-owned layout presets such as `left-tower-overlay`. The telemetry panel can embed the car/driver overview, but the overview is also a separately mountable package-owned component. The left-tower overlay preset is responsible for internal component placement and package-owned proportions; the project/radio lower-third remains owned by the race canvas so it can either use the race space beside the timing tower in `auto` sizing mode or overlap the tower when space is constrained. Composable hosts can also ask the race-canvas template to embed the timing tower directly with `includeTimingTower`, using the same expand-vs-scroll vertical fit contract. `F1SimulatorApp` measures the resulting timing-tower gutter when framing the PixiJS camera, whether the tower comes from the prebuilt shell or from an embedded composable race canvas. Hosts should not provide the internal simulator markup or tune preset internals with raw sizing options.
206
+ `src/ui/shellTemplate.js` composes those component templates into the default all-in-one simulator DOM and package-owned layout presets such as `left-tower-overlay`. Telemetry graph/table surfaces are independent package-owned components controlled by `ui.telemetryModules` when used through stack/drawer templates; the app renders them from `car.lapTelemetry` without requiring host-owned DOM. The telemetry stack can embed the car/driver overview, but the overview is also a separately mountable package-owned component. The broadcast sector banner is a separate lower-third-style sector graph that can be mounted standalone or embedded inside the race canvas only when a host explicitly asks for that independent popup; `F1SimulatorApp` binds the selected car name/code/color to it through the same readout path as the other telemetry surfaces. The race telemetry drawer template composes a race canvas with embedded timing tower, project/radio lower-third, safety-car control, and detached telemetry components in a right drawer. Its open/close state is owned by `F1SimulatorApp`; the safety-car control remains available while telemetry is open. The drawer reserves final race-view space with a stable margin while the sidebar itself animates with a compositor transform, avoiding grid-template reflow during the slide. The drawer race area inherits the workbench height so host embeds do not leave unused black space below the simulation. The left-tower overlay preset is responsible for internal component placement and package-owned proportions; the project/radio lower-third remains owned by the race canvas so it can either use the race space beside the timing tower in `auto` sizing mode or overlap the tower when space is constrained. Composable hosts can also ask the race-canvas template to embed the timing tower directly with `includeTimingTower`, using the same expand-vs-scroll vertical fit contract. `F1SimulatorApp` measures the resulting timing-tower gutter when framing the PixiJS camera, whether the tower comes from the prebuilt shell or from an embedded composable race canvas. On narrow hosts, CSS stacks timing boards full-width and the camera safe-area measurement treats those boards as stacked content instead of side gutters. Hosts should not provide the internal simulator markup or tune preset internals with raw sizing options.
164
207
 
165
- `src/config/defaultOptions.js` owns preset resolution and the public theme contract. Presets are merged before host options; theme fields are applied as package CSS variables by the all-in-one app and composable controller. This keeps sizing/color customization explicit without turning internal layout ratios into public API.
208
+ `src/config/defaultOptions.js` owns preset resolution, telemetry module normalization, and the public theme contract. Presets are merged before host options; theme fields are applied as package CSS variables by the all-in-one app and composable controller. This keeps sizing/color customization explicit without turning internal layout ratios into public API.
166
209
 
167
210
  Race completion is owned by `src/simulation/raceSimulation.js`. The app layer reads `raceControl.finished`, `winner`, and `classification` from snapshots, renders the winner banner, and emits lifecycle callbacks. UI code must not infer finish state from lap text or timing rows.
168
211
 
@@ -191,6 +234,7 @@ Current coverage includes:
191
234
  - Championship metadata.
192
235
  - Component API and callback behavior.
193
236
  - Driver controller behavior.
237
+ - Expert environment actions, observations, rewards, events, and browser adapter behavior.
194
238
  - Procedural track rendering helpers.
195
239
  - Race simulation rules.
196
240
  - Render interpolation.
package/docs/concepts.md CHANGED
@@ -131,6 +131,10 @@ Progress is the wrapped distance around the current lap.
131
131
 
132
132
  Race distance is cumulative and can increase beyond one lap. Ranking uses race distance, not wrapped progress.
133
133
 
134
+ ## Sectors
135
+
136
+ Every built track is divided into three equal sectors, published as `track.sectors`. Sector timing is derived from cumulative race distance crossing those sector boundaries, not from UI state.
137
+
134
138
  ## Snapshot
135
139
 
136
140
  A snapshot is the read-only state returned by the race simulation and exposed through `getSnapshot()`.
@@ -146,7 +150,7 @@ Snapshots include:
146
150
  - Events from the last step.
147
151
  - Ordered cars with telemetry and setup data.
148
152
 
149
- Per-car timing exposes both interval to the car ahead and cumulative gap to the leader. Per-car speed and distance display fields are calibrated through the simulator unit conversion helpers instead of treating rendered world units as meters.
153
+ Per-car timing exposes both interval to the car ahead and cumulative gap to the leader. Per-car `lapTelemetry` exposes current lap, current sector, current/last/best lap times, current/last/best sector times, sector progress, and sector performance status. Sector performance status marks completed sector times as `overall-best`, `personal-best`, or `slower`, which drives the purple/green/yellow timing colors in sector graphs and tables. Per-car speed and distance display fields are calibrated through the simulator unit conversion helpers instead of treating rendered world units as meters.
150
154
 
151
155
  ## Render Snapshot
152
156