@inventure71/paddockjs 0.2.0 → 1.0.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 (63) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/INSTALL_AND_UPDATE.md +165 -0
  3. package/README.md +217 -20
  4. package/docs/architecture.md +84 -15
  5. package/docs/concepts.md +20 -3
  6. package/docs/data_contract.md +412 -20
  7. package/docs/data_model.md +2 -1
  8. package/docs/index.md +1 -0
  9. package/docs/learnings.md +1 -1
  10. package/docs/rules.md +140 -19
  11. package/docs/system_specs.md +74 -20
  12. package/docs/training.md +206 -0
  13. package/examples/train-basic-policy.mjs +276 -0
  14. package/examples/trainingData.mjs +133 -0
  15. package/package.json +18 -8
  16. package/src/api/PaddockSimulatorController.js +39 -4
  17. package/src/app/BrowserExpertAdapter.js +32 -0
  18. package/src/app/F1SimulatorApp.js +1251 -146
  19. package/src/app/domBindings.js +10 -2
  20. package/src/config/defaultOptions.js +11 -3
  21. package/src/data/championship.js +13 -0
  22. package/src/data/normalizeDrivers.js +7 -2
  23. package/src/environment/actions.js +68 -0
  24. package/src/environment/episode.js +17 -0
  25. package/src/environment/evaluation.js +184 -0
  26. package/src/environment/events.js +17 -0
  27. package/src/environment/index.d.ts +521 -0
  28. package/src/environment/index.js +15 -0
  29. package/src/environment/observationOptions.js +12 -0
  30. package/src/environment/observations.js +284 -0
  31. package/src/environment/options.js +157 -0
  32. package/src/environment/recorder.js +28 -0
  33. package/src/environment/rewards.js +69 -0
  34. package/src/environment/runtime.js +175 -0
  35. package/src/environment/scenarios.js +220 -0
  36. package/src/environment/sensors.js +367 -0
  37. package/src/environment/specs.js +163 -0
  38. package/src/environment/workerProtocol.js +52 -0
  39. package/src/index.d.ts +405 -6
  40. package/src/index.js +27 -2
  41. package/src/rendering/proceduralTrackAsset.js +343 -24
  42. package/src/simulation/collisionGeometry.js +214 -0
  43. package/src/simulation/driverController.js +431 -65
  44. package/src/simulation/raceSimulation.js +2140 -171
  45. package/src/simulation/rules/collisionSteward.js +69 -0
  46. package/src/simulation/rules/penaltyLedger.js +153 -0
  47. package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
  48. package/src/simulation/rules/tireRequirementSteward.js +29 -0
  49. package/src/simulation/rules/trackLimitsSteward.js +85 -0
  50. package/src/simulation/rulesConfig.js +352 -0
  51. package/src/simulation/trackModel.js +1154 -76
  52. package/src/simulation/units.js +6 -4
  53. package/src/simulation/vehicleGeometry.js +247 -0
  54. package/src/simulation/vehiclePhysics.js +82 -50
  55. package/src/simulation/wheelSurface.js +338 -0
  56. package/src/styles.css +369 -67
  57. package/src/ui/bannerTemplates.js +69 -0
  58. package/src/ui/componentTemplates.js +45 -81
  59. package/src/ui/raceControlStatusBanner.js +39 -0
  60. package/src/ui/shellTemplate.js +1 -1
  61. package/src/ui/templateUtils.js +23 -0
  62. package/AGENTS.md +0 -86
  63. package/src/standalone.js +0 -15
