@inventure71/paddockjs 0.3.0 → 2.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.
- package/CHANGELOG.md +91 -0
- package/INSTALL_AND_UPDATE.md +5 -2
- package/README.md +328 -13
- package/docs/architecture.md +150 -41
- package/docs/concepts.md +22 -3
- package/docs/custom_model_controller.md +329 -0
- package/docs/data_contract.md +474 -34
- package/docs/data_model.md +1 -1
- package/docs/index.md +1 -0
- package/docs/learnings.md +23 -1
- package/docs/rules.md +169 -21
- package/docs/system_specs.md +56 -20
- package/docs/training.md +369 -25
- package/examples/python/README.md +62 -0
- package/examples/python/base_policy_server.py +191 -0
- package/examples/python/requirements.txt +2 -0
- package/examples/train-basic-policy.mjs +5 -6
- package/examples/trainingData.mjs +133 -0
- package/package.json +15 -4
- package/src/api/PaddockSimulatorController.js +36 -2
- package/src/app/BrowserExpertAdapter.js +367 -9
- package/src/app/F1SimulatorApp.js +615 -940
- package/src/app/banners/bannerFormatters.js +55 -0
- package/src/app/banners/raceDataBannerController.js +141 -0
- package/src/app/banners/stewardMessageController.js +99 -0
- package/src/app/camera/cameraConstants.js +21 -0
- package/src/app/camera/cameraController.js +265 -0
- package/src/app/camera/cameraGeometry.js +176 -0
- package/src/app/domBindings.js +14 -1
- package/src/app/readouts/carOverviewRenderer.js +109 -0
- package/src/app/readouts/controlStateRenderer.js +37 -0
- package/src/app/readouts/raceStatusRenderer.js +133 -0
- package/src/app/readouts/readoutFormatters.js +58 -0
- package/src/app/readouts/telemetryRenderer.js +113 -0
- package/src/app/readouts/timingTowerRenderer.js +177 -0
- package/src/app/rendering/appAssets.js +38 -0
- package/src/app/rendering/carRenderer.js +286 -0
- package/src/app/rendering/displayUtils.js +22 -0
- package/src/app/rendering/drsTrailRenderer.js +90 -0
- package/src/app/rendering/expertSensorRenderer.js +126 -0
- package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
- package/src/app/rendering/replayGhostRenderer.js +124 -0
- package/src/app/rendering/trackRenderer.js +25 -0
- package/src/app/runtime/frameLoop.js +66 -0
- package/src/app/runtime/runtimeTiming.js +34 -0
- package/src/app/runtime/runtimeVisibility.js +45 -0
- package/src/config/defaultOptions.js +24 -4
- package/src/environment/actions.js +38 -7
- package/src/environment/controllerLoop.js +246 -0
- package/src/environment/episode.js +86 -2
- package/src/environment/evaluation.js +206 -0
- package/src/environment/index.d.ts +638 -6
- package/src/environment/index.js +14 -0
- package/src/environment/metrics.js +159 -0
- package/src/environment/observationOptions.js +33 -0
- package/src/environment/observations.js +500 -43
- package/src/environment/options.js +41 -2
- package/src/environment/recorder.js +28 -0
- package/src/environment/runtime.js +260 -24
- package/src/environment/scenarios.js +249 -0
- package/src/environment/sensors/bodySenses.js +23 -0
- package/src/environment/sensors/boundarySenses.js +17 -0
- package/src/environment/sensors/carRays.js +75 -0
- package/src/environment/sensors/contactSenses.js +32 -0
- package/src/environment/sensors/index.js +157 -0
- package/src/environment/sensors/indexedRayBands.js +103 -0
- package/src/environment/sensors/nearbyCars.js +52 -0
- package/src/environment/sensors/opponentRadar.js +65 -0
- package/src/environment/sensors/rayConfig.js +110 -0
- package/src/environment/sensors/rayDefaults.js +6 -0
- package/src/environment/sensors/rayGeometry.js +60 -0
- package/src/environment/sensors/rayGuards.js +19 -0
- package/src/environment/sensors/sensorTargets.js +60 -0
- package/src/environment/sensors/surfaceRays.js +296 -0
- package/src/environment/sensors/trackRays.js +261 -0
- package/src/environment/sensors.js +1 -257
- package/src/environment/specs.js +188 -9
- package/src/environment/workerProtocol.js +64 -0
- package/src/index.d.ts +533 -7
- package/src/index.js +26 -0
- package/src/rendering/proceduralTrackAsset.js +20 -510
- package/src/rendering/renderSnapshot.js +38 -12
- package/src/rendering/track/finishLineRenderer.js +34 -0
- package/src/rendering/track/gridRenderer.js +47 -0
- package/src/rendering/track/offsetStrokeSafety.js +136 -0
- package/src/rendering/track/pitLaneRenderer.js +248 -0
- package/src/rendering/track/trackMaterialRenderer.js +326 -0
- package/src/rendering/track/trackRenderConstants.js +38 -0
- package/src/rendering/track/trackRenderGeometry.js +94 -0
- package/src/simulation/collisionGeometry.js +214 -0
- package/src/simulation/driver/driverControlConstants.js +28 -0
- package/src/simulation/driver/driverController.js +47 -0
- package/src/simulation/driver/driverInput.js +28 -0
- package/src/simulation/driver/driverMath.js +6 -0
- package/src/simulation/driver/driverPersonality.js +25 -0
- package/src/simulation/driver/edgeRecovery.js +48 -0
- package/src/simulation/driver/racingControls.js +399 -0
- package/src/simulation/driver/racingLinePlan.js +166 -0
- package/src/simulation/driver/recoveryDynamics.js +38 -0
- package/src/simulation/driver/rejoinControls.js +213 -0
- package/src/simulation/driver/safetyCarControls.js +35 -0
- package/src/simulation/driver/trafficScan.js +51 -0
- package/src/simulation/driverController.js +4 -253
- package/src/simulation/participants/participantInteractions.js +120 -0
- package/src/simulation/pit/pitFlow.js +234 -0
- package/src/simulation/pit/pitIntent.js +92 -0
- package/src/simulation/pit/pitOccupancy.js +47 -0
- package/src/simulation/pit/pitPenaltyService.js +71 -0
- package/src/simulation/pit/pitQueue.js +55 -0
- package/src/simulation/pit/pitRouteMovement.js +160 -0
- package/src/simulation/pit/pitRouting.js +319 -0
- package/src/simulation/pit/pitService.js +41 -0
- package/src/simulation/pit/pitServiceConstants.js +21 -0
- package/src/simulation/pit/pitServiceProfile.js +45 -0
- package/src/simulation/pit/pitSnapshots.js +61 -0
- package/src/simulation/pit/pitState.js +113 -0
- package/src/simulation/pit/pitTireService.js +92 -0
- package/src/simulation/race/classification.js +104 -0
- package/src/simulation/race/gridPenalties.js +37 -0
- package/src/simulation/race/raceControlState.js +38 -0
- package/src/simulation/race/raceDistance.js +32 -0
- package/src/simulation/race/raceFinish.js +65 -0
- package/src/simulation/race/raceLifecycle.js +71 -0
- package/src/simulation/race/raceOrder.js +113 -0
- package/src/simulation/race/raceProgress.js +124 -0
- package/src/simulation/race/raceSetup.js +93 -0
- package/src/simulation/race/raceStep.js +80 -0
- package/src/simulation/race/redFlag.js +15 -0
- package/src/simulation/race/retirements.js +44 -0
- package/src/simulation/race/safetyCar.js +86 -0
- package/src/simulation/race/stalledDnf.js +86 -0
- package/src/simulation/raceSimulation.js +386 -1012
- package/src/simulation/replay/replayGhosts.js +128 -0
- package/src/simulation/rules/collisionSteward.js +69 -0
- package/src/simulation/rules/moduleConfig.js +72 -0
- package/src/simulation/rules/penaltyConfig.js +99 -0
- package/src/simulation/rules/penaltyEvents.js +18 -0
- package/src/simulation/rules/penaltyLedger.js +153 -0
- package/src/simulation/rules/penaltyStats.js +26 -0
- package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
- package/src/simulation/rules/ruleConfigMerge.js +41 -0
- package/src/simulation/rules/ruleDefaults.js +85 -0
- package/src/simulation/rules/rulesReview.js +141 -0
- package/src/simulation/rules/rulesetPresets.js +47 -0
- package/src/simulation/rules/tireRequirementSteward.js +29 -0
- package/src/simulation/rules/trackLimitsSteward.js +85 -0
- package/src/simulation/rulesConfig.js +34 -0
- package/src/simulation/snapshots/raceSnapshots.js +192 -0
- package/src/simulation/timing/drsTiming.js +87 -0
- package/src/simulation/timing/gapEstimation.js +38 -0
- package/src/simulation/timing/lapTelemetry.js +182 -0
- package/src/simulation/timing/raceTiming.js +15 -0
- package/src/simulation/timing/sectorPerformance.js +60 -0
- package/src/simulation/timing/timingConstants.js +5 -0
- package/src/simulation/timing/timingHistory.js +46 -0
- package/src/simulation/timing/timingLines.js +79 -0
- package/src/simulation/track/centerlineSpline.js +59 -0
- package/src/simulation/track/drsZones.js +67 -0
- package/src/simulation/track/pitLaneAccess.js +103 -0
- package/src/simulation/track/pitLaneLayout.js +395 -0
- package/src/simulation/track/pitLaneState.js +160 -0
- package/src/simulation/track/proceduralCenterline.js +237 -0
- package/src/simulation/track/sampleGeometry.js +49 -0
- package/src/simulation/track/spatialQueries.js +154 -0
- package/src/simulation/track/startStraight.js +141 -0
- package/src/simulation/track/trackConstants.js +109 -0
- package/src/simulation/track/trackGenerationOptions.js +283 -0
- package/src/simulation/track/trackMath.js +241 -0
- package/src/simulation/track/trackModel.js +240 -0
- package/src/simulation/track/trackQueryIndex.js +945 -0
- package/src/simulation/track/trackSectors.js +19 -0
- package/src/simulation/track/trackStatePolicy.js +24 -0
- package/src/simulation/track/trackValidation.js +98 -0
- package/src/simulation/trackModel.js +1 -628
- package/src/simulation/units.js +6 -4
- package/src/simulation/vehicle/contactResolution.js +116 -0
- package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
- package/src/simulation/vehicle/pitWheelSurface.js +69 -0
- package/src/simulation/vehicle/runoffResponse.js +88 -0
- package/src/simulation/vehicle/surfacePriority.js +28 -0
- package/src/simulation/vehicle/vehicleGeometry.js +247 -0
- package/src/simulation/vehicle/vehiclePhysics.js +363 -0
- package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
- package/src/simulation/vehicle/vehicleState.js +186 -0
- package/src/simulation/vehicle/wheelSurface.js +150 -0
- package/src/simulation/vehicleGeometry.js +1 -0
- package/src/simulation/vehiclePhysics.js +1 -104
- package/src/simulation/warmup/runtimeWarmup.js +224 -0
- package/src/simulation/wheelSurface.js +1 -0
- package/src/styles.css +342 -42
- package/src/ui/bannerTemplates.js +69 -0
- package/src/ui/cameraControlsTemplate.js +20 -0
- package/src/ui/carOverviewTemplate.js +33 -0
- package/src/ui/componentTemplates.js +20 -426
- package/src/ui/raceCanvasTemplate.js +69 -0
- package/src/ui/raceControlStatusBanner.js +39 -0
- package/src/ui/raceControlsTemplate.js +36 -0
- package/src/ui/shellTemplate.js +6 -5
- package/src/ui/telemetryDrawerTemplate.js +55 -0
- package/src/ui/telemetryTemplates.js +136 -0
- package/src/ui/templateUtils.js +23 -0
- package/src/ui/timingTowerTemplate.js +34 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
export function createPaddockDriverControllerLoop({
|
|
2
|
+
runtime,
|
|
3
|
+
controller,
|
|
4
|
+
actionRepeat = 4,
|
|
5
|
+
mode = 'manual',
|
|
6
|
+
scheduler = null,
|
|
7
|
+
} = {}) {
|
|
8
|
+
if (!runtime || typeof runtime.step !== 'function' || typeof runtime.reset !== 'function') {
|
|
9
|
+
throw new Error('createPaddockDriverControllerLoop requires a runtime with reset() and step(actions).');
|
|
10
|
+
}
|
|
11
|
+
if (!controller || typeof controller.decideBatch !== 'function') {
|
|
12
|
+
throw new Error('createPaddockDriverControllerLoop requires a controller with decideBatch(context).');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const repeat = Math.max(1, Math.floor(Number(actionRepeat) || 1));
|
|
16
|
+
let actionSpec = null;
|
|
17
|
+
let observationSpec = null;
|
|
18
|
+
let initialized = false;
|
|
19
|
+
let result = null;
|
|
20
|
+
let previousResult = null;
|
|
21
|
+
let heldActions = null;
|
|
22
|
+
let heldFramesRemaining = 0;
|
|
23
|
+
let policyStep = 0;
|
|
24
|
+
let runtimeStep = 0;
|
|
25
|
+
let running = false;
|
|
26
|
+
let scheduled = null;
|
|
27
|
+
let previousActions = {};
|
|
28
|
+
let lastDecisionMs = 0;
|
|
29
|
+
let lastError = null;
|
|
30
|
+
|
|
31
|
+
async function ensureSpecs() {
|
|
32
|
+
if (!actionSpec) actionSpec = runtime.getActionSpec();
|
|
33
|
+
if (!observationSpec) observationSpec = runtime.getObservationSpec();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function ensureInitialized(context) {
|
|
37
|
+
if (initialized) return;
|
|
38
|
+
await controller.init?.(context);
|
|
39
|
+
initialized = true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function resetRuntime(options = {}, { stopPlayback = true } = {}) {
|
|
43
|
+
if (stopPlayback) stop();
|
|
44
|
+
result = runtime.reset(options);
|
|
45
|
+
previousResult = null;
|
|
46
|
+
heldActions = null;
|
|
47
|
+
heldFramesRemaining = 0;
|
|
48
|
+
policyStep = 0;
|
|
49
|
+
runtimeStep = Number(result?.info?.step ?? 0);
|
|
50
|
+
previousActions = {};
|
|
51
|
+
lastError = null;
|
|
52
|
+
await ensureSpecs();
|
|
53
|
+
const context = buildContext({ resetDriverIds: controlledDrivers() });
|
|
54
|
+
await ensureInitialized(context);
|
|
55
|
+
await controller.reset?.(context);
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function reset(options = {}) {
|
|
60
|
+
return resetRuntime(options, { stopPlayback: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function ensureResult() {
|
|
64
|
+
if (!result) await resetRuntime({}, { stopPlayback: false });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function resetDrivers(placements = {}, resultOptions = {}) {
|
|
68
|
+
if (typeof runtime.resetDrivers !== 'function') {
|
|
69
|
+
throw new Error('The supplied Paddock runtime does not support resetDrivers().');
|
|
70
|
+
}
|
|
71
|
+
const resetDriverIds = Object.keys(placements);
|
|
72
|
+
previousResult = result;
|
|
73
|
+
result = runtime.resetDrivers(placements, resultOptions);
|
|
74
|
+
heldActions = null;
|
|
75
|
+
heldFramesRemaining = 0;
|
|
76
|
+
lastError = null;
|
|
77
|
+
resetDriverIds.forEach((driverId) => {
|
|
78
|
+
delete previousActions[driverId];
|
|
79
|
+
});
|
|
80
|
+
await ensureSpecs();
|
|
81
|
+
const context = buildContext({ resetDriverIds });
|
|
82
|
+
await ensureInitialized(context);
|
|
83
|
+
await controller.reset?.(context);
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function beginDecision() {
|
|
88
|
+
await ensureResult();
|
|
89
|
+
const context = buildContext();
|
|
90
|
+
await ensureInitialized(context);
|
|
91
|
+
const startedAt = now();
|
|
92
|
+
const actions = await controller.decideBatch(context);
|
|
93
|
+
lastDecisionMs = now() - startedAt;
|
|
94
|
+
heldActions = actions && typeof actions === 'object' ? actions : {};
|
|
95
|
+
heldFramesRemaining = repeat;
|
|
96
|
+
policyStep += 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function stepFrame() {
|
|
100
|
+
await ensureResult();
|
|
101
|
+
if (!heldActions || heldFramesRemaining <= 0) {
|
|
102
|
+
await beginDecision();
|
|
103
|
+
}
|
|
104
|
+
const actionIndex = repeat - heldFramesRemaining;
|
|
105
|
+
const actions = heldActions;
|
|
106
|
+
const previousActionsForStep = previousActions;
|
|
107
|
+
previousResult = result;
|
|
108
|
+
result = runtime.step(actions);
|
|
109
|
+
heldFramesRemaining -= 1;
|
|
110
|
+
runtimeStep = Number(result?.info?.step ?? runtimeStep + 1);
|
|
111
|
+
previousActions = actions;
|
|
112
|
+
await controller.onStep?.(buildContext({
|
|
113
|
+
actionIndex,
|
|
114
|
+
actions,
|
|
115
|
+
previousActions: previousActionsForStep,
|
|
116
|
+
previousResult,
|
|
117
|
+
}));
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function step() {
|
|
122
|
+
let latest = result;
|
|
123
|
+
const targetPolicyStep = policyStep + 1;
|
|
124
|
+
while (policyStep < targetPolicyStep || heldFramesRemaining > 0) {
|
|
125
|
+
latest = await stepFrame();
|
|
126
|
+
if (latest?.done) break;
|
|
127
|
+
if (policyStep >= targetPolicyStep && heldFramesRemaining <= 0) break;
|
|
128
|
+
}
|
|
129
|
+
return latest;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function start() {
|
|
133
|
+
if (running) return;
|
|
134
|
+
lastError = null;
|
|
135
|
+
running = true;
|
|
136
|
+
const schedule = scheduler ?? defaultScheduler();
|
|
137
|
+
const tick = async () => {
|
|
138
|
+
if (!running) return;
|
|
139
|
+
scheduled = null;
|
|
140
|
+
try {
|
|
141
|
+
await stepFrame();
|
|
142
|
+
} catch (error) {
|
|
143
|
+
lastError = error;
|
|
144
|
+
running = false;
|
|
145
|
+
scheduled = null;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!running || result?.done) {
|
|
149
|
+
running = false;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
scheduled = schedule(tick);
|
|
153
|
+
};
|
|
154
|
+
scheduled = schedule(tick);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function stop() {
|
|
158
|
+
running = false;
|
|
159
|
+
if (scheduled && typeof scheduled.cancel === 'function') scheduled.cancel();
|
|
160
|
+
else if (typeof scheduled === 'number' && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(scheduled);
|
|
161
|
+
scheduled = null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function controlledDrivers() {
|
|
165
|
+
return actionSpec?.controlledDrivers ?? result?.info?.controlledDrivers ?? [];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function buildContext(extra = {}) {
|
|
169
|
+
const drivers = controlledDrivers();
|
|
170
|
+
const observation = result?.observation ?? runtime.getObservation?.() ?? {};
|
|
171
|
+
return {
|
|
172
|
+
runtime,
|
|
173
|
+
mode,
|
|
174
|
+
actionRepeat: repeat,
|
|
175
|
+
controlledDrivers: drivers,
|
|
176
|
+
actionSpec,
|
|
177
|
+
observationSpec,
|
|
178
|
+
result,
|
|
179
|
+
previousResult: extra.previousResult ?? previousResult,
|
|
180
|
+
observation,
|
|
181
|
+
orderedObservations: drivers.map((driverId, index) => ({
|
|
182
|
+
driverId,
|
|
183
|
+
index,
|
|
184
|
+
observation: observation?.[driverId] ?? null,
|
|
185
|
+
vector: observation?.[driverId]?.vector ?? null,
|
|
186
|
+
})),
|
|
187
|
+
metrics: result?.metrics ?? {},
|
|
188
|
+
events: result?.events ?? [],
|
|
189
|
+
info: result?.info ?? null,
|
|
190
|
+
previousActions: extra.previousActions ?? previousActions,
|
|
191
|
+
orderedPreviousActions: drivers.map((driverId) => (extra.previousActions ?? previousActions)?.[driverId] ?? null),
|
|
192
|
+
actions: extra.actions ?? heldActions,
|
|
193
|
+
policyStep,
|
|
194
|
+
runtimeStep,
|
|
195
|
+
heldFramesRemaining,
|
|
196
|
+
actionIndex: extra.actionIndex ?? 0,
|
|
197
|
+
resetDriverIds: extra.resetDriverIds ?? [],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
reset,
|
|
203
|
+
resetDrivers,
|
|
204
|
+
step,
|
|
205
|
+
stepFrame,
|
|
206
|
+
start,
|
|
207
|
+
stop,
|
|
208
|
+
get result() {
|
|
209
|
+
return result;
|
|
210
|
+
},
|
|
211
|
+
get actionSpec() {
|
|
212
|
+
return actionSpec;
|
|
213
|
+
},
|
|
214
|
+
get observationSpec() {
|
|
215
|
+
return observationSpec;
|
|
216
|
+
},
|
|
217
|
+
get stats() {
|
|
218
|
+
return {
|
|
219
|
+
policyStep,
|
|
220
|
+
runtimeStep,
|
|
221
|
+
heldFramesRemaining,
|
|
222
|
+
actions: heldActions,
|
|
223
|
+
actionRepeat: repeat,
|
|
224
|
+
lastDecisionMs,
|
|
225
|
+
running,
|
|
226
|
+
lastError,
|
|
227
|
+
};
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function now() {
|
|
233
|
+
return typeof performance !== 'undefined' && typeof performance.now === 'function'
|
|
234
|
+
? performance.now()
|
|
235
|
+
: Date.now();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function defaultScheduler() {
|
|
239
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
240
|
+
return (callback) => requestAnimationFrame(callback);
|
|
241
|
+
}
|
|
242
|
+
return (callback) => {
|
|
243
|
+
const id = setTimeout(callback, 0);
|
|
244
|
+
return { cancel: () => clearTimeout(id) };
|
|
245
|
+
};
|
|
246
|
+
}
|
|
@@ -1,17 +1,101 @@
|
|
|
1
1
|
export function createEpisodeState() {
|
|
2
2
|
return {
|
|
3
3
|
step: 0,
|
|
4
|
+
drivers: new Map(),
|
|
4
5
|
previousSnapshot: null,
|
|
5
6
|
lastResult: null,
|
|
6
7
|
};
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
export function
|
|
10
|
+
export function initializeDriverEpisodes(episodeState, driverIds = []) {
|
|
11
|
+
episodeState.drivers = new Map(driverIds.map((driverId) => [driverId, createDriverEpisodeState()]));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function advanceDriverEpisodes(episodeState, driverIds = []) {
|
|
15
|
+
driverIds.forEach((driverId) => {
|
|
16
|
+
const state = ensureDriverEpisodeState(episodeState, driverId);
|
|
17
|
+
if (!state.terminated && !state.truncated) state.episodeStep += 1;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resetDriverEpisodes(episodeState, driverIds = []) {
|
|
22
|
+
driverIds.forEach((driverId) => {
|
|
23
|
+
const state = ensureDriverEpisodeState(episodeState, driverId);
|
|
24
|
+
state.episodeId += 1;
|
|
25
|
+
state.episodeStep = 0;
|
|
26
|
+
state.terminated = false;
|
|
27
|
+
state.truncated = false;
|
|
28
|
+
state.endReason = null;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function markDestroyedDriverEpisodes(episodeState, driverIds = [], snapshot = null) {
|
|
33
|
+
const carsById = new Map((snapshot?.cars ?? []).map((car) => [car.id, car]));
|
|
34
|
+
driverIds.forEach((driverId) => {
|
|
35
|
+
const car = carsById.get(driverId);
|
|
36
|
+
if (!car?.destroyed && !car?.dnf && !car?.outOfRace) return;
|
|
37
|
+
const state = ensureDriverEpisodeState(episodeState, driverId);
|
|
38
|
+
state.terminated = true;
|
|
39
|
+
state.truncated = false;
|
|
40
|
+
state.endReason = car.destroyed ? 'destroyed' : (car.dnfReason ?? car.destroyReason ?? 'destroyed');
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildDriverEpisodeInfo(episodeState, options, episode) {
|
|
45
|
+
return Object.fromEntries(options.controlledDrivers.map((driverId) => {
|
|
46
|
+
const state = ensureDriverEpisodeState(episodeState, driverId);
|
|
47
|
+
const maxStepTruncated = state.episodeStep >= options.episode.maxSteps;
|
|
48
|
+
const terminated = Boolean(state.terminated || episode.terminated);
|
|
49
|
+
const truncated = Boolean(state.truncated || maxStepTruncated || episode.truncated);
|
|
50
|
+
const endReason = state.endReason ??
|
|
51
|
+
(terminated ? episode.endReason : null) ??
|
|
52
|
+
(maxStepTruncated ? 'max-steps' : null) ??
|
|
53
|
+
(truncated ? episode.endReason : null);
|
|
54
|
+
return [driverId, {
|
|
55
|
+
terminated,
|
|
56
|
+
truncated,
|
|
57
|
+
endReason,
|
|
58
|
+
episodeStep: state.episodeStep,
|
|
59
|
+
episodeId: state.episodeId,
|
|
60
|
+
}];
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function evaluateEpisode(snapshot, options, episodeState, controlledDrivers = options.controlledDrivers) {
|
|
10
65
|
if (snapshot.raceControl.finished && options.episode.endOnRaceFinish) {
|
|
11
66
|
return { terminated: true, truncated: false, endReason: 'race-finish' };
|
|
12
67
|
}
|
|
13
|
-
|
|
68
|
+
let allTerminated = controlledDrivers.length > 0;
|
|
69
|
+
let allTruncated = controlledDrivers.length > 0;
|
|
70
|
+
let firstTerminatedReason = null;
|
|
71
|
+
for (const driverId of controlledDrivers) {
|
|
72
|
+
const state = ensureDriverEpisodeState(episodeState, driverId);
|
|
73
|
+
if (!state.terminated) allTerminated = false;
|
|
74
|
+
else firstTerminatedReason ??= state.endReason;
|
|
75
|
+
if (!state.truncated && state.episodeStep < options.episode.maxSteps) allTruncated = false;
|
|
76
|
+
if (!allTerminated && !allTruncated) break;
|
|
77
|
+
}
|
|
78
|
+
if (allTerminated) {
|
|
79
|
+
return { terminated: true, truncated: false, endReason: controlledDrivers.length === 1 ? firstTerminatedReason : 'all-drivers-terminated' };
|
|
80
|
+
}
|
|
81
|
+
if (allTruncated) {
|
|
14
82
|
return { terminated: false, truncated: true, endReason: 'max-steps' };
|
|
15
83
|
}
|
|
16
84
|
return { terminated: false, truncated: false, endReason: null };
|
|
17
85
|
}
|
|
86
|
+
|
|
87
|
+
function createDriverEpisodeState() {
|
|
88
|
+
return {
|
|
89
|
+
episodeStep: 0,
|
|
90
|
+
episodeId: 0,
|
|
91
|
+
terminated: false,
|
|
92
|
+
truncated: false,
|
|
93
|
+
endReason: null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function ensureDriverEpisodeState(episodeState, driverId) {
|
|
98
|
+
if (!episodeState.drivers) episodeState.drivers = new Map();
|
|
99
|
+
if (!episodeState.drivers.has(driverId)) episodeState.drivers.set(driverId, createDriverEpisodeState());
|
|
100
|
+
return episodeState.drivers.get(driverId);
|
|
101
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { simUnitsToMeters } from '../simulation/units.js';
|
|
2
|
+
import { isEnvironmentCarOffTrack, isEnvironmentContactEvent } from './metrics.js';
|
|
3
|
+
import { createPaddockEnvironment } from './runtime.js';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_EVALUATION_CASES = Object.freeze([
|
|
6
|
+
{
|
|
7
|
+
name: 'cornering',
|
|
8
|
+
seed: 7101,
|
|
9
|
+
trackSeed: 2601,
|
|
10
|
+
maxSteps: 420,
|
|
11
|
+
scenario: { preset: 'cornering' },
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'off-track-recovery',
|
|
15
|
+
seed: 7102,
|
|
16
|
+
trackSeed: 2602,
|
|
17
|
+
maxSteps: 360,
|
|
18
|
+
scenario: { preset: 'off-track-recovery' },
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'overtaking-pack',
|
|
22
|
+
seed: 7103,
|
|
23
|
+
trackSeed: 2603,
|
|
24
|
+
maxSteps: 600,
|
|
25
|
+
scenario: { preset: 'overtaking-pack' },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'pit-entry',
|
|
29
|
+
seed: 7104,
|
|
30
|
+
trackSeed: 2604,
|
|
31
|
+
maxSteps: 480,
|
|
32
|
+
scenario: { preset: 'pit-entry' },
|
|
33
|
+
},
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
export function runEnvironmentEvaluation({
|
|
37
|
+
baseOptions,
|
|
38
|
+
policy,
|
|
39
|
+
cases = DEFAULT_EVALUATION_CASES,
|
|
40
|
+
createEnvironment = createPaddockEnvironment,
|
|
41
|
+
} = {}) {
|
|
42
|
+
if (!baseOptions || typeof baseOptions !== 'object') {
|
|
43
|
+
throw new Error('PaddockJS environment evaluation requires baseOptions.');
|
|
44
|
+
}
|
|
45
|
+
if (!isEvaluationPolicy(policy)) {
|
|
46
|
+
throw new Error('PaddockJS environment evaluation requires a policy function or object with predict().');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
cases: cases.map((evaluationCase) => runEvaluationCase({
|
|
51
|
+
baseOptions,
|
|
52
|
+
policy,
|
|
53
|
+
evaluationCase,
|
|
54
|
+
createEnvironment,
|
|
55
|
+
})),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isEvaluationPolicy(policy) {
|
|
60
|
+
return typeof policy === 'function' ||
|
|
61
|
+
(policy != null && typeof policy.predict === 'function');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createEvaluationTracker(initialResult) {
|
|
65
|
+
if (!initialResult?.state?.snapshot) {
|
|
66
|
+
throw new Error('PaddockJS environment evaluation tracker requires result.state.snapshot; use result.stateOutput "minimal" or "full".');
|
|
67
|
+
}
|
|
68
|
+
const initialCars = carsById(initialResult.state.snapshot);
|
|
69
|
+
const metrics = Object.fromEntries(initialResult.info.controlledDrivers.map((driverId) => {
|
|
70
|
+
const car = initialCars.get(driverId);
|
|
71
|
+
const offTrack = isEnvironmentCarOffTrack(car);
|
|
72
|
+
return [driverId, {
|
|
73
|
+
distanceMeters: 0,
|
|
74
|
+
lapProgressMeters: lapProgressMetersFromSnapshotCar(car),
|
|
75
|
+
offTrackSteps: offTrack ? 1 : 0,
|
|
76
|
+
contactCount: 0,
|
|
77
|
+
recoverySuccess: false,
|
|
78
|
+
passCount: 0,
|
|
79
|
+
lapTimeSeconds: null,
|
|
80
|
+
startedOffTrack: offTrack,
|
|
81
|
+
startPosition: car?.rank ?? null,
|
|
82
|
+
bestPosition: car?.rank ?? null,
|
|
83
|
+
startDistanceMeters: cumulativeDistanceMetersFromSnapshotCar(car),
|
|
84
|
+
}];
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
update(result) {
|
|
89
|
+
const currentCars = carsById(result.state.snapshot);
|
|
90
|
+
result.info.controlledDrivers.forEach((driverId) => {
|
|
91
|
+
const car = currentCars.get(driverId);
|
|
92
|
+
const entry = metrics[driverId];
|
|
93
|
+
if (!car || !entry) return;
|
|
94
|
+
entry.distanceMeters = Math.max(0, cumulativeDistanceMetersFromSnapshotCar(car) - entry.startDistanceMeters);
|
|
95
|
+
entry.lapProgressMeters = lapProgressMetersFromSnapshotCar(car);
|
|
96
|
+
const offTrack = isEnvironmentCarOffTrack(car);
|
|
97
|
+
if (offTrack) entry.offTrackSteps += 1;
|
|
98
|
+
if (entry.startedOffTrack && !offTrack) entry.recoverySuccess = true;
|
|
99
|
+
if (Number.isFinite(car.rank)) {
|
|
100
|
+
entry.bestPosition = entry.bestPosition == null ? car.rank : Math.min(entry.bestPosition, car.rank);
|
|
101
|
+
entry.passCount = entry.startPosition == null ? 0 : Math.max(0, entry.startPosition - entry.bestPosition);
|
|
102
|
+
}
|
|
103
|
+
const completedLaps = car.lapTelemetry?.completedLaps ?? 0;
|
|
104
|
+
if (completedLaps > 0 && entry.lapTimeSeconds == null) {
|
|
105
|
+
entry.lapTimeSeconds = result.info.elapsedSeconds;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
result.events.forEach((event) => {
|
|
109
|
+
if (!isEnvironmentContactEvent(event)) return;
|
|
110
|
+
Object.keys(metrics).forEach((driverId) => {
|
|
111
|
+
if (event.driverId === driverId || event.carId === driverId || event.otherCarId === driverId || event.driverIds?.includes?.(driverId)) {
|
|
112
|
+
metrics[driverId].contactCount += 1;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
finish() {
|
|
118
|
+
return Object.fromEntries(Object.entries(metrics).map(([driverId, entry]) => [
|
|
119
|
+
driverId,
|
|
120
|
+
{
|
|
121
|
+
distanceMeters: entry.distanceMeters,
|
|
122
|
+
lapProgressMeters: entry.lapProgressMeters,
|
|
123
|
+
offTrackSteps: entry.offTrackSteps,
|
|
124
|
+
contactCount: entry.contactCount,
|
|
125
|
+
recoverySuccess: entry.recoverySuccess,
|
|
126
|
+
passCount: entry.passCount,
|
|
127
|
+
lapTimeSeconds: entry.lapTimeSeconds,
|
|
128
|
+
},
|
|
129
|
+
]));
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function cumulativeDistanceMetersFromSnapshotCar(car) {
|
|
135
|
+
const distance = Number(car?.distanceMeters ?? car?.raceDistanceMeters);
|
|
136
|
+
if (Number.isFinite(distance)) return distance;
|
|
137
|
+
return simUnitsToMeters(car?.raceDistance ?? 0);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function lapProgressMetersFromSnapshotCar(car) {
|
|
141
|
+
const progressMeters = Number(car?.lapProgressMeters ?? car?.progressMeters);
|
|
142
|
+
if (Number.isFinite(progressMeters)) return progressMeters;
|
|
143
|
+
return simUnitsToMeters(car?.progress ?? 0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function runEvaluationCase({ baseOptions, policy, evaluationCase, createEnvironment }) {
|
|
147
|
+
const env = createEnvironment({
|
|
148
|
+
...baseOptions,
|
|
149
|
+
result: {
|
|
150
|
+
...(baseOptions.result ?? {}),
|
|
151
|
+
stateOutput: normalizeEvaluationStateOutput(baseOptions.result?.stateOutput),
|
|
152
|
+
},
|
|
153
|
+
seed: evaluationCase.seed ?? baseOptions.seed,
|
|
154
|
+
trackSeed: evaluationCase.trackSeed ?? baseOptions.trackSeed,
|
|
155
|
+
scenario: {
|
|
156
|
+
...(baseOptions.scenario ?? {}),
|
|
157
|
+
...(evaluationCase.scenario ?? {}),
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
try {
|
|
161
|
+
let result = env.reset();
|
|
162
|
+
const tracker = createEvaluationTracker(result);
|
|
163
|
+
const maxSteps = Math.max(1, Math.floor(evaluationCase.maxSteps ?? baseOptions.episode?.maxSteps ?? 1000));
|
|
164
|
+
let steps = 0;
|
|
165
|
+
while (!result.done && steps < maxSteps) {
|
|
166
|
+
const actions = Object.fromEntries(result.info.controlledDrivers.map((driverId) => [
|
|
167
|
+
driverId,
|
|
168
|
+
predictAction(policy, result.observation[driverId], {
|
|
169
|
+
driverId,
|
|
170
|
+
caseName: evaluationCase.name,
|
|
171
|
+
step: steps,
|
|
172
|
+
result,
|
|
173
|
+
}),
|
|
174
|
+
]));
|
|
175
|
+
result = env.step(actions);
|
|
176
|
+
tracker.update(result);
|
|
177
|
+
steps += 1;
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
name: evaluationCase.name,
|
|
181
|
+
seed: evaluationCase.seed ?? baseOptions.seed,
|
|
182
|
+
trackSeed: evaluationCase.trackSeed ?? baseOptions.trackSeed,
|
|
183
|
+
steps,
|
|
184
|
+
done: result.done,
|
|
185
|
+
endReason: result.info.endReason,
|
|
186
|
+
metrics: tracker.finish(),
|
|
187
|
+
};
|
|
188
|
+
} finally {
|
|
189
|
+
env.destroy?.();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeEvaluationStateOutput(stateOutput) {
|
|
194
|
+
return stateOutput === 'none' ? 'minimal' : stateOutput;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function predictAction(policy, observation, context) {
|
|
198
|
+
const action = typeof policy === 'function'
|
|
199
|
+
? policy(observation, context)
|
|
200
|
+
: policy.predict(observation, context);
|
|
201
|
+
return action ?? { steering: 0, throttle: 0, brake: 1 };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function carsById(snapshot) {
|
|
205
|
+
return new Map(snapshot.cars.map((car) => [car.id, car]));
|
|
206
|
+
}
|