@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
@@ -0,0 +1,104 @@
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
+ - browser expert stepping
15
+ - sensor visualization
16
+
17
+ Users own:
18
+
19
+ - ML framework choice
20
+ - training algorithm
21
+ - model weights
22
+ - model storage
23
+ - model loading
24
+ - reward design beyond optional starter helpers
25
+
26
+ ## Policy Shape
27
+
28
+ Use this convention for browser playback:
29
+
30
+ ```js
31
+ const policy = {
32
+ predict(observation) {
33
+ return {
34
+ steering: 0,
35
+ throttle: 1,
36
+ brake: 0,
37
+ };
38
+ },
39
+ };
40
+ ```
41
+
42
+ `predict()` receives one controlled driver's observation. It returns normalized controls:
43
+
44
+ - `steering`: `-1` full left, `1` full right
45
+ - `throttle`: `0` to `1`
46
+ - `brake`: `0` to `1`
47
+
48
+ ## Headless Training Loop
49
+
50
+ ```js
51
+ import { createPaddockEnvironment } from '@inventure71/paddockjs/environment';
52
+
53
+ const env = createPaddockEnvironment({
54
+ drivers,
55
+ entries,
56
+ controlledDrivers: ['budget'],
57
+ frameSkip: 4,
58
+ reward: myReward,
59
+ });
60
+
61
+ let result = env.reset();
62
+ while (!result.done) {
63
+ const observation = result.observation.budget;
64
+ const action = policy.predict(observation);
65
+ result = env.step({ budget: action });
66
+ }
67
+ ```
68
+
69
+ ## Visual Playback Loop
70
+
71
+ ```js
72
+ import { mountF1Simulator } from '@inventure71/paddockjs';
73
+
74
+ const simulator = await mountF1Simulator(root, {
75
+ drivers,
76
+ entries,
77
+ expert: {
78
+ enabled: true,
79
+ controlledDrivers: ['budget'],
80
+ frameSkip: 4,
81
+ visualizeSensors: { rays: true },
82
+ },
83
+ });
84
+
85
+ function frame() {
86
+ const observation = simulator.expert.getObservation().budget;
87
+ const action = policy.predict(observation);
88
+ simulator.expert.step({ budget: action });
89
+ requestAnimationFrame(frame);
90
+ }
91
+
92
+ frame();
93
+ ```
94
+
95
+ ## Specs
96
+
97
+ Use specs to connect external training code without guessing:
98
+
99
+ ```js
100
+ const actionSpec = env.getActionSpec();
101
+ const observationSpec = env.getObservationSpec();
102
+ ```
103
+
104
+ These specs describe the action ranges, controlled drivers, ray layout, nearby-car limits, and vector schema.
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createPaddockEnvironment, createProgressReward } from '../src/environment/index.js';
4
+ import { CHAMPIONSHIP_ENTRY_BLUEPRINTS } from '../src/data/championship.js';
5
+ import { DEMO_PROJECT_DRIVERS } from '../src/data/demoDrivers.js';
6
+
7
+ const DEFAULT_OPTIONS = Object.freeze({
8
+ generations: 4,
9
+ candidates: 5,
10
+ episodes: 1,
11
+ steps: 240,
12
+ frameSkip: 4,
13
+ seed: 71,
14
+ traffic: false,
15
+ evalOnly: false,
16
+ });
17
+
18
+ const INITIAL_POLICY = Object.freeze({
19
+ steeringHeading: -1.2,
20
+ steeringOffset: -0.35,
21
+ steeringRayBalance: 0.45,
22
+ throttleBias: 0.72,
23
+ throttleFrontClearance: 0.28,
24
+ throttleSpeedDamping: 0.18,
25
+ brakeBias: 0,
26
+ brakeFrontDanger: 0.65,
27
+ brakeOffTrack: 0.2,
28
+ });
29
+
30
+ const POLICY_KEYS = Object.keys(INITIAL_POLICY);
31
+
32
+ main();
33
+
34
+ function main() {
35
+ const options = parseCliArgs(process.argv.slice(2));
36
+ if (options.help) {
37
+ printHelp();
38
+ return;
39
+ }
40
+
41
+ const driverId = DEMO_PROJECT_DRIVERS[0].id;
42
+ const rng = createSeededRandom(options.seed);
43
+ const reward = createProgressReward();
44
+ const evaluate = (policy, seedOffset = 0) => evaluatePolicy({
45
+ policy,
46
+ driverId,
47
+ reward,
48
+ options,
49
+ seedOffset,
50
+ });
51
+
52
+ if (options.evalOnly) {
53
+ const baseline = evaluate(INITIAL_POLICY);
54
+ printSummary({ options, driverId, bestPolicy: INITIAL_POLICY, best: baseline });
55
+ return;
56
+ }
57
+
58
+ let bestPolicy = { ...INITIAL_POLICY };
59
+ let best = evaluate(bestPolicy);
60
+
61
+ console.log(formatGenerationLine(0, best));
62
+
63
+ for (let generation = 1; generation <= options.generations; generation += 1) {
64
+ const mutationScale = 0.45 / Math.sqrt(generation);
65
+ const candidates = [{ policy: bestPolicy, scoreSeedOffset: generation * 1000 }];
66
+
67
+ for (let index = 1; index < options.candidates; index += 1) {
68
+ candidates.push({
69
+ policy: mutatePolicy(bestPolicy, rng, mutationScale),
70
+ scoreSeedOffset: generation * 1000 + index * 100,
71
+ });
72
+ }
73
+
74
+ for (const candidate of candidates) {
75
+ const score = evaluate(candidate.policy, candidate.scoreSeedOffset);
76
+ if (score.averageReward > best.averageReward) {
77
+ best = score;
78
+ bestPolicy = candidate.policy;
79
+ }
80
+ }
81
+
82
+ console.log(formatGenerationLine(generation, best));
83
+ }
84
+
85
+ printSummary({ options, driverId, bestPolicy, best });
86
+ }
87
+
88
+ function evaluatePolicy({ policy, driverId, reward, options, seedOffset = 0 }) {
89
+ let totalReward = 0;
90
+ let totalDistanceMeters = 0;
91
+ let totalCollisions = 0;
92
+ let totalOffTrackSteps = 0;
93
+ let completedEpisodes = 0;
94
+
95
+ for (let episode = 0; episode < options.episodes; episode += 1) {
96
+ const env = createTrainingEnvironment({ driverId, reward, options, episode, seedOffset });
97
+ let result = env.reset();
98
+ let episodeReward = 0;
99
+
100
+ for (let step = 0; step < options.steps && !result.done; step += 1) {
101
+ const observation = result.observation[driverId];
102
+ const action = chooseAction(policy, observation);
103
+ result = env.step({ [driverId]: action });
104
+ episodeReward += result.reward?.[driverId] ?? 0;
105
+ totalCollisions += result.events.filter((event) => event.type === 'collision').length;
106
+ if (result.observation[driverId]?.object?.self?.onTrack === false) totalOffTrackSteps += 1;
107
+ }
108
+
109
+ const car = result.state.snapshot.cars.find((entry) => entry.id === driverId);
110
+ totalReward += episodeReward;
111
+ totalDistanceMeters += Number(car?.distanceMeters ?? 0);
112
+ completedEpisodes += 1;
113
+ env.destroy();
114
+ }
115
+
116
+ return {
117
+ averageReward: totalReward / Math.max(1, completedEpisodes),
118
+ averageDistanceMeters: totalDistanceMeters / Math.max(1, completedEpisodes),
119
+ collisions: totalCollisions,
120
+ offTrackSteps: totalOffTrackSteps,
121
+ };
122
+ }
123
+
124
+ function createTrainingEnvironment({ driverId, reward, options, episode, seedOffset }) {
125
+ const drivers = options.traffic ? DEMO_PROJECT_DRIVERS.slice(0, 8) : DEMO_PROJECT_DRIVERS.slice(0, 1);
126
+ return createPaddockEnvironment({
127
+ drivers,
128
+ entries: CHAMPIONSHIP_ENTRY_BLUEPRINTS,
129
+ controlledDrivers: [driverId],
130
+ seed: options.seed + seedOffset + episode,
131
+ trackSeed: options.seed + 2026 + seedOffset + episode,
132
+ totalLaps: 3,
133
+ frameSkip: options.frameSkip,
134
+ scenario: {
135
+ participants: options.traffic ? 'all' : 'controlled-only',
136
+ nonControlled: 'ai',
137
+ },
138
+ episode: {
139
+ maxSteps: options.steps,
140
+ endOnRaceFinish: true,
141
+ },
142
+ rules: {
143
+ standingStart: false,
144
+ },
145
+ reward,
146
+ });
147
+ }
148
+
149
+ function chooseAction(policy, observation) {
150
+ const self = observation.object.self;
151
+ const frontRay = getRayDistance(observation, 0);
152
+ const leftRay = getRayDistance(observation, -60);
153
+ const rightRay = getRayDistance(observation, 60);
154
+ const frontDanger = clamp01((35 - frontRay) / 35);
155
+ const rayBalanceMeters = rightRay - leftRay;
156
+
157
+ const steering = clamp(
158
+ policy.steeringHeading * self.trackHeadingErrorRadians +
159
+ policy.steeringOffset * (self.trackOffsetMeters / 12) +
160
+ policy.steeringRayBalance * (rayBalanceMeters / 120),
161
+ -1,
162
+ 1,
163
+ );
164
+ const throttle = clamp01(
165
+ policy.throttleBias +
166
+ policy.throttleFrontClearance * (frontRay / 120) -
167
+ policy.throttleSpeedDamping * (self.speedKph / 320) -
168
+ (self.onTrack ? 0 : 0.35),
169
+ );
170
+ const brake = clamp01(
171
+ policy.brakeBias +
172
+ policy.brakeFrontDanger * frontDanger +
173
+ (self.onTrack ? 0 : policy.brakeOffTrack),
174
+ );
175
+
176
+ return { steering, throttle, brake };
177
+ }
178
+
179
+ function getRayDistance(observation, angleDegrees) {
180
+ const ray = observation.object.rays.find((entry) => entry.angleDegrees === angleDegrees);
181
+ return Number(ray?.track?.distanceMeters ?? ray?.lengthMeters ?? 120);
182
+ }
183
+
184
+ function mutatePolicy(policy, rng, scale) {
185
+ return Object.fromEntries(POLICY_KEYS.map((key) => [
186
+ key,
187
+ policy[key] + randomNormal(rng) * scale,
188
+ ]));
189
+ }
190
+
191
+ function parseCliArgs(argv) {
192
+ const options = { ...DEFAULT_OPTIONS };
193
+
194
+ for (const arg of argv) {
195
+ const [name, rawValue] = arg.split('=');
196
+ if (name === '--help' || name === '-h') options.help = true;
197
+ else if (name === '--eval-only') options.evalOnly = true;
198
+ else if (name === '--traffic') options.traffic = true;
199
+ else if (name === '--no-traffic') options.traffic = false;
200
+ else if (name === '--generations') options.generations = parsePositiveInt(rawValue, name);
201
+ else if (name === '--candidates') options.candidates = parsePositiveInt(rawValue, name);
202
+ else if (name === '--episodes') options.episodes = parsePositiveInt(rawValue, name);
203
+ else if (name === '--steps') options.steps = parsePositiveInt(rawValue, name);
204
+ else if (name === '--frame-skip') options.frameSkip = parsePositiveInt(rawValue, name);
205
+ else if (name === '--seed') options.seed = parsePositiveInt(rawValue, name);
206
+ else throw new Error(`Unknown option: ${name}`);
207
+ }
208
+
209
+ return options;
210
+ }
211
+
212
+ function parsePositiveInt(value, optionName) {
213
+ const parsed = Number.parseInt(value, 10);
214
+ if (!Number.isFinite(parsed) || parsed <= 0) {
215
+ throw new Error(`${optionName} requires a positive integer value.`);
216
+ }
217
+ return parsed;
218
+ }
219
+
220
+ function createSeededRandom(seed) {
221
+ let state = seed >>> 0;
222
+ return function random() {
223
+ state += 0x6D2B79F5;
224
+ let value = state;
225
+ value = Math.imul(value ^ (value >>> 15), value | 1);
226
+ value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
227
+ return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
228
+ };
229
+ }
230
+
231
+ function randomNormal(rng) {
232
+ const a = Math.max(Number.EPSILON, rng());
233
+ const b = Math.max(Number.EPSILON, rng());
234
+ return Math.sqrt(-2 * Math.log(a)) * Math.cos(2 * Math.PI * b);
235
+ }
236
+
237
+ function clamp(value, min, max) {
238
+ return Math.min(max, Math.max(min, value));
239
+ }
240
+
241
+ function clamp01(value) {
242
+ return clamp(value, 0, 1);
243
+ }
244
+
245
+ function formatGenerationLine(generation, score) {
246
+ return [
247
+ `generation=${generation}`,
248
+ `reward=${score.averageReward.toFixed(2)}`,
249
+ `distanceMeters=${score.averageDistanceMeters.toFixed(1)}`,
250
+ `collisions=${score.collisions}`,
251
+ `offTrackSteps=${score.offTrackSteps}`,
252
+ ].join(' ');
253
+ }
254
+
255
+ function printSummary({ options, driverId, bestPolicy, best }) {
256
+ console.log(JSON.stringify({
257
+ driverId,
258
+ options,
259
+ best,
260
+ bestPolicy,
261
+ }, null, 2));
262
+ }
263
+
264
+ function printHelp() {
265
+ console.log(`Usage: node examples/train-basic-policy.mjs [options]
266
+
267
+ Options:
268
+ --generations=N Random-search generations. Default: ${DEFAULT_OPTIONS.generations}
269
+ --candidates=N Candidate policies per generation. Default: ${DEFAULT_OPTIONS.candidates}
270
+ --episodes=N Episodes per policy evaluation. Default: ${DEFAULT_OPTIONS.episodes}
271
+ --steps=N Maximum manual environment steps per episode. Default: ${DEFAULT_OPTIONS.steps}
272
+ --frame-skip=N Simulator frames advanced by each step(action). Default: ${DEFAULT_OPTIONS.frameSkip}
273
+ --seed=N Deterministic random seed. Default: ${DEFAULT_OPTIONS.seed}
274
+ --traffic Keep built-in AI traffic on track during training.
275
+ --eval-only Evaluate the starter policy without training.
276
+ `);
277
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@inventure71/paddockjs",
3
- "version": "0.1.0",
3
+ "version": "0.3.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,24 +40,30 @@
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
  ],
