@inventure71/paddockjs 0.2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
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
+
3
9
  ## 0.2.0
4
10
 
5
11
  ### Minor Changes
@@ -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,19 +10,13 @@ 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`
@@ -41,6 +35,67 @@ npm run changeset
41
35
 
42
36
  `npm run check` verifies the runtime tests, public declaration file, dry package contents, and showcase build.
43
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
+
44
99
  ## API
45
100
 
46
101
  All-in-one mount:
@@ -68,12 +123,14 @@ const simulator = await mountF1Simulator(document.getElementById('sim-root'), {
68
123
  vehicle: { id: 'budget-bb01', name: 'BB-01 Ledger', power: 48, braking: 72, aero: 55, dragEfficiency: 66, mechanicalGrip: 63, weightControl: 58, tireCare: 82 },
69
124
  },
70
125
  ],
126
+ initialCameraMode: 'show-all',
127
+ expert: {
128
+ enabled: true,
129
+ controlledDrivers: ['budget'],
130
+ },
71
131
  onDriverOpen(driver) {
72
132
  window.location.href = driver.link;
73
133
  },
74
- onRaceFinish({ winner, classification }) {
75
- console.log('Race winner:', winner.name, classification);
76
- },
77
134
  });
78
135
  ```
79
136
 
@@ -96,7 +153,6 @@ simulator.mountSafetyCarControl(document.getElementById('sim-safety-car'));
96
153
  simulator.mountTimingTower(document.getElementById('sim-timing'));
97
154
  simulator.mountRaceCanvas(document.getElementById('sim-race'), {
98
155
  includeRaceDataPanel: true,
99
- includeTelemetrySectorBanner: true,
100
156
  });
101
157
  simulator.mountTelemetryCore(document.getElementById('sim-telemetry-core'));
102
158
  simulator.mountTelemetrySectors(document.getElementById('sim-telemetry-sectors'));
@@ -114,7 +170,6 @@ The race canvas can also own the timing tower when a host wants a single reusabl
114
170
  simulator.mountRaceCanvas(document.getElementById('sim-race'), {
115
171
  includeTimingTower: true,
116
172
  includeRaceDataPanel: true,
117
- includeTelemetrySectorBanner: true,
118
173
  timingTowerVerticalFit: 'scroll',
119
174
  });
120
175
  ```
@@ -124,6 +179,7 @@ For a packaged race-window template with a right-side telemetry drawer:
124
179
  ```js
125
180
  simulator.mountRaceTelemetryDrawer(document.getElementById('sim-race-workbench'), {
126
181
  timingTowerVerticalFit: 'expand-race-view',
182
+ raceDataTelemetryDetail: true,
127
183
  });
128
184
  ```
129
185
 
@@ -132,13 +188,14 @@ simulator.mountRaceTelemetryDrawer(document.getElementById('sim-race-workbench')
132
188
  The returned object supports:
133
189
 
134
190
  - `destroy()`
135
- - `restart(nextOptions)`
191
+ - `restart(nextOptions)` for non-asset, non-expert race/data/seed changes
136
192
  - `selectDriver(driverId)`
137
193
  - `setSafetyCarDeployed(deployed)`
138
194
  - `callSafetyCar()`
139
195
  - `clearSafetyCar()`
140
196
  - `toggleSafetyCar()`
141
197
  - `getSnapshot()`
198
+ - `expert` when explicitly enabled, otherwise `null`
142
199
 
143
200
  Useful UI options:
144
201
 
@@ -160,15 +217,22 @@ ui: {
160
217
  enabled: ['project', 'radio'],
161
218
  },
162
219
  raceDataBannerSize: 'auto',
220
+ raceDataTelemetryDetail: true,
163
221
  timingTowerVerticalFit: 'expand-race-view',
164
222
  }
165
223
  ```
166
224
 
167
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`.
168
226
 
169
- 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.
170
234
 
171
- `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 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, safety-car control, lower-third, and a right telemetry drawer together; 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.
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.
172
236
 
173
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.
174
238
 
@@ -60,6 +60,24 @@ host page
60
60
  -> PixiJS render loop + DOM readout updates
61
61
  ```
62
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
+
63
81
  ## Public Entry
64
82
 
65
83
  `src/index.js` is the public package entry.
@@ -75,6 +93,18 @@ Responsibilities:
75
93
  - Return the controller methods.
76
94
  - Re-export the composable mount API.
77
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
+
78
108
  `src/api/PaddockSimulatorController.js` owns composable mounting.
79
109
 
80
110
  Responsibilities:
@@ -104,8 +134,12 @@ Responsibilities:
104
134
  - Race data panel rendering.
105
135
  - Safety car button.
106
136
  - Multiple package-rendered safety-car buttons bound to one simulation state.
107
- - Restart behavior.
108
- - 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.
109
143
 
110
144
  This file is still large. When changing it substantially, prefer extracting cohesive modules rather than adding unrelated responsibilities.
111
145
 
@@ -147,9 +181,9 @@ The app runtime pauses its PixiJS ticker when the race canvas is outside the vie
147
181
 
148
182
  `src/data/vehicleData.js` converts vehicle rating sheets into physical setup values.
149
183
 
150
- `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.
151
185
 
152
- `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.
153
187
 
154
188
  `src/data/demoDrivers.js` is demo/portfolio-flavored sample data. It should not become the only supported data path.
155
189
 
@@ -162,14 +196,14 @@ The app runtime pauses its PixiJS ticker when the race canvas is outside the vie
162
196
  - Safety-car control.
163
197
  - Timing tower.
164
198
  - Race canvas.
165
- - Race-data panel.
199
+ - Race-data panel, including the optional project telemetry detail variant.
166
200
  - Telemetry stack template.
167
201
  - Detached telemetry core, sector graph, lap-time table, and sector-time table.
168
202
  - Broadcast sector banner.
169
203
  - Race telemetry drawer template.
170
204
  - Car/driver overview panel.
171
205
 
172
- `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; `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, safety-car control, lower-third, sector banner, and detached telemetry components in a right drawer; the drawer's open/close state and safety-car button are owned by `F1SimulatorApp`. 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.
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.
173
207
 
174
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.
175
209
 
@@ -200,6 +234,7 @@ Current coverage includes:
200
234
  - Championship metadata.
201
235
  - Component API and callback behavior.
202
236
  - Driver controller behavior.
237
+ - Expert environment actions, observations, rewards, events, and browser adapter behavior.
203
238
  - Procedural track rendering helpers.
204
239
  - Race simulation rules.
205
240
  - Render interpolation.