@@ -0,0 +1,206 @@
1
+ # Bring Your Own Model
2
+
3
+ PaddockJS does not train models for you. It provides a simulator environment contract so users can train with any toolchain, then run the resulting policy in the visual simulator.
4
+
5
+ ## Boundary
6
+
7
+ PaddockJS owns:
8
+
9
+ - deterministic simulator stepping
10
+ - observations
11
+ - normalized actions
12
+ - events
13
+ - optional reward callback hooks
14
+ - race-rule overrides for the existing simulator rules
15
+ - browser expert stepping
16
+ - sensor visualization
17
+
18
+ Users own:
19
+
20
+ - ML framework choice
21
+ - training algorithm
22
+ - model weights
23
+ - model storage
24
+ - model loading
25
+ - reward design
26
+
27
+ ## Supported Now
28
+
29
+ The current expert API is a JavaScript environment contract. It supports:
30
+
31
+ - `createPaddockEnvironment()` from `@inventure71/paddockjs/environment`
32
+ - explicit `controlledDrivers`
33
+ - normalized actions: `steering`, `throttle`, `brake`, optional `pitIntent`, and optional `pitCompound`
34
+ - manual stepping with optional `frameSkip`
35
+ - object observations in real units plus a versioned numeric vector and schema, including rays, nearby cars, track lookahead/curvature, pit-lane surface, pit target compound, pit service state, pit-lane open state, and red-flag state
36
+ - full simulator state under `result.state.snapshot`
37
+ - global and per-controlled-driver events
38
+ - optional host-owned `reward(context)` callbacks, or no reward
39
+ - `getActionSpec()` and `getObservationSpec()`
40
+ - scenarios with `participants: 'all'`, `'controlled-only'`, or an explicit driver-id list
41
+ - reset-only scenario placement through `scenario.preset`, `scenario.placements`, and `scenario.traffic`
42
+ - neutral rollout recording through `createRolloutRecorder()`
43
+ - deterministic evaluation metrics through `runEnvironmentEvaluation()` and `createEvaluationTracker()`
44
+ - a JSON-serializable worker protocol wrapper through `createEnvironmentWorkerProtocol()`
45
+ - built-in AI for non-controlled cars
46
+ - browser expert mode through `mountF1Simulator(..., { expert })`
47
+ - opt-in browser ray visualization with `expert.visualizeSensors`
48
+
49
+ The environment also accepts `rules` as a narrow override of the existing race rules, such as `standingStart: false` for training loops. Rule overrides change simulator behavior; keep them fixed when comparing policy runs.
50
+
51
+ ## Deferred
52
+
53
+ These are not part of the current package API:
54
+
55
+ - Python Gymnasium wrapper
56
+ - static obstacle/ghost-car scenario modes
57
+ - debug mutation APIs for arbitrary simulator state
58
+ - assisted-control modes that blend model output with built-in AI
59
+ - model loading, model storage, model registries, or trained policies
60
+
61
+ Those features need separate design work because they change scenario ownership, reproducibility, and the safety boundary between user experiments and package internals. The worker protocol is intentionally only a message wrapper around the JavaScript environment; it is not a Gymnasium package and does not choose a Python RL stack.
62
+
63
+ ## Policy Shape
64
+
65
+ Use this convention for browser playback:
66
+
67
+ ```js
68
+ const policy = {
69
+ predict(observation) {
70
+ return {
71
+ steering: 0,
72
+ throttle: 1,
73
+ brake: 0,
74
+ };
75
+ },
76
+ };
77
+ ```
78
+
79
+ `predict()` receives one controlled driver's observation. It returns normalized controls:
80
+
81
+ - `steering`: `-1` full left, `1` full right
82
+ - `throttle`: `0` to `1`
83
+ - `brake`: `0` to `1`
84
+ - `pitIntent`: optional `0`, `1`, or `2`; `0` means no pit request and is accepted as a no-op even when pit stops are disabled, `1` means keep trying until a free-enough pit-entry window appears, and `2` means commit to entering at the next pit-entry window even if pit-lane capacity or gap checks would block mode `1`
85
+ - `pitCompound`: optional tire target such as `'S'`, `'M'`, or `'H'`; it must be one of `rules.modules.tireStrategy.compounds`
86
+
87
+ Controlled drivers do not receive tire-threshold automatic pit calls from the built-in strategy. A model must request a stop with `pitIntent`, and may choose the tire with `pitCompound`; then the simulator owns the pit entry, queue, service, penalty hold, tire change, and pit exit sequence until `pitStopStatus` returns to `completed`. For deterministic training, keep `rules.modules.pitStops.variability.enabled` false or set `rules.modules.pitStops.variability.perfect: true`; when variability is enabled without `perfect`, team `pitCrew` speed, consistency, and reliability affect service time from the same seeded simulation RNG.
88
+
89
+ `observation.object.self.onTrack` follows the simulator's wheel-level legality rules: track, kerb, and legal pit-lane/box surfaces are on-track for reward/observation purposes, while gravel, grass, and barrier surfaces are off-track. Invalid `observation.lookaheadMeters` values fall back to `[20, 50, 100, 150]` so fixed observation schemas stay usable.
90
+
91
+ ## Headless Training Loop
92
+
93
+ ```js
94
+ import { createPaddockEnvironment } from '@inventure71/paddockjs/environment';
95
+
96
+ const env = createPaddockEnvironment({
97
+ drivers,
98
+ entries,
99
+ controlledDrivers: ['budget'],
100
+ frameSkip: 4,
101
+ rules: {
102
+ standingStart: false,
103
+ },
104
+ reward: myReward,
105
+ });
106
+
107
+ let result = env.reset();
108
+ while (!result.done) {
109
+ const observation = result.observation.budget;
110
+ const action = policy.predict(observation);
111
+ result = env.step({ budget: action });
112
+ }
113
+ ```
114
+
115
+ ## Scenario Reset Control
116
+
117
+ Scenario placement belongs to environment reset, not to model control. It is useful for deterministic training or evaluation starts such as cornering, recovery, overtaking packs, and pit entry:
118
+
119
+ ```js
120
+ const env = createPaddockEnvironment({
121
+ drivers,
122
+ entries,
123
+ controlledDrivers: ['budget'],
124
+ scenario: {
125
+ preset: 'off-track-recovery',
126
+ placements: {
127
+ budget: {
128
+ distanceMeters: 420,
129
+ offsetMeters: 16,
130
+ speedKph: 65,
131
+ headingErrorRadians: 0.4,
132
+ },
133
+ },
134
+ traffic: [
135
+ {
136
+ driverId: 'alpha',
137
+ relativeTo: 'budget',
138
+ deltaDistanceMeters: 24,
139
+ offsetMeters: -1.5,
140
+ speedKph: 68,
141
+ },
142
+ ],
143
+ },
144
+ });
145
+ ```
146
+
147
+ Supported fixed presets are `cornering`, `off-track-recovery`, `overtaking-pack`, and `pit-entry`. Explicit `placements` override preset placement for the same driver. `traffic` places cars relative to another placed or existing car. After reset, `step(actions)` still advances controlled cars only through the normal action contract.
148
+
149
+ ## Rollouts And Evaluation
150
+
151
+ PaddockJS can export neutral transition data without owning a trainer:
152
+
153
+ ```js
154
+ import { createRolloutRecorder } from '@inventure71/paddockjs/environment';
155
+
156
+ const recorder = createRolloutRecorder();
157
+ let result = env.reset();
158
+ const action = { budget: policy.predict(result.observation.budget) };
159
+ const next = env.step(action);
160
+ recorder.recordStep(result, action, next);
161
+ ```
162
+
163
+ Each transition has `{ observation, action, reward, nextObservation, terminated, truncated, info }`. If the environment has no reward callback, `reward` is still `null`; the recorder does not invent one.
164
+
165
+ Deterministic evaluation helpers report simulator quality metrics such as distance, off-track steps, contacts, recovery success, pass count, and first lap time when available. They do not update a model or compute rewards.
166
+
167
+ ## Example Rewards
168
+
169
+ `createProgressReward()` is exported for examples and quick smoke tests, but it is non-canonical demo code. It is not the official reward function and should not be treated as the recommended objective for every user. Real training code should pass a domain-specific `reward(context)` or omit rewards entirely when collecting observations or imitation data.
170
+
171
+ ## Visual Playback Loop
172
+
173
+ ```js
174
+ import { mountF1Simulator } from '@inventure71/paddockjs';
175
+
176
+ const simulator = await mountF1Simulator(root, {
177
+ drivers,
178
+ entries,
179
+ expert: {
180
+ enabled: true,
181
+ controlledDrivers: ['budget'],
182
+ frameSkip: 4,
183
+ visualizeSensors: { rays: true },
184
+ },
185
+ });
186
+
187
+ function frame() {
188
+ const observation = simulator.expert.getObservation().budget;
189
+ const action = policy.predict(observation);
190
+ simulator.expert.step({ budget: action });
191
+ requestAnimationFrame(frame);
192
+ }
193
+
194
+ frame();
195
+ ```
196
+
197
+ ## Specs
198
+
199
+ Use specs to connect external training code without guessing:
200
+
201
+ ```js
202
+ const actionSpec = env.getActionSpec();
203
+ const observationSpec = env.getObservationSpec();
204
+ ```
205
+
206
+ These specs describe the action ranges, controlled drivers, ray layout, nearby-car limits, and vector schema.
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createPaddockEnvironment, createProgressReward } from '@inventure71/paddockjs/environment';
4
+ import { TRAINING_DEMO_DRIVERS, TRAINING_DEMO_ENTRIES } from './trainingData.mjs';
5
+
6
+ const DEFAULT_OPTIONS = Object.freeze({
7
+ generations: 4,
8
+ candidates: 5,
9
+ episodes: 1,
10
+ steps: 240,
11
+ frameSkip: 4,
12
+ seed: 71,
13
+ traffic: false,
14
+ evalOnly: false,
15
+ });
16
+
17
+ const INITIAL_POLICY = Object.freeze({
18
+ steeringHeading: -1.2,
19
+ steeringOffset: -0.35,
20
+ steeringRayBalance: 0.45,
21
+ throttleBias: 0.72,
22
+ throttleFrontClearance: 0.28,
23
+ throttleSpeedDamping: 0.18,
24
+ brakeBias: 0,
25
+ brakeFrontDanger: 0.65,
26
+ brakeOffTrack: 0.2,
27
+ });
28
+
29
+ const POLICY_KEYS = Object.keys(INITIAL_POLICY);
30
+
31
+ main();
32
+
33
+ function main() {
34
+ const options = parseCliArgs(process.argv.slice(2));
35
+ if (options.help) {
36
+ printHelp();
37
+ return;
38
+ }
39
+
40
+ const driverId = TRAINING_DEMO_DRIVERS[0].id;
41
+ const rng = createSeededRandom(options.seed);
42
+ const reward = createProgressReward();
43
+ const evaluate = (policy, seedOffset = 0) => evaluatePolicy({
44
+ policy,
45
+ driverId,
46
+ reward,
47
+ options,
48
+ seedOffset,
49
+ });
50
+
51
+ if (options.evalOnly) {
52
+ const baseline = evaluate(INITIAL_POLICY);
53
+ printSummary({ options, driverId, bestPolicy: INITIAL_POLICY, best: baseline });
54
+ return;
55
+ }
56
+
57
+ let bestPolicy = { ...INITIAL_POLICY };
58
+ let best = evaluate(bestPolicy);
59
+
60
+ console.log(formatGenerationLine(0, best));
61
+
62
+ for (let generation = 1; generation <= options.generations; generation += 1) {
63
+ const mutationScale = 0.45 / Math.sqrt(generation);
64
+ const candidates = [{ policy: bestPolicy, scoreSeedOffset: generation * 1000 }];
65
+
66
+ for (let index = 1; index < options.candidates; index += 1) {
67
+ candidates.push({
68
+ policy: mutatePolicy(bestPolicy, rng, mutationScale),
69
+ scoreSeedOffset: generation * 1000 + index * 100,
70
+ });
71
+ }
72
+
73
+ for (const candidate of candidates) {
74
+ const score = evaluate(candidate.policy, candidate.scoreSeedOffset);
75
+ if (score.averageReward > best.averageReward) {
76
+ best = score;
77
+ bestPolicy = candidate.policy;
78
+ }
79
+ }
80
+
81
+ console.log(formatGenerationLine(generation, best));
82
+ }
83
+
84
+ printSummary({ options, driverId, bestPolicy, best });
85
+ }
86
+
87
+ function evaluatePolicy({ policy, driverId, reward, options, seedOffset = 0 }) {
88
+ let totalReward = 0;
89
+ let totalDistanceMeters = 0;
90
+ let totalCollisions = 0;
91
+ let totalOffTrackSteps = 0;
92
+ let completedEpisodes = 0;
93
+
94
+ for (let episode = 0; episode < options.episodes; episode += 1) {
95
+ const env = createTrainingEnvironment({ driverId, reward, options, episode, seedOffset });
96
+ let result = env.reset();
97
+ let episodeReward = 0;
98
+
99
+ for (let step = 0; step < options.steps && !result.done; step += 1) {
100
+ const observation = result.observation[driverId];
101
+ const action = chooseAction(policy, observation);
102
+ result = env.step({ [driverId]: action });
103
+ episodeReward += result.reward?.[driverId] ?? 0;
104
+ totalCollisions += result.events.filter((event) => event.type === 'collision').length;
105
+ if (result.observation[driverId]?.object?.self?.onTrack === false) totalOffTrackSteps += 1;
106
+ }
107
+
108
+ const car = result.state.snapshot.cars.find((entry) => entry.id === driverId);
109
+ totalReward += episodeReward;
110
+ totalDistanceMeters += Number(car?.distanceMeters ?? 0);
111
+ completedEpisodes += 1;
112
+ env.destroy();
113
+ }
114
+
115
+ return {
116
+ averageReward: totalReward / Math.max(1, completedEpisodes),
117
+ averageDistanceMeters: totalDistanceMeters / Math.max(1, completedEpisodes),
118
+ collisions: totalCollisions,
119
+ offTrackSteps: totalOffTrackSteps,
120
+ };
121
+ }
122
+
123
+ function createTrainingEnvironment({ driverId, reward, options, episode, seedOffset }) {
124
+ const drivers = options.traffic ? TRAINING_DEMO_DRIVERS.slice(0, 8) : TRAINING_DEMO_DRIVERS.slice(0, 1);
125
+ return createPaddockEnvironment({
126
+ drivers,
127
+ entries: TRAINING_DEMO_ENTRIES,
128
+ controlledDrivers: [driverId],
129
+ seed: options.seed + seedOffset + episode,
130
+ trackSeed: options.seed + 2026 + seedOffset + episode,
131
+ totalLaps: 3,
132
+ frameSkip: options.frameSkip,
133
+ scenario: {
134
+ participants: options.traffic ? 'all' : 'controlled-only',
135
+ nonControlled: 'ai',
136
+ },
137
+ episode: {
138
+ maxSteps: options.steps,
139
+ endOnRaceFinish: true,
140
+ },
141
+ rules: {
142
+ standingStart: false,
143
+ },
144
+ reward,
145
+ });
146
+ }
147
+
148
+ function chooseAction(policy, observation) {
149
+ const self = observation.object.self;
150
+ const frontRay = getRayDistance(observation, 0);
151
+ const leftRay = getRayDistance(observation, -60);
152
+ const rightRay = getRayDistance(observation, 60);
153
+ const frontDanger = clamp01((35 - frontRay) / 35);
154
+ const rayBalanceMeters = rightRay - leftRay;
155
+
156
+ const steering = clamp(
157
+ policy.steeringHeading * self.trackHeadingErrorRadians +
158
+ policy.steeringOffset * (self.trackOffsetMeters / 12) +
159
+ policy.steeringRayBalance * (rayBalanceMeters / 120),
160
+ -1,
161
+ 1,
162
+ );
163
+ const throttle = clamp01(
164
+ policy.throttleBias +
165
+ policy.throttleFrontClearance * (frontRay / 120) -
166
+ policy.throttleSpeedDamping * (self.speedKph / 320) -
167
+ (self.onTrack ? 0 : 0.35),
168
+ );
169
+ const brake = clamp01(
170
+ policy.brakeBias +
171
+ policy.brakeFrontDanger * frontDanger +
172
+ (self.onTrack ? 0 : policy.brakeOffTrack),
173
+ );
174
+
175
+ return { steering, throttle, brake };
176
+ }
177
+
178
+ function getRayDistance(observation, angleDegrees) {
179
+ const ray = observation.object.rays.find((entry) => entry.angleDegrees === angleDegrees);
180
+ return Number(ray?.track?.distanceMeters ?? ray?.lengthMeters ?? 120);
181
+ }
182
+
183
+ function mutatePolicy(policy, rng, scale) {
184
+ return Object.fromEntries(POLICY_KEYS.map((key) => [
185
+ key,
186
+ policy[key] + randomNormal(rng) * scale,
187
+ ]));
188
+ }
189
+
190
+ function parseCliArgs(argv) {
191
+ const options = { ...DEFAULT_OPTIONS };
192
+
193
+ for (const arg of argv) {
194
+ const [name, rawValue] = arg.split('=');
195
+ if (name === '--help' || name === '-h') options.help = true;
196
+ else if (name === '--eval-only') options.evalOnly = true;
197
+ else if (name === '--traffic') options.traffic = true;
198
+ else if (name === '--no-traffic') options.traffic = false;
199
+ else if (name === '--generations') options.generations = parsePositiveInt(rawValue, name);
200
+ else if (name === '--candidates') options.candidates = parsePositiveInt(rawValue, name);
201
+ else if (name === '--episodes') options.episodes = parsePositiveInt(rawValue, name);
202
+ else if (name === '--steps') options.steps = parsePositiveInt(rawValue, name);
203
+ else if (name === '--frame-skip') options.frameSkip = parsePositiveInt(rawValue, name);
204
+ else if (name === '--seed') options.seed = parsePositiveInt(rawValue, name);
205
+ else throw new Error(`Unknown option: ${name}`);
206
+ }
207
+
208
+ return options;
209
+ }
210
+
211
+ function parsePositiveInt(value, optionName) {
212
+ const parsed = Number.parseInt(value, 10);
213
+ if (!Number.isFinite(parsed) || parsed <= 0) {
214
+ throw new Error(`${optionName} requires a positive integer value.`);
215
+ }
216
+ return parsed;
217
+ }
218
+
219
+ function createSeededRandom(seed) {
220
+ let state = seed >>> 0;
221
+ return function random() {
222
+ state += 0x6D2B79F5;
223
+ let value = state;
224
+ value = Math.imul(value ^ (value >>> 15), value | 1);
225
+ value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
226
+ return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
227
+ };
228
+ }
229
+
230
+ function randomNormal(rng) {
231
+ const a = Math.max(Number.EPSILON, rng());
232
+ const b = Math.max(Number.EPSILON, rng());
233
+ return Math.sqrt(-2 * Math.log(a)) * Math.cos(2 * Math.PI * b);
234
+ }
235
+
236
+ function clamp(value, min, max) {
237
+ return Math.min(max, Math.max(min, value));
238
+ }
239
+
240
+ function clamp01(value) {
241
+ return clamp(value, 0, 1);
242
+ }
243
+
244
+ function formatGenerationLine(generation, score) {
245
+ return [
246
+ `generation=${generation}`,
247
+ `reward=${score.averageReward.toFixed(2)}`,
248
+ `distanceMeters=${score.averageDistanceMeters.toFixed(1)}`,
249
+ `collisions=${score.collisions}`,
250
+ `offTrackSteps=${score.offTrackSteps}`,
251
+ ].join(' ');
252
+ }
253
+
254
+ function printSummary({ options, driverId, bestPolicy, best }) {
255
+ console.log(JSON.stringify({
256
+ driverId,
257
+ options,
258
+ best,
259
+ bestPolicy,
260
+ }, null, 2));
261
+ }
262
+
263
+ function printHelp() {
264
+ console.log(`Usage: node examples/train-basic-policy.mjs [options]
265
+
266
+ Options:
267
+ --generations=N Random-search generations. Default: ${DEFAULT_OPTIONS.generations}
268
+ --candidates=N Candidate policies per generation. Default: ${DEFAULT_OPTIONS.candidates}
269
+ --episodes=N Episodes per policy evaluation. Default: ${DEFAULT_OPTIONS.episodes}
270
+ --steps=N Maximum manual environment steps per episode. Default: ${DEFAULT_OPTIONS.steps}
271
+ --frame-skip=N Simulator frames advanced by each step(action). Default: ${DEFAULT_OPTIONS.frameSkip}
272
+ --seed=N Deterministic random seed. Default: ${DEFAULT_OPTIONS.seed}
273
+ --traffic Keep built-in AI traffic on track during training.
274
+ --eval-only Evaluate the starter policy without training.
275
+ `);
276
+ }
@@ -0,0 +1,133 @@
1
+ export const TRAINING_DEMO_DRIVERS = [
2
+ {
3
+ id: 'budget',
4
+ code: 'BUD',
5
+ icon: 'BB',
6
+ name: 'Budget Buddy',
7
+ color: '#ff2d55',
8
+ tire: 'M',
9
+ raceData: ['AI finance coach', 'Python + LLM', 'Budget guardrails'],
10
+ },
11
+ {
12
+ id: 'noir',
13
+ code: 'NOI',
14
+ icon: 'NN',
15
+ name: 'Neural Noir',
16
+ color: '#ffb000',
17
+ tire: 'M',
18
+ raceData: ['Procedural mystery', 'Gemini storytelling', 'Unreal loop'],
19
+ },
20
+ {
21
+ id: 'vinyl',
22
+ code: 'HOL',
23
+ icon: 'HV',
24
+ name: 'HoloVinyl',
25
+ color: '#00c2a8',
26
+ tire: 'H',
27
+ raceData: ['Touchless DJ control', 'Computer vision', 'Hand tracking'],
28
+ },
29
+ {
30
+ id: 'drsorriso',
31
+ code: 'DRS',
32
+ icon: 'DR',
33
+ name: 'DrSorrisoDonations',
34
+ color: '#e10600',
35
+ tire: 'S',
36
+ raceData: ['Donation intake', 'Database workflow', 'Automation tool'],
37
+ },
38
+ {
39
+ id: 'victoria',
40
+ code: 'VIC',
41
+ icon: 'VI',
42
+ name: 'VictorIA',
43
+ color: '#00a3ff',
44
+ tire: 'M',
45
+ raceData: ['Game AI sandbox', 'Robotics ideas', 'Vision systems'],
46
+ },
47
+ {
48
+ id: 'reminderz',
49
+ code: 'REM',
50
+ icon: 'RZ',
51
+ name: 'ReminderZ',
52
+ color: '#a3e635',
53
+ tire: 'H',
54
+ raceData: ['Knowledge workflow', 'Personal graph', 'Capture tool'],
55
+ },
56
+ {
57
+ id: 'clipclop',
58
+ code: 'CLP',
59
+ icon: 'CC',
60
+ name: 'ClipClop',
61
+ color: '#f97316',
62
+ tire: 'S',
63
+ raceData: ['Mac + Android sync', 'Local clipboard', 'Productivity app'],
64
+ },
65
+ {
66
+ id: 'core',
67
+ code: 'COR',
68
+ icon: 'AI',
69
+ name: 'Core Conflict',
70
+ color: '#38bdf8',
71
+ tire: 'M',
72
+ raceData: ['Runtime AI agents', 'Multiplayer state', 'Game systems'],
73
+ },
74
+ ];
75
+
76
+ export const TRAINING_DEMO_ENTRIES = [
77
+ {
78
+ driverId: 'budget',
79
+ driverNumber: 71,
80
+ timingName: 'Budget',
81
+ driver: { pace: 52, racecraft: 74, aggression: 38, riskTolerance: 47, patience: 81, consistency: 86 },
82
+ vehicle: { id: 'budget-bb01', name: 'BB-01 Ledger', power: 48, braking: 72, aero: 55, dragEfficiency: 66, mechanicalGrip: 63, weightControl: 58, tireCare: 82 },
83
+ },
84
+ {
85
+ driverId: 'noir',
86
+ driverNumber: 13,
87
+ timingName: 'Noir',
88
+ driver: { pace: 68, racecraft: 83, aggression: 61, riskTolerance: 72, patience: 57, consistency: 64 },
89
+ vehicle: { id: 'noir-nn13', name: 'NN-13 Shadow', power: 64, braking: 60, aero: 73, dragEfficiency: 57, mechanicalGrip: 69, weightControl: 54, tireCare: 51 },
90
+ },
91
+ {
92
+ driverId: 'vinyl',
93
+ driverNumber: 33,
94
+ timingName: 'HoloVinyl',
95
+ driver: { pace: 71, racecraft: 66, aggression: 52, riskTolerance: 59, patience: 62, consistency: 58 },
96
+ vehicle: { id: 'vinyl-hv33', name: 'HV-33 Groove', power: 70, braking: 49, aero: 58, dragEfficiency: 74, mechanicalGrip: 52, weightControl: 68, tireCare: 46 },
97
+ },
98
+ {
99
+ driverId: 'drsorriso',
100
+ driverNumber: 55,
101
+ timingName: 'DrSorriso',
102
+ driver: { pace: 46, racecraft: 69, aggression: 44, riskTolerance: 43, patience: 76, consistency: 73 },
103
+ vehicle: { id: 'sorriso-ds55', name: 'DS-55 Intake', power: 54, braking: 67, aero: 62, dragEfficiency: 49, mechanicalGrip: 71, weightControl: 45, tireCare: 69 },
104
+ },
105
+ {
106
+ driverId: 'victoria',
107
+ driverNumber: 11,
108
+ timingName: 'VictorIA',
109
+ driver: { pace: 78, racecraft: 79, aggression: 58, riskTolerance: 65, patience: 55, consistency: 68 },
110
+ vehicle: { id: 'victoria-vi11', name: 'VI-11 Vector', power: 76, braking: 58, aero: 65, dragEfficiency: 63, mechanicalGrip: 61, weightControl: 60, tireCare: 48 },
111
+ },
112
+ {
113
+ driverId: 'reminderz',
114
+ driverNumber: 7,
115
+ timingName: 'ReminderZ',
116
+ driver: { pace: 41, racecraft: 61, aggression: 34, riskTolerance: 39, patience: 85, consistency: 80 },
117
+ vehicle: { id: 'reminderz-rz07', name: 'RZ-07 Recall', power: 42, braking: 76, aero: 50, dragEfficiency: 72, mechanicalGrip: 58, weightControl: 63, tireCare: 88 },
118
+ },
119
+ {
120
+ driverId: 'clipclop',
121
+ driverNumber: 14,
122
+ timingName: 'ClipClop',
123
+ driver: { pace: 73, racecraft: 88, aggression: 76, riskTolerance: 78, patience: 48, consistency: 61 },
124
+ vehicle: { id: 'clipclop-cc14', name: 'CC-14 Sync', power: 68, braking: 63, aero: 59, dragEfficiency: 61, mechanicalGrip: 78, weightControl: 56, tireCare: 44 },
125
+ },
126
+ {
127
+ driverId: 'core',
128
+ driverNumber: 91,
129
+ timingName: 'Core',
130
+ driver: { pace: 82, racecraft: 85, aggression: 67, riskTolerance: 74, patience: 53, consistency: 72 },
131
+ vehicle: { id: 'core-ai91', name: 'AI-91 Core', power: 80, braking: 61, aero: 72, dragEfficiency: 59, mechanicalGrip: 75, weightControl: 51, tireCare: 50 },
132
+ },
133
+ ];
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@inventure71/paddockjs",
3
- "version": "0.2.0",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
- "description": "A data-driven browser F1 simulator component for project portfolios.",
5
+ "description": "A data-driven browser F1 simulator component for host websites.",
6
6
  "license": "Apache-2.0",