@@ -72,7 +78,8 @@
72
78
  "showcase:ci": "npm --prefix local-preview ci && npm --prefix local-preview run build",
73
79
  "changeset": "changeset",
74
80
  "version-packages": "changeset version",
75
- "release": "npm publish --provenance"
81
+ "release": "npm publish --provenance",
82
+ "example:train": "node examples/train-basic-policy.mjs"
76
83
  },
77
84
  "dependencies": {
78
85
  "pixi.js": "8.18.1"
@@ -5,8 +5,14 @@ import {
5
5
  createRaceCanvasMarkup,
6
6
  createRaceControlsMarkup,
7
7
  createRaceDataPanelMarkup,
8
+ createRaceTelemetryDrawerMarkup,
8
9
  createSafetyCarControlMarkup,
10
+ createTelemetryCoreMarkup,
11
+ createTelemetryLapTimesMarkup,
9
12
  createTelemetryPanelMarkup,
13
+ createTelemetrySectorBannerMarkup,
14
+ createTelemetrySectorTimesMarkup,
15
+ createTelemetrySectorsMarkup,
10
16
  createTimingTowerMarkup,
11
17
  } from '../ui/componentTemplates.js';
12
18
  import { applyPaddockThemeCssVariables, resolveF1SimulatorOptions } from '../config/defaultOptions.js';
@@ -117,18 +123,44 @@ export class PaddockSimulatorController {
117
123
  mountRaceCanvas(root, {
118
124
  includeRaceDataPanel = false,
119
125
  includeTimingTower = false,
126
+ includeTelemetrySectorBanner = false,
120
127
  timingTowerVerticalFit,
121
128
  } = {}) {
122
129
  return this.mountComponent(root, 'race-canvas', createRaceCanvasMarkup({
123
130
  ...this.options,
124
131
  includeRaceDataPanel,
125
132
  includeTimingTower,
133
+ includeTelemetrySectorBanner,
126
134
  timingTowerVerticalFit,
127
135
  }));
128
136
  }
129
137
 
130
138
  mountTelemetryPanel(root, { includeOverview } = {}) {
131
- return this.mountComponent(root, 'telemetry-panel', createTelemetryPanelMarkup(this.options, { includeOverview }));
139
+ return this.mountComponent(root, 'telemetry-stack', createTelemetryPanelMarkup(this.options, { includeOverview }));
140
+ }
141
+
142
+ mountTelemetryCore(root) {
143
+ return this.mountComponent(root, 'telemetry-core', createTelemetryCoreMarkup(this.options));
144
+ }
145
+
146
+ mountTelemetrySectors(root) {
147
+ return this.mountComponent(root, 'telemetry-sectors', createTelemetrySectorsMarkup(this.options));
148
+ }
149
+
150
+ mountTelemetryLapTimes(root) {
151
+ return this.mountComponent(root, 'telemetry-lap-times', createTelemetryLapTimesMarkup(this.options));
152
+ }
153
+
154
+ mountTelemetrySectorTimes(root) {
155
+ return this.mountComponent(root, 'telemetry-sector-times', createTelemetrySectorTimesMarkup(this.options));
156
+ }
157
+
158
+ mountTelemetrySectorBanner(root) {
159
+ return this.mountComponent(root, 'telemetry-sector-banner', createTelemetrySectorBannerMarkup(this.options));
160
+ }
161
+
162
+ mountRaceTelemetryDrawer(root, options = {}) {
163
+ return this.mountComponent(root, 'race-telemetry-drawer', createRaceTelemetryDrawerMarkup(this.options, options));
132
164
  }
133
165
 
134
166
  mountCarDriverOverview(root) {
@@ -155,6 +187,10 @@ export class PaddockSimulatorController {
155
187
  return this;
156
188
  }
157
189
 
190
+ get expert() {
191
+ return this.app?.expert ?? null;
192
+ }
193
+
158
194
  destroy() {
159
195
  this.app?.destroy();
160
196
  this.app = null;
@@ -165,9 +201,10 @@ export class PaddockSimulatorController {
165
201
  }
166
202
 
167
203
  restart(nextOptions = {}) {
168
- this.options = resolveF1SimulatorOptions(mergeResolvedOptions(this.options, nextOptions));
204
+ const nextResolvedOptions = resolveF1SimulatorOptions(mergeResolvedOptions(this.options, nextOptions));
205
+ this.app?.restart(nextResolvedOptions);
206
+ this.options = nextResolvedOptions;
169
207
  this.compositeRoot.applyCssVariables();
170
- this.app?.restart(this.options);
171
208
  }
172
209
 
173
210
  selectDriver(driverId) {
@@ -228,6 +265,30 @@ export function mountTelemetryPanel(root, simulator, options) {
228
265
  return simulator.mountTelemetryPanel(root, options);
229
266
  }
230
267
 
268
+ export function mountTelemetryCore(root, simulator) {
269
+ return simulator.mountTelemetryCore(root);
270
+ }
271
+
272
+ export function mountTelemetrySectors(root, simulator) {
273
+ return simulator.mountTelemetrySectors(root);
274
+ }
275
+
276
+ export function mountTelemetryLapTimes(root, simulator) {
277
+ return simulator.mountTelemetryLapTimes(root);
278
+ }
279
+
280
+ export function mountTelemetrySectorTimes(root, simulator) {
281
+ return simulator.mountTelemetrySectorTimes(root);
282
+ }
283
+
284
+ export function mountTelemetrySectorBanner(root, simulator) {
285
+ return simulator.mountTelemetrySectorBanner(root);
286
+ }
287
+
288
+ export function mountRaceTelemetryDrawer(root, simulator, options) {
289
+ return simulator.mountRaceTelemetryDrawer(root, options);
290
+ }
291
+
231
292
  export function mountRaceDataPanel(root, simulator) {
232
293
  return simulator.mountRaceDataPanel(root);
233
294
  }
@@ -0,0 +1,32 @@
1
+ import { createEnvironmentRuntime } from '../environment/runtime.js';
2
+ import { resolveEnvironmentOptions } from '../environment/options.js';
3
+
4
+ export function createBrowserExpertAdapter(app, expertOptions = {}) {
5
+ let resolvedOptions = resolveEnvironmentOptions({
6
+ ...app.options,
7
+ ...expertOptions,
8
+ controlledDrivers: expertOptions.controlledDrivers,
9
+ });
10
+
11
+ return createEnvironmentRuntime({
12
+ getSimulation: () => app.sim,
13
+ setSimulation(nextSim) {
14
+ app.sim = nextSim;
15
+ },
16
+ createSimulation(nextOptions) {
17
+ app.applyExpertOptions(nextOptions);
18
+ return app.createRaceSimulation(nextOptions);
19
+ },
20
+ getOptions: () => resolvedOptions,
21
+ setOptions(nextOptions) {
22
+ resolvedOptions = nextOptions;
23
+ },
24
+ afterReset(result) {
25
+ app.renderTrack();
26
+ app.renderExpertFrame(result.state.snapshot, { observation: result.observation });
27
+ },
28
+ afterStep(result) {
29
+ app.renderExpertFrame(result.state.snapshot, { observation: result.observation });
30
+ },
31
+ });
32
+ }