@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.
- package/CHANGELOG.md +16 -0
- package/INSTALL_AND_UPDATE.md +163 -0
- package/README.md +96 -16
- package/docs/architecture.md +55 -11
- package/docs/concepts.md +5 -1
- package/docs/data_contract.md +324 -8
- package/docs/data_model.md +1 -0
- package/docs/index.md +1 -0
- package/docs/rules.md +2 -0
- package/docs/system_specs.md +66 -11
- package/docs/training.md +104 -0
- package/examples/train-basic-policy.mjs +277 -0
- package/package.json +14 -7
- package/src/api/PaddockSimulatorController.js +64 -3
- package/src/app/BrowserExpertAdapter.js +32 -0
- package/src/app/F1SimulatorApp.js +308 -38
- package/src/app/domBindings.js +35 -11
- package/src/config/defaultOptions.js +33 -0
- package/src/data/championship.js +13 -0
- package/src/data/normalizeDrivers.js +7 -2
- package/src/environment/actions.js +45 -0
- package/src/environment/episode.js +17 -0
- package/src/environment/events.js +17 -0
- package/src/environment/index.d.ts +241 -0
- package/src/environment/index.js +2 -0
- package/src/environment/observations.js +112 -0
- package/src/environment/options.js +150 -0
- package/src/environment/rewards.js +69 -0
- package/src/environment/runtime.js +140 -0
- package/src/environment/sensors.js +257 -0
- package/src/environment/specs.js +79 -0
- package/src/index.d.ts +97 -4
- package/src/index.js +12 -2
- package/src/rendering/proceduralTrackAsset.js +7 -1
- package/src/simulation/raceSimulation.js +214 -1
- package/src/simulation/trackModel.js +21 -0
- package/src/styles.css +495 -2
- package/src/ui/componentTemplates.js +201 -4
- package/AGENTS.md +0 -86
- package/src/standalone.js +0 -15
package/docs/training.md
ADDED
|
@@ -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.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "A data-driven browser F1 simulator component for
|
|
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-
|
|
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
|
-
|
|
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
|
+
}
|