7
7
  "author": "Matteo Giorgetti",
8
8
  "homepage": "https://github.com/Inventure71/PaddockJS#readme",
@@ -30,7 +30,7 @@
30
30
  "access": "public"
31
31
  },
32
32
  "engines": {
33
- "node": ">=20"
33
+ "node": ">=20.19.0"
34
34
  },
35
35
  "main": "src/index.js",
36
36
  "types": "src/index.d.ts",
@@ -40,45 +40,55 @@
40
40
  "import": "./src/index.js",
41
41
  "default": "./src/index.js"
42
42
  },
43
+ "./environment": {
44
+ "types": "./src/environment/index.d.ts",
45
+ "import": "./src/environment/index.js",
46
+ "default": "./src/environment/index.js"
47
+ },
43
48
  "./styles.css": "./src/styles.css"
44
49
  },
45
50
  "files": [
46
- "AGENTS.md",
47
51
  "CHANGELOG.md",
52
+ "INSTALL_AND_UPDATE.md",
48
53
  "LICENSE",
49
54
  "assets",
50
- "docs",
55
+ "docs/*.md",
56
+ "examples",
51
57
  "src/api",
52
58
  "src/app",
53
59
  "src/config",
54
60
  "src/data",
61
+ "src/environment",
55
62
  "src/rendering",
56
63
  "src/simulation",
57
64
  "src/ui",
58
65
  "src/index.d.ts",
59
66
  "src/index.js",
60
- "src/standalone.js",
61
67
  "src/styles.css",
62
68
  "README.md"
63
69
  ],
