@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
@@ -2,6 +2,13 @@ function buttonHiddenAttribute(isVisible) {
2
2
  return isVisible ? '' : ' hidden';
3
3
  }
4
4
 
5
+ let telemetryDrawerIdSequence = 0;
6
+
7
+ function createTelemetryDrawerId() {
8
+ telemetryDrawerIdSequence += 1;
9
+ return `paddock-telemetry-drawer-${telemetryDrawerIdSequence}`;
10
+ }
11
+
5
12
  function createLoadingMarkup(label = 'Loading') {
6
13
  return `
7
14
  <div class="paddock-loading" data-paddock-loading aria-label="${escapeHtml(label)} loading">
@@ -110,6 +117,7 @@ export function createCameraControlsMarkup({ embedded = false } = {}) {
110
117
  export function createRaceCanvasMarkup({
111
118
  includeRaceDataPanel = false,
112
119
  includeTimingTower = false,
120
+ includeTelemetrySectorBanner = false,
113
121
  timingTowerVerticalFit,
114
122
  assets,
115
123
  totalLaps,
@@ -147,6 +155,7 @@ export function createRaceCanvasMarkup({
147
155
  </div>
148
156
  ${showEmbeddedCameraControls ? createCameraControlsMarkup({ embedded: true }) : ''}
149
157
  ${includeRaceDataPanel ? createRaceDataPanelMarkup({ assets, ui }) : ''}
158
+ ${includeTelemetrySectorBanner ? createTelemetrySectorBannerMarkup({ ui }) : ''}
150
159
  <div class="race-finish-panel" data-race-finish-panel hidden aria-live="polite">
151
160
  <span>Race winner</span>
152
161
  <strong data-race-finish-winner>--</strong>
@@ -159,13 +168,17 @@ export function createRaceCanvasMarkup({
159
168
 
160
169
  export function createRaceDataPanelMarkup({ ui = {} } = {}) {
161
170
  const sizeMode = ui.raceDataBannerSize === 'auto' ? 'auto' : 'custom';
171
+ const telemetryDetail = Boolean(ui.raceDataTelemetryDetail);
172
+ const classNames = ['race-data-panel', `race-data-panel--${sizeMode}`];
173
+ if (telemetryDetail) classNames.push('race-data-panel--with-telemetry');
162
174
  return `
163
- <div class="race-data-panel race-data-panel--${sizeMode}" data-paddock-component="race-data-panel" data-race-data-panel aria-live="polite">
175
+ <div class="${classNames.join(' ')}" data-paddock-component="race-data-panel" data-race-data-panel aria-live="polite">
164
176
  <div class="race-data-copy">
165
177
  <span class="race-data-kicker" data-race-data-kicker>Project</span>
166
178
  <strong data-race-data-title>Select driver</strong>
167
179
  <span class="race-data-subtitle" data-race-data-subtitle>Race entry</span>
168
180
  </div>
181
+ ${telemetryDetail ? createRaceDataTelemetryMarkup() : ''}
169
182
  <strong class="race-data-number" data-race-data-number>--</strong>
170
183
  <button class="race-data-link" type="button" data-race-data-open>Open project</button>
171
184
  ${createLoadingMarkup('Race data')}
@@ -173,6 +186,22 @@ export function createRaceDataPanelMarkup({ ui = {} } = {}) {
173
186
  `;
174
187
  }
175
188
 
189
+ function createRaceDataTelemetryMarkup() {
190
+ return `
191
+ <div class="race-data-telemetry" data-race-data-telemetry aria-label="Project sector telemetry">
192
+ <span class="race-data-telemetry__label">Sectors</span>
193
+ <div class="race-data-telemetry__bars">
194
+ ${[1, 2, 3].map((sector) => `
195
+ <div class="telemetry-sector-bar race-data-sector-bar" data-telemetry-sector-bar="${sector}" style="--sector-fill: 0%">
196
+ <span>S${sector}</span>
197
+ <strong data-telemetry-sector-time="${sector}">--</strong>
198
+ </div>
199
+ `).join('')}
200
+ </div>
201
+ </div>
202
+ `;
203
+ }
204
+
176
205
  export function createCarDriverOverviewMarkup({ assets }) {
177
206
  const cells = Array.from({ length: 7 }, (_, index) => `
178
207
  <div class="car-overview-cell car-overview-cell--slot-${index + 1}" data-overview-field data-overview-slot="${index}">
@@ -205,9 +234,33 @@ export function createCarDriverOverviewMarkup({ assets }) {
205
234
  `;
206
235
  }
207
236
 
208
- export function createTelemetryPanelMarkup(options, { includeOverview = options.ui?.telemetryIncludesOverview !== false } = {}) {
237
+ function getTelemetryModules(ui = {}) {
238
+ const defaults = {
239
+ core: true,
240
+ sectors: true,
241
+ lapTimes: true,
242
+ sectorTimes: true,
243
+ };
244
+ const modules = ui.telemetryModules;
245
+ if (modules === false) return Object.fromEntries(Object.keys(defaults).map((name) => [name, false]));
246
+ if (Array.isArray(modules)) {
247
+ const requested = new Set(modules);
248
+ return Object.fromEntries(Object.keys(defaults).map((name) => [name, requested.has(name)]));
249
+ }
250
+ if (!modules || typeof modules !== 'object') return defaults;
251
+ return Object.fromEntries(Object.entries(defaults).map(([name, fallback]) => [
252
+ name,
253
+ modules[name] == null ? fallback : Boolean(modules[name]),
254
+ ]));
255
+ }
256
+
257
+ function getTelemetryModuleClass(componentName) {
258
+ return `sim-telemetry telemetry-component telemetry-component--${componentName}`;
259
+ }
260
+
261
+ export function createTelemetryCoreMarkup() {
209
262
  return `
210
- <aside class="sim-telemetry" data-paddock-component="telemetry-panel" aria-label="Selected car telemetry">
263
+ <section class="${getTelemetryModuleClass('core')}" data-paddock-component="telemetry-core" aria-label="Selected car core telemetry">
211
264
  <div class="telemetry-header">
212
265
  <span data-selected-code>---</span>
213
266
  <strong data-selected-name>Select car</strong>
@@ -222,8 +275,152 @@ export function createTelemetryPanelMarkup(options, { includeOverview = options.
222
275
  <div><dt>Interval</dt><dd data-telemetry-gap>--</dd></div>
223
276
  <div><dt>Leader</dt><dd data-telemetry-leader-gap>--</dd></div>
224
277
  </dl>
278
+ ${createLoadingMarkup('Core telemetry')}
279
+ </section>
280
+ `;
281
+ }
282
+
283
+ export function createTelemetrySectorsMarkup() {
284
+ return `
285
+ <section class="${getTelemetryModuleClass('sectors')} telemetry-sector-strip" data-paddock-component="telemetry-sectors" data-telemetry-sector-strip aria-label="Sector progress">
286
+ <div class="telemetry-module-header">
287
+ <span>Sector map</span>
288
+ <strong data-telemetry-current-sector>S1</strong>
289
+ </div>
290
+ <div class="telemetry-sector-bars">
291
+ ${[1, 2, 3].map((sector) => `
292
+ <div class="telemetry-sector-bar" data-telemetry-sector-bar="${sector}" style="--sector-fill: 0%">
293
+ <span>S${sector}</span>
294
+ <strong data-telemetry-sector-time="${sector}">--</strong>
295
+ </div>
296
+ `).join('')}
297
+ </div>
298
+ ${createLoadingMarkup('Sector telemetry')}
299
+ </section>
300
+ `;
301
+ }
302
+
303
+ export function createTelemetrySectorBannerMarkup() {
304
+ return `
305
+ <section class="telemetry-sector-banner" data-paddock-component="telemetry-sector-banner" data-telemetry-sector-banner aria-label="Broadcast sector telemetry">
306
+ <div class="telemetry-sector-banner__copy">
307
+ <span><b data-selected-code>--</b> sector telemetry</span>
308
+ <strong data-selected-name>Select driver</strong>
309
+ <em data-telemetry-current-sector>S1</em>
310
+ </div>
311
+ <div class="telemetry-sector-banner__bars">
312
+ ${[1, 2, 3].map((sector) => `
313
+ <div class="telemetry-sector-bar" data-telemetry-sector-bar="${sector}" style="--sector-fill: 0%">
314
+ <span>S${sector}</span>
315
+ <strong data-telemetry-sector-time="${sector}">--</strong>
316
+ </div>
317
+ `).join('')}
318
+ </div>
319
+ ${createLoadingMarkup('Sector banner')}
320
+ </section>
321
+ `;
322
+ }
323
+
324
+ export function createTelemetryLapTimesMarkup() {
325
+ return `
326
+ <section class="${getTelemetryModuleClass('lap-times')} telemetry-lap-module" data-paddock-component="telemetry-lap-times" aria-label="Lap timing">
327
+ <div class="telemetry-module-header">
328
+ <span>Lap timing</span>
329
+ <strong data-telemetry-completed-laps>0 laps</strong>
330
+ </div>
331
+ <table class="telemetry-lap-table" data-telemetry-lap-table>
332
+ <tbody>
333
+ <tr><th scope="row">Current</th><td data-telemetry-current-lap-time>--</td></tr>
334
+ <tr><th scope="row">Last</th><td data-telemetry-last-lap-time>--</td></tr>
335
+ <tr><th scope="row">Best</th><td data-telemetry-best-lap-time>--</td></tr>
336
+ </tbody>
337
+ </table>
338
+ ${createLoadingMarkup('Lap telemetry')}
339
+ </section>
340
+ `;
341
+ }
342
+
343
+ export function createTelemetrySectorTimesMarkup() {
344
+ return `
345
+ <section class="${getTelemetryModuleClass('sector-times')} telemetry-sector-table-module" data-paddock-component="telemetry-sector-times" aria-label="Sector timing table">
346
+ <div class="telemetry-module-header">
347
+ <span>Sector timing</span>
348
+ <strong>Last / Best</strong>
349
+ </div>
350
+ <table class="telemetry-sector-table" data-telemetry-sector-table>
351
+ <thead>
352
+ <tr><th scope="col">Sector</th><th scope="col">Last</th><th scope="col">Best</th></tr>
353
+ </thead>
354
+ <tbody>
355
+ ${[1, 2, 3].map((sector) => `
356
+ <tr data-telemetry-sector-row="${sector}">
357
+ <th scope="row">S${sector}</th>
358
+ <td data-telemetry-sector-last="${sector}">--</td>
359
+ <td data-telemetry-sector-best="${sector}">--</td>
360
+ </tr>
361
+ `).join('')}
362
+ </tbody>
363
+ </table>
364
+ ${createLoadingMarkup('Sector table')}
365
+ </section>
366
+ `;
367
+ }
368
+
369
+ function createTelemetryComponentMarkup(options, modules = getTelemetryModules(options.ui)) {
370
+ return `
371
+ ${modules.core ? createTelemetryCoreMarkup(options) : ''}
372
+ ${modules.sectors ? createTelemetrySectorsMarkup(options) : ''}
373
+ ${modules.lapTimes ? createTelemetryLapTimesMarkup(options) : ''}
374
+ ${modules.sectorTimes ? createTelemetrySectorTimesMarkup(options) : ''}
375
+ `;
376
+ }
377
+
378
+ export function createTelemetryPanelMarkup(options, { includeOverview = options.ui?.telemetryIncludesOverview !== false } = {}) {
379
+ const modules = getTelemetryModules(options.ui);
380
+ return `
381
+ <aside class="telemetry-stack" data-paddock-component="telemetry-stack" aria-label="Selected car telemetry stack">
382
+ ${createTelemetryComponentMarkup(options, modules)}
225
383
  ${includeOverview ? createCarDriverOverviewMarkup(options) : ''}
226
- ${createLoadingMarkup('Telemetry')}
227
384
  </aside>
228
385
  `;
229
386
  }
387
+
388
+ export function createRaceTelemetryDrawerMarkup(options, {
389
+ timingTowerVerticalFit,
390
+ drawerInitiallyOpen = false,
391
+ raceDataTelemetryDetail = options.ui?.raceDataTelemetryDetail,
392
+ } = {}) {
393
+ const openClass = drawerInitiallyOpen ? ' is-telemetry-open' : '';
394
+ const drawerId = createTelemetryDrawerId();
395
+ const drawerOptions = {
396
+ ...options,
397
+ ui: {
398
+ ...(options.ui ?? {}),
399
+ raceDataTelemetryDetail: Boolean(raceDataTelemetryDetail),
400
+ },
401
+ };
402
+ return `
403
+ <section class="race-telemetry-drawer${openClass}" data-paddock-component="race-telemetry-drawer" data-race-telemetry-drawer aria-label="Race view with telemetry drawer">
404
+ <div class="race-telemetry-drawer__race">
405
+ ${createRaceCanvasMarkup({
406
+ ...drawerOptions,
407
+ includeRaceDataPanel: true,
408
+ includeTimingTower: true,
409
+ timingTowerVerticalFit,
410
+ })}
411
+ </div>
412
+ <div class="race-telemetry-drawer__controls" aria-label="Race workbench controls">
413
+ ${createSafetyCarControlMarkup({ compact: true })}
414
+ <button class="telemetry-drawer-toggle" type="button" data-telemetry-drawer-toggle aria-expanded="${drawerInitiallyOpen ? 'true' : 'false'}" aria-controls="${drawerId}">
415
+ ${drawerInitiallyOpen ? 'Close telemetry' : 'Telemetry'}
416
+ </button>
417
+ </div>
418
+ <aside id="${drawerId}" class="telemetry-drawer" data-telemetry-drawer aria-label="Telemetry drawer" aria-hidden="${drawerInitiallyOpen ? 'false' : 'true'}"${drawerInitiallyOpen ? '' : ' inert'}>
419
+ <div class="telemetry-drawer__header" aria-hidden="true"></div>
420
+ <div class="telemetry-drawer__content">
421
+ ${createTelemetryComponentMarkup(options)}
422
+ </div>
423
+ </aside>
424
+ </section>
425
+ `;
426
+ }
package/AGENTS.md DELETED
@@ -1,86 +0,0 @@
1
- # AGENTS.md
2
-
3
- ## Project
4
-
5
- PaddockJS is an installable browser component package for an F1-style project race simulator.
6
-
7
- The package lives independently from the portfolio website. Host websites install it and mount it with:
8
-
9
- ```js
10
- import { mountF1Simulator } from '@inventure71/paddockjs';
11
- ```
12
-
13
- It can also be mounted as rearrangeable package-owned pieces with:
14
-
15
- ```js
16
- import { createPaddockSimulator } from '@inventure71/paddockjs';
17
- ```
18
-
19
- ## What Is In This Repo
20
-
21
- - `src/index.js`: public package API.
22
- - `src/api/`: public composable mounting controller.
23
- - `src/app/`: browser runtime, DOM bindings, PixiJS app lifecycle, controls, readouts, and camera orchestration.
24
- - `src/config/`: default options and bundled default asset mapping.
25
- - `src/data/`: driver/project data normalization, driver/car pairing, rating conversion, and demo data.
26
- - `src/rendering/`: PixiJS track rendering helpers and render interpolation.
27
- - `src/simulation/`: race rules, driver AI, vehicle physics, track model, and math utilities.
28
- - `src/ui/`: generated markup for the all-in-one shell and individually mounted UI surfaces.
29
- - `src/__tests__/`: package tests.
30
- - `assets/`: simulator-owned default assets.
31
- - `docs/`: package specs and design documentation.
32
- - `INSTALL_AND_UPDATE.md`: install/update workflow.
33
-
34
- ## Required Documentation Discipline
35
-
36
- When changing simulator behavior, update docs in the same change.
37
-
38
- Use this mapping:
39
-
40
- - Public API, lifecycle, install assumptions: update `docs/system_specs.md` and `README.md` if needed.
41
- - Race behavior, DRS, safety car, starts, physics, collisions: update `docs/rules.md`.
42
- - Vocabulary or domain model: update `docs/concepts.md`.
43
- - Driver/entry/options/assets callback shapes: update `docs/data_contract.md`.
44
- - Module ownership or data/control flow: update `docs/architecture.md`.
45
- - Local install/update workflow: update `INSTALL_AND_UPDATE.md`.
46
-
47
- Do not leave docs describing old behavior after code changes.
48
-
49
- ## Linear Work Logging
50
-
51
- For any implementation that is more than a trivial/no-op change, log the work in the PaddockJS project on Linear.
52
-
53
- The Linear update must include:
54
-
55
- - what task was done
56
- - how it was solved
57
- - any important verification or remaining follow-up
58
-
59
- ## Engineering Rules
60
-
61
- - Preserve the package boundary: PaddockJS should stay reusable and host-agnostic.
62
- - Host-specific routing belongs in `onDriverOpen(driver)`, not package internals.
63
- - Host-specific project data should be passed as `drivers` and `entries`, not hardcoded into the runtime.
64
- - The package should own default simulator assets so hosts do not need to copy them.
65
- - Treat the timing board width and broadcast proportions as package-owned. Do not resize or retune timing-board width as a casual fix; preserve its intended width and solve layout bugs through internal column/content constraints unless the user explicitly asks to redesign the timing board sizing.
66
- - Prefer small, focused modules when extracting from `F1SimulatorApp.js`.
67
- - Keep simulation logic deterministic for the same seed, track seed, drivers, entries, and rules.
68
- - Do not claim completion without running verification.
69
-
70
- ## Verification
71
-
72
- Run before claiming package changes are complete:
73
-
74
- ```bash
75
- npm run check
76
- ```
77
-
78
- For portfolio integration changes, also run from the portfolio repo:
79
-
80
- ```bash
81
- cd /Users/inventure71/VSProjects/Inventure71.github.io
82
- npm install ../PaddockJS
83
- npm run check
84
- ```
85
-
86
- Browser behavior changes need a browser smoke test against the host page.
package/src/standalone.js DELETED
@@ -1,15 +0,0 @@
1
- import { DEMO_PROJECT_DRIVERS, mountF1Simulator } from './index.js';
2
-
3
- const root = document.getElementById('f1-simulator-root');
4
-
5
- if (root) {
6
- mountF1Simulator(root, {
7
- drivers: DEMO_PROJECT_DRIVERS,
8
- onDriverOpen(driver) {
9
- if (driver.link) window.location.href = driver.link;
10
- },
11
- }).catch((error) => {
12
- root.dataset.simulatorState = 'error';
13
- console.error('F1 simulator failed to initialize.', error);
14
- });
15
- }