@inventure71/paddockjs 1.0.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 +52 -0
- package/INSTALL_AND_UPDATE.md +4 -3
- package/README.md +194 -12
- package/docs/architecture.md +119 -44
- package/docs/concepts.md +4 -2
- package/docs/custom_model_controller.md +329 -0
- package/docs/data_contract.md +299 -17
- package/docs/index.md +1 -0
- package/docs/learnings.md +22 -0
- package/docs/rules.md +37 -10
- package/docs/system_specs.md +23 -10
- package/docs/training.md +272 -30
- package/examples/python/README.md +62 -0
- package/examples/python/base_policy_server.py +191 -0
- package/examples/python/requirements.txt +2 -0
- package/package.json +12 -4
- package/src/api/PaddockSimulatorController.js +4 -0
- package/src/app/BrowserExpertAdapter.js +367 -9
- package/src/app/F1SimulatorApp.js +305 -1603
- 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 +4 -0
- 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 +20 -0
- package/src/environment/actions.js +13 -5
- package/src/environment/controllerLoop.js +246 -0
- package/src/environment/episode.js +86 -2
- package/src/environment/evaluation.js +54 -32
- package/src/environment/index.d.ts +361 -9
- package/src/environment/index.js +1 -0
- package/src/environment/metrics.js +159 -0
- package/src/environment/observationOptions.js +22 -1
- package/src/environment/observations.js +325 -40
- package/src/environment/options.js +37 -5
- package/src/environment/runtime.js +222 -21
- package/src/environment/scenarios.js +37 -8
- 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 -367
- package/src/environment/specs.js +100 -5
- package/src/environment/workerProtocol.js +13 -1
- package/src/index.d.ts +169 -6
- package/src/index.js +5 -0
- package/src/rendering/proceduralTrackAsset.js +20 -823
- 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 +1 -1
- 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 -619
- 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 +230 -2817
- package/src/simulation/replay/replayGhosts.js +128 -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/penaltyStats.js +26 -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/trackLimitsSteward.js +1 -1
- package/src/simulation/rulesConfig.js +5 -323
- 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 -1706
- 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 -247
- package/src/simulation/vehiclePhysics.js +1 -136
- package/src/simulation/warmup/runtimeWarmup.js +224 -0
- package/src/simulation/wheelSurface.js +1 -338
- package/src/styles.css +42 -0
- package/src/ui/cameraControlsTemplate.js +20 -0
- package/src/ui/carOverviewTemplate.js +33 -0
- package/src/ui/componentTemplates.js +14 -360
- package/src/ui/raceCanvasTemplate.js +69 -0
- package/src/ui/raceControlsTemplate.js +36 -0
- package/src/ui/shellTemplate.js +5 -4
- package/src/ui/telemetryDrawerTemplate.js +55 -0
- package/src/ui/telemetryTemplates.js +136 -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
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { simUnitsToMeters } from '../simulation/units.js';
|
|
2
|
+
import { isEnvironmentCarOffTrack, isEnvironmentContactEvent } from './metrics.js';
|
|
2
3
|
import { createPaddockEnvironment } from './runtime.js';
|
|
3
4
|
|
|
4
5
|
export const DEFAULT_EVALUATION_CASES = Object.freeze([
|
|
@@ -41,7 +42,7 @@ export function runEnvironmentEvaluation({
|
|
|
41
42
|
if (!baseOptions || typeof baseOptions !== 'object') {
|
|
42
43
|
throw new Error('PaddockJS environment evaluation requires baseOptions.');
|
|
43
44
|
}
|
|
44
|
-
if (!policy) {
|
|
45
|
+
if (!isEvaluationPolicy(policy)) {
|
|
45
46
|
throw new Error('PaddockJS environment evaluation requires a policy function or object with predict().');
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -55,19 +56,28 @@ export function runEnvironmentEvaluation({
|
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
function isEvaluationPolicy(policy) {
|
|
60
|
+
return typeof policy === 'function' ||
|
|
61
|
+
(policy != null && typeof policy.predict === 'function');
|
|
62
|
+
}
|
|
63
|
+
|
|
58
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
|
+
}
|
|
59
68
|
const initialCars = carsById(initialResult.state.snapshot);
|
|
60
69
|
const metrics = Object.fromEntries(initialResult.info.controlledDrivers.map((driverId) => {
|
|
61
70
|
const car = initialCars.get(driverId);
|
|
71
|
+
const offTrack = isEnvironmentCarOffTrack(car);
|
|
62
72
|
return [driverId, {
|
|
63
73
|
distanceMeters: 0,
|
|
64
74
|
lapProgressMeters: lapProgressMetersFromSnapshotCar(car),
|
|
65
|
-
offTrackSteps:
|
|
75
|
+
offTrackSteps: offTrack ? 1 : 0,
|
|
66
76
|
contactCount: 0,
|
|
67
77
|
recoverySuccess: false,
|
|
68
78
|
passCount: 0,
|
|
69
79
|
lapTimeSeconds: null,
|
|
70
|
-
startedOffTrack:
|
|
80
|
+
startedOffTrack: offTrack,
|
|
71
81
|
startPosition: car?.rank ?? null,
|
|
72
82
|
bestPosition: car?.rank ?? null,
|
|
73
83
|
startDistanceMeters: cumulativeDistanceMetersFromSnapshotCar(car),
|
|
@@ -83,8 +93,9 @@ export function createEvaluationTracker(initialResult) {
|
|
|
83
93
|
if (!car || !entry) return;
|
|
84
94
|
entry.distanceMeters = Math.max(0, cumulativeDistanceMetersFromSnapshotCar(car) - entry.startDistanceMeters);
|
|
85
95
|
entry.lapProgressMeters = lapProgressMetersFromSnapshotCar(car);
|
|
86
|
-
|
|
87
|
-
if (
|
|
96
|
+
const offTrack = isEnvironmentCarOffTrack(car);
|
|
97
|
+
if (offTrack) entry.offTrackSteps += 1;
|
|
98
|
+
if (entry.startedOffTrack && !offTrack) entry.recoverySuccess = true;
|
|
88
99
|
if (Number.isFinite(car.rank)) {
|
|
89
100
|
entry.bestPosition = entry.bestPosition == null ? car.rank : Math.min(entry.bestPosition, car.rank);
|
|
90
101
|
entry.passCount = entry.startPosition == null ? 0 : Math.max(0, entry.startPosition - entry.bestPosition);
|
|
@@ -95,7 +106,7 @@ export function createEvaluationTracker(initialResult) {
|
|
|
95
106
|
}
|
|
96
107
|
});
|
|
97
108
|
result.events.forEach((event) => {
|
|
98
|
-
if (event
|
|
109
|
+
if (!isEnvironmentContactEvent(event)) return;
|
|
99
110
|
Object.keys(metrics).forEach((driverId) => {
|
|
100
111
|
if (event.driverId === driverId || event.carId === driverId || event.otherCarId === driverId || event.driverIds?.includes?.(driverId)) {
|
|
101
112
|
metrics[driverId].contactCount += 1;
|
|
@@ -135,6 +146,10 @@ function lapProgressMetersFromSnapshotCar(car) {
|
|
|
135
146
|
function runEvaluationCase({ baseOptions, policy, evaluationCase, createEnvironment }) {
|
|
136
147
|
const env = createEnvironment({
|
|
137
148
|
...baseOptions,
|
|
149
|
+
result: {
|
|
150
|
+
...(baseOptions.result ?? {}),
|
|
151
|
+
stateOutput: normalizeEvaluationStateOutput(baseOptions.result?.stateOutput),
|
|
152
|
+
},
|
|
138
153
|
seed: evaluationCase.seed ?? baseOptions.seed,
|
|
139
154
|
trackSeed: evaluationCase.trackSeed ?? baseOptions.trackSeed,
|
|
140
155
|
scenario: {
|
|
@@ -142,34 +157,41 @@ function runEvaluationCase({ baseOptions, policy, evaluationCase, createEnvironm
|
|
|
142
157
|
...(evaluationCase.scenario ?? {}),
|
|
143
158
|
},
|
|
144
159
|
});
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
driverId
|
|
152
|
-
predictAction(policy, result.observation[driverId], {
|
|
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) => [
|
|
153
167
|
driverId,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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?.();
|
|
162
190
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
trackSeed: evaluationCase.trackSeed ?? baseOptions.trackSeed,
|
|
168
|
-
steps,
|
|
169
|
-
done: result.done,
|
|
170
|
-
endReason: result.info.endReason,
|
|
171
|
-
metrics: tracker.finish(),
|
|
172
|
-
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeEvaluationStateOutput(stateOutput) {
|
|
194
|
+
return stateOutput === 'none' ? 'minimal' : stateOutput;
|
|
173
195
|
}
|
|
174
196
|
|
|
175
197
|
function predictAction(policy, observation, context) {
|