64
70
  "scripts": {
65
71
  "test": "vitest run src",
66
72
  "types:check": "tsc -p tsconfig.types.json --noEmit",
67
- "check": "npm test && npm run types:check && npm run pack:dry && npm run showcase:ci",
73
+ "check": "npm test && npm run types:check && npm run pack:dry && npm run consumer:smoke && npm run showcase:ci && npm run browser:smoke",
68
74
  "pack:dry": "npm pack --dry-run",
75
+ "consumer:smoke": "node scripts/consumer-package-smoke.mjs",
76
+ "browser:smoke": "node smoke/browser-smoke.mjs",
69
77
  "showcase:install": "npm --prefix local-preview ci",
70
78
  "showcase:dev": "npm --prefix local-preview run dev",
71
79
  "showcase:build": "npm --prefix local-preview run build",
72
80
  "showcase:ci": "npm --prefix local-preview ci && npm --prefix local-preview run build",
73
81
  "changeset": "changeset",
74
82
  "version-packages": "changeset version",
75
- "release": "npm publish --provenance"
83
+ "release": "npm publish --provenance",
84
+ "example:train": "node examples/train-basic-policy.mjs"
76
85
  },
77
86
  "dependencies": {
78
87
  "pixi.js": "8.18.1"
79
88
  },
80
89
  "devDependencies": {
81
90
  "@changesets/cli": "2.29.7",
91
+ "playwright": "^1.57.0",
82
92
  "typescript": "5.9.3",
83
93
  "vitest": "4.1.5"
84
94
  }