@inventure71/paddockjs 2.0.0 → 3.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 +37 -1
- package/INSTALL_AND_UPDATE.md +10 -1
- package/README.md +92 -17
- package/docs/architecture.md +26 -16
- package/docs/component_inventory.md +72 -0
- package/docs/concepts.md +7 -5
- package/docs/custom_model_controller.md +2 -2
- package/docs/data_contract.md +121 -33
- package/docs/data_model.md +1 -1
- package/docs/index.md +10 -0
- package/docs/learnings.md +6 -6
- package/docs/rules.md +39 -9
- package/docs/sense_contract.md +59 -0
- package/docs/system_specs.md +76 -20
- package/docs/training.md +27 -20
- package/package.json +1 -1
- package/src/api/PaddockSimulatorController.js +75 -36
- package/src/app/BrowserExpertAdapter.js +9 -300
- package/src/app/BrowserExpertExternalRenderer.js +303 -0
- package/src/app/F1SimulatorApp.js +190 -68
- package/src/app/banners/raceDataBannerController.js +71 -36
- package/src/app/banners/stewardMessageController.js +5 -4
- package/src/app/camera/cameraConstants.js +2 -0
- package/src/app/camera/cameraController.js +86 -29
- package/src/app/camera/cameraGeometry.js +12 -1
- package/src/app/domBindings.js +89 -9
- package/src/app/layoutScheduler.js +27 -0
- package/src/app/layoutSupport.js +165 -0
- package/src/app/raceOverlayClearanceSupport.js +158 -0
- package/src/app/readouts/carOverviewRenderer.js +16 -8
- package/src/app/readouts/controlStateRenderer.js +1 -1
- package/src/app/readouts/raceStatusRenderer.js +30 -21
- package/src/app/readouts/readoutFormatters.js +2 -0
- package/src/app/readouts/telemetryRenderer.js +17 -8
- package/src/app/readouts/timingTowerRenderer.js +62 -10
- package/src/app/runtime/runtimeVisibility.js +66 -5
- package/src/config/cssValues.js +16 -0
- package/src/config/defaultOptions.js +51 -38
- package/src/config/restartOptions.js +34 -0
- package/src/config/themeOptions.js +825 -0
- package/src/config/timingGapMode.js +13 -0
- package/src/config/urlOptions.js +25 -0
- package/src/data/championship.js +14 -9
- package/src/environment/controllerLoop.js +34 -5
- package/src/environment/episode.js +14 -2
- package/src/environment/index.d.ts +17 -4
- package/src/environment/observationVector.js +200 -0
- package/src/environment/observations.js +58 -285
- package/src/environment/options.js +1 -1
- package/src/environment/recorder.js +13 -6
- package/src/environment/rewards.js +13 -2
- package/src/environment/runtime.js +65 -25
- package/src/environment/scenarios.js +22 -3
- package/src/environment/sensors/index.js +3 -3
- package/src/environment/sensors/opponentRadar.js +13 -3
- package/src/environment/sensors/sensorTargets.js +8 -2
- package/src/environment/sensors/surfaceRays.js +84 -12
- package/src/environment/sensors/trackRays.js +173 -15
- package/src/environment/specs.js +78 -40
- package/src/index.d.ts +112 -9
- package/src/index.js +26 -33
- package/src/simulation/driver/driverController.js +1 -1
- package/src/simulation/driver/edgeRecovery.js +1 -1
- package/src/simulation/driver/racingControls.js +2 -2
- package/src/simulation/driver/rejoinControls.js +2 -2
- package/src/simulation/pit/pitFlow.js +5 -0
- package/src/simulation/pit/pitKinematics.js +24 -0
- package/src/simulation/pit/pitQueue.js +5 -0
- package/src/simulation/pit/pitRouteMovement.js +7 -0
- package/src/simulation/pit/pitRouting.js +2 -4
- package/src/simulation/pit/pitService.js +2 -0
- package/src/simulation/pit/pitTireService.js +2 -0
- package/src/simulation/race/raceLifecycle.js +2 -6
- package/src/simulation/race/raceProgress.js +6 -1
- package/src/simulation/race/raceSetup.js +2 -6
- package/src/simulation/race/raceStep.js +2 -2
- package/src/simulation/race/redFlag.js +3 -3
- package/src/simulation/race/safetyCar.js +10 -4
- package/src/simulation/race/stalledDnf.js +8 -17
- package/src/simulation/raceSimulation.js +1 -0
- package/src/simulation/rules/rulesReview.js +3 -0
- package/src/simulation/snapshots/raceSnapshots.js +1 -0
- package/src/simulation/timing/lapTelemetry.js +28 -9
- package/src/simulation/track/pitLaneLayout.js +7 -3
- package/src/simulation/track/spatialQueries.js +5 -28
- package/src/simulation/track/trackQueryGrid.js +94 -0
- package/src/simulation/track/trackQueryIndex.js +193 -434
- package/src/simulation/track/trackQueryPitIndex.js +140 -0
- package/src/simulation/track/trackQueryScratch.js +46 -0
- package/src/simulation/track/trackQueryStats.js +28 -0
- package/src/simulation/track/trackStatePolicy.js +0 -1
- package/src/simulation/vehicle/contactResolution.js +67 -2
- package/src/simulation/vehicle/runoffResponse.js +11 -12
- package/src/simulation/vehicle/vehicleKinematics.js +37 -0
- package/src/simulation/vehicle/vehiclePhysics.js +5 -4
- package/src/simulation/vehicle/vehicleSnapshots.js +22 -0
- package/src/simulation/vehicle/vehicleState.js +36 -2
- package/src/simulation/vehicle/wheelSurface.js +3 -1
- package/src/simulation/warmup/runtimeWarmup.js +0 -1
- package/src/styles.css +1185 -136
- package/src/ui/bannerTemplates.js +38 -28
- package/src/ui/cameraControlsTemplate.js +16 -5
- package/src/ui/carOverviewTemplate.js +12 -5
- package/src/ui/raceCanvasTemplate.js +31 -8
- package/src/ui/raceControlsTemplate.js +27 -10
- package/src/ui/shellTemplate.js +3 -1
- package/src/ui/telemetryDrawerTemplate.js +23 -10
- package/src/ui/telemetryTemplates.js +58 -32
- package/src/ui/templateUtils.js +48 -0
- package/src/ui/timingTowerTemplate.js +29 -12
|
@@ -7,9 +7,9 @@ import { buildBoundarySenses } from './sensors/boundarySenses.js';
|
|
|
7
7
|
import { buildContactPatchSenses } from './sensors/contactSenses.js';
|
|
8
8
|
import { enrichOpponentRadar } from './sensors/opponentRadar.js';
|
|
9
9
|
import { buildNearbyCars, buildRaySensors, createRayBatchContext, normalizeRayOptions } from './sensors.js';
|
|
10
|
+
import { buildObservationVector } from './observationVector.js';
|
|
10
11
|
|
|
11
12
|
const EMPTY_EVENTS = Object.freeze([]);
|
|
12
|
-
const PHYSICAL_RAY_SURFACE_CHANNELS = Object.freeze(['kerb', 'illegalSurface']);
|
|
13
13
|
|
|
14
14
|
export function buildEnvironmentObservation({ snapshot, options, events = [], controlledDrivers = options.controlledDrivers }) {
|
|
15
15
|
const carsById = new Map(snapshot.cars.map((entry) => [entry.id, entry]));
|
|
@@ -171,168 +171,7 @@ function isCarLegallyOnTrack(car) {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
function buildDriverVector(object, sensors, { includeSchema = true, vectorType = 'array' } = {}) {
|
|
174
|
-
|
|
175
|
-
const schema = includeSchema ? [
|
|
176
|
-
{ name: 'self.speedKph', unit: 'kph', scale: 'fixed:400' },
|
|
177
|
-
{ name: 'self.speedMetersPerSecond', unit: 'm/s', scale: 'fixed:120' },
|
|
178
|
-
{ name: 'self.steeringAngleRadians', unit: 'rad', scale: 'fixed:pi' },
|
|
179
|
-
{ name: 'self.throttle', scale: '0..1' },
|
|
180
|
-
{ name: 'self.brake', scale: '0..1' },
|
|
181
|
-
{ name: 'self.lateralG', scale: 'fixed:8g' },
|
|
182
|
-
{ name: 'self.longitudinalG', scale: 'fixed:6g' },
|
|
183
|
-
{ name: 'self.gripUsage', scale: '0..2' },
|
|
184
|
-
{ name: 'self.slipAngleRadians', unit: 'rad', scale: 'fixed:pi' },
|
|
185
|
-
{ name: 'self.tractionLimited', scale: 'boolean' },
|
|
186
|
-
{ name: 'self.lapProgressRatio', scale: '0..1' },
|
|
187
|
-
{ name: 'self.trackOffsetMeters', unit: 'm', scale: 'fixed:meters' },
|
|
188
|
-
{ name: 'self.trackHeadingErrorRadians', unit: 'rad', scale: 'fixed:pi' },
|
|
189
|
-
{ name: 'self.onTrack', scale: 'boolean' },
|
|
190
|
-
{ name: 'self.inPitLane', scale: 'boolean' },
|
|
191
|
-
{ name: 'self.tireEnergy', scale: '0..100' },
|
|
192
|
-
{ name: 'self.pitIntent', scale: '0..2' },
|
|
193
|
-
{ name: 'self.pitStopActive', scale: 'boolean' },
|
|
194
|
-
{ name: 'race.positionNormalized', scale: '0..1' },
|
|
195
|
-
{ name: 'race.raceModeGreen', scale: 'boolean' },
|
|
196
|
-
{ name: 'race.raceModeSafetyCar', scale: 'boolean' },
|
|
197
|
-
{ name: 'race.redFlag', scale: 'boolean' },
|
|
198
|
-
{ name: 'race.pitLaneOpen', scale: 'boolean' },
|
|
199
|
-
{ name: 'track.curvature', scale: 'track-curvature' },
|
|
200
|
-
] : null;
|
|
201
|
-
const vector = [
|
|
202
|
-
object.self.speedKph / 400,
|
|
203
|
-
object.self.speedMetersPerSecond / 120,
|
|
204
|
-
object.self.steeringAngleRadians / Math.PI,
|
|
205
|
-
object.self.throttle,
|
|
206
|
-
object.self.brake,
|
|
207
|
-
object.self.lateralG / 8,
|
|
208
|
-
object.self.longitudinalG / 6,
|
|
209
|
-
object.self.gripUsage / 2,
|
|
210
|
-
object.self.slipAngleRadians / Math.PI,
|
|
211
|
-
object.self.tractionLimited ? 1 : 0,
|
|
212
|
-
normalizeLapProgress(object),
|
|
213
|
-
object.self.trackOffsetMeters,
|
|
214
|
-
object.self.trackHeadingErrorRadians / Math.PI,
|
|
215
|
-
object.self.onTrack ? 1 : 0,
|
|
216
|
-
object.self.inPitLane ? 1 : 0,
|
|
217
|
-
(object.self.tireEnergy ?? 0) / 100,
|
|
218
|
-
(object.self.pitIntent ?? 0) / 2,
|
|
219
|
-
object.self.pitStopStatus && object.self.pitStopStatus !== 'pending' && object.self.pitStopStatus !== 'completed' ? 1 : 0,
|
|
220
|
-
normalizeRacePosition(object),
|
|
221
|
-
object.race.raceMode === 'green' ? 1 : 0,
|
|
222
|
-
object.race.raceMode === 'safety-car' ? 1 : 0,
|
|
223
|
-
object.race.redFlag ? 1 : 0,
|
|
224
|
-
object.race.pitLaneOpen ? 1 : 0,
|
|
225
|
-
object.track.curvature ?? 0,
|
|
226
|
-
];
|
|
227
|
-
if (includePhysicalDriverSenses) {
|
|
228
|
-
pushSchema(schema,
|
|
229
|
-
{ name: 'self.yawRateRadiansPerSecond', unit: 'rad/s', scale: 'fixed:pi' },
|
|
230
|
-
{ name: 'trackRelation.leftBoundaryMeters', unit: 'm', scale: 'fixed:meters' },
|
|
231
|
-
{ name: 'trackRelation.rightBoundaryMeters', unit: 'm', scale: 'fixed:meters' },
|
|
232
|
-
{ name: 'trackRelation.legalWidthMeters', unit: 'm', scale: 'fixed:meters' },
|
|
233
|
-
);
|
|
234
|
-
vector.push(
|
|
235
|
-
object.self.yawRateRadiansPerSecond / Math.PI,
|
|
236
|
-
object.trackRelation.leftBoundaryMeters,
|
|
237
|
-
object.trackRelation.rightBoundaryMeters,
|
|
238
|
-
object.trackRelation.legalWidthMeters,
|
|
239
|
-
);
|
|
240
|
-
object.contactPatches.forEach((patch, index) => {
|
|
241
|
-
pushSchema(schema,
|
|
242
|
-
{ name: `contactPatches[${index}].present`, scale: 'boolean' },
|
|
243
|
-
{ name: `contactPatches[${index}].surfaceCode`, scale: 'surface-code' },
|
|
244
|
-
{ name: `contactPatches[${index}].onLegalSurface`, scale: 'boolean' },
|
|
245
|
-
{ name: `contactPatches[${index}].signedOffsetMeters`, unit: 'm', scale: 'fixed:meters' },
|
|
246
|
-
);
|
|
247
|
-
vector.push(
|
|
248
|
-
patch.present ? 1 : 0,
|
|
249
|
-
patch.surfaceCode / 5,
|
|
250
|
-
patch.onLegalSurface ? 1 : 0,
|
|
251
|
-
patch.signedOffsetMeters,
|
|
252
|
-
);
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
object.track.lookahead.forEach((sample, index) => {
|
|
256
|
-
pushSchema(schema,
|
|
257
|
-
{ name: `track.lookahead[${index}].curvature`, scale: 'track-curvature' },
|
|
258
|
-
{ name: `track.lookahead[${index}].headingDeltaRadians`, unit: 'rad', scale: 'fixed:pi' },
|
|
259
|
-
);
|
|
260
|
-
vector.push(sample.curvature ?? 0, (sample.headingDeltaRadians ?? 0) / Math.PI);
|
|
261
|
-
});
|
|
262
|
-
object.rays.forEach((ray, index) => {
|
|
263
|
-
pushSchema(schema,
|
|
264
|
-
{ name: `rays[${index}].track.distanceRatio`, scale: '0..1' },
|
|
265
|
-
{ name: `rays[${index}].track.hit`, scale: 'boolean' },
|
|
266
|
-
{ name: `rays[${index}].track.kindExit`, scale: 'boolean' },
|
|
267
|
-
{ name: `rays[${index}].track.kindEntry`, scale: 'boolean' },
|
|
268
|
-
{ name: `rays[${index}].car.distanceRatio`, scale: '0..1' },
|
|
269
|
-
{ name: `rays[${index}].car.hit`, scale: 'boolean' },
|
|
270
|
-
{ name: `rays[${index}].car.relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
|
|
271
|
-
);
|
|
272
|
-
vector.push(
|
|
273
|
-
ratio(ray.track.distanceMeters, ray.lengthMeters),
|
|
274
|
-
ray.track.hit ? 1 : 0,
|
|
275
|
-
ray.track.kind === 'exit' ? 1 : 0,
|
|
276
|
-
ray.track.kind === 'entry' ? 1 : 0,
|
|
277
|
-
ratio(ray.car.distanceMeters, ray.lengthMeters),
|
|
278
|
-
ray.car.hit ? 1 : 0,
|
|
279
|
-
ray.car.relativeSpeedKph / 200,
|
|
280
|
-
);
|
|
281
|
-
if (includePhysicalDriverSenses) {
|
|
282
|
-
PHYSICAL_RAY_SURFACE_CHANNELS.forEach((channel) => {
|
|
283
|
-
pushSchema(schema,
|
|
284
|
-
{ name: `rays[${index}].${channel}.distanceRatio`, scale: '0..1' },
|
|
285
|
-
{ name: `rays[${index}].${channel}.hit`, scale: 'boolean' },
|
|
286
|
-
);
|
|
287
|
-
vector.push(
|
|
288
|
-
ratio(ray[channel]?.distanceMeters ?? ray.lengthMeters, ray.lengthMeters),
|
|
289
|
-
ray[channel]?.hit ? 1 : 0,
|
|
290
|
-
);
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
const nearbyLimit = sensors.nearbyCars.enabled ? (sensors.nearbyCars.maxCars ?? object.nearbyCars.length) : 0;
|
|
295
|
-
const nearbyRadius = sensors.nearbyCars.radiusMeters ?? 150;
|
|
296
|
-
for (let index = 0; index < nearbyLimit; index += 1) {
|
|
297
|
-
const nearby = object.nearbyCars[index] ?? null;
|
|
298
|
-
pushSchema(schema,
|
|
299
|
-
{ name: `nearbyCars[${index}].present`, scale: 'boolean' },
|
|
300
|
-
{ name: `nearbyCars[${index}].relativeForwardRatio`, scale: '-1..1' },
|
|
301
|
-
{ name: `nearbyCars[${index}].relativeRightRatio`, scale: '-1..1' },
|
|
302
|
-
{ name: `nearbyCars[${index}].relativeDistanceRatio`, scale: '0..1' },
|
|
303
|
-
{ name: `nearbyCars[${index}].relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
|
|
304
|
-
{ name: `nearbyCars[${index}].relativeHeadingRadians`, unit: 'rad', scale: 'fixed:pi' },
|
|
305
|
-
{ name: `nearbyCars[${index}].ahead`, scale: 'boolean' },
|
|
306
|
-
{ name: `nearbyCars[${index}].sameLap`, scale: 'boolean' },
|
|
307
|
-
);
|
|
308
|
-
vector.push(
|
|
309
|
-
nearby ? 1 : 0,
|
|
310
|
-
clampRatio((nearby?.relativeForwardMeters ?? 0) / nearbyRadius),
|
|
311
|
-
clampRatio((nearby?.relativeRightMeters ?? 0) / nearbyRadius),
|
|
312
|
-
ratio(nearby?.relativeDistanceMeters ?? nearbyRadius, nearbyRadius),
|
|
313
|
-
(nearby?.relativeSpeedKph ?? 0) / 200,
|
|
314
|
-
(nearby?.relativeHeadingRadians ?? 0) / Math.PI,
|
|
315
|
-
nearby?.ahead ? 1 : 0,
|
|
316
|
-
nearby?.sameLap ? 1 : 0,
|
|
317
|
-
);
|
|
318
|
-
if (includePhysicalDriverSenses) {
|
|
319
|
-
pushSchema(schema,
|
|
320
|
-
{ name: `nearbyCars[${index}].behind`, scale: 'boolean' },
|
|
321
|
-
{ name: `nearbyCars[${index}].closingRateMetersPerSecond`, unit: 'm/s', scale: 'fixed:100' },
|
|
322
|
-
{ name: `nearbyCars[${index}].timeToContactSeconds`, unit: 's', scale: 'fixed:10' },
|
|
323
|
-
{ name: `nearbyCars[${index}].leftOverlap`, scale: 'boolean' },
|
|
324
|
-
{ name: `nearbyCars[${index}].rightOverlap`, scale: 'boolean' },
|
|
325
|
-
);
|
|
326
|
-
vector.push(
|
|
327
|
-
nearby?.behind ? 1 : 0,
|
|
328
|
-
(nearby?.closingRateMetersPerSecond ?? 0) / 100,
|
|
329
|
-
ratio(nearby?.timeToContactSeconds ?? 10, 10),
|
|
330
|
-
nearby?.leftOverlap ? 1 : 0,
|
|
331
|
-
nearby?.rightOverlap ? 1 : 0,
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
return { vector: finalizeVector(vector, vectorType), schema: schema ?? [] };
|
|
174
|
+
return buildObservationVector(object, sensors, { includeSchema, vectorType });
|
|
336
175
|
}
|
|
337
176
|
|
|
338
177
|
function buildDriverVectorDirect(car, snapshot, options, events, sensors, getRayBatchContext = null) {
|
|
@@ -353,95 +192,40 @@ function buildDriverVectorDirect(car, snapshot, options, events, sensors, getRay
|
|
|
353
192
|
: [];
|
|
354
193
|
const trackLengthMeters = simUnitsToMeters(snapshot.track.length ?? 0);
|
|
355
194
|
const trackCurvature = car.trackState?.curvature ?? pointAt(snapshot.track, car.progress ?? 0).curvature ?? 0;
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
contactPatches.forEach((patch) => {
|
|
390
|
-
vector.push(
|
|
391
|
-
patch.present ? 1 : 0,
|
|
392
|
-
patch.surfaceCode / 5,
|
|
393
|
-
patch.onLegalSurface ? 1 : 0,
|
|
394
|
-
patch.signedOffsetMeters,
|
|
395
|
-
);
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
buildTrackLookahead(car, snapshot, options).forEach((sample) => {
|
|
399
|
-
vector.push(sample.curvature ?? 0, (sample.headingDeltaRadians ?? 0) / Math.PI);
|
|
400
|
-
});
|
|
401
|
-
rays.forEach((ray) => {
|
|
402
|
-
vector.push(
|
|
403
|
-
ratio(ray.track.distanceMeters, ray.lengthMeters),
|
|
404
|
-
ray.track.hit ? 1 : 0,
|
|
405
|
-
ray.track.kind === 'exit' ? 1 : 0,
|
|
406
|
-
ray.track.kind === 'entry' ? 1 : 0,
|
|
407
|
-
ratio(ray.car.distanceMeters, ray.lengthMeters),
|
|
408
|
-
ray.car.hit ? 1 : 0,
|
|
409
|
-
ray.car.relativeSpeedKph / 200,
|
|
410
|
-
);
|
|
411
|
-
if (includePhysicalDriverSenses) {
|
|
412
|
-
PHYSICAL_RAY_SURFACE_CHANNELS.forEach((channel) => {
|
|
413
|
-
vector.push(
|
|
414
|
-
ratio(ray[channel]?.distanceMeters ?? ray.lengthMeters, ray.lengthMeters),
|
|
415
|
-
ray[channel]?.hit ? 1 : 0,
|
|
416
|
-
);
|
|
417
|
-
});
|
|
418
|
-
}
|
|
195
|
+
const source = {
|
|
196
|
+
profile,
|
|
197
|
+
self: {
|
|
198
|
+
...body,
|
|
199
|
+
lapProgressMeters: simUnitsToMeters(car.progress ?? 0),
|
|
200
|
+
trackOffsetMeters: trackRelation.lateralOffsetMeters,
|
|
201
|
+
trackHeadingErrorRadians: trackHeadingError,
|
|
202
|
+
onTrack,
|
|
203
|
+
inPitLane: Boolean(car.inPitLane),
|
|
204
|
+
tireEnergy: car.tireEnergy ?? null,
|
|
205
|
+
pitIntent: car.pitIntent ?? car.pitStop?.intent ?? 0,
|
|
206
|
+
pitStopStatus: car.pitStop?.status ?? null,
|
|
207
|
+
},
|
|
208
|
+
trackRelation,
|
|
209
|
+
contactPatches,
|
|
210
|
+
race: {
|
|
211
|
+
position: car.rank,
|
|
212
|
+
totalCars: snapshot.cars.length,
|
|
213
|
+
raceMode: snapshot.raceControl.mode,
|
|
214
|
+
pitLaneOpen: Boolean(snapshot.raceControl.pitLaneOpen),
|
|
215
|
+
redFlag: Boolean(snapshot.raceControl.redFlag),
|
|
216
|
+
},
|
|
217
|
+
track: {
|
|
218
|
+
lengthMeters: trackLengthMeters,
|
|
219
|
+
curvature: trackCurvature,
|
|
220
|
+
lookahead: buildTrackLookahead(car, snapshot, options),
|
|
221
|
+
},
|
|
222
|
+
rays,
|
|
223
|
+
nearbyCars,
|
|
224
|
+
};
|
|
225
|
+
return buildObservationVector(source, sensors, {
|
|
226
|
+
includeSchema: false,
|
|
227
|
+
vectorType: options.observation?.vectorType,
|
|
419
228
|
});
|
|
420
|
-
const nearbyLimit = sensors.nearbyCars.enabled ? (sensors.nearbyCars.maxCars ?? nearbyCars.length) : 0;
|
|
421
|
-
const nearbyRadius = sensors.nearbyCars.radiusMeters ?? 150;
|
|
422
|
-
for (let index = 0; index < nearbyLimit; index += 1) {
|
|
423
|
-
const nearby = nearbyCars[index] ?? null;
|
|
424
|
-
vector.push(
|
|
425
|
-
nearby ? 1 : 0,
|
|
426
|
-
clampRatio((nearby?.relativeForwardMeters ?? 0) / nearbyRadius),
|
|
427
|
-
clampRatio((nearby?.relativeRightMeters ?? 0) / nearbyRadius),
|
|
428
|
-
ratio(nearby?.relativeDistanceMeters ?? nearbyRadius, nearbyRadius),
|
|
429
|
-
(nearby?.relativeSpeedKph ?? 0) / 200,
|
|
430
|
-
(nearby?.relativeHeadingRadians ?? 0) / Math.PI,
|
|
431
|
-
nearby?.ahead ? 1 : 0,
|
|
432
|
-
nearby?.sameLap ? 1 : 0,
|
|
433
|
-
);
|
|
434
|
-
if (includePhysicalDriverSenses) {
|
|
435
|
-
vector.push(
|
|
436
|
-
nearby?.behind ? 1 : 0,
|
|
437
|
-
(nearby?.closingRateMetersPerSecond ?? 0) / 100,
|
|
438
|
-
ratio(nearby?.timeToContactSeconds ?? 10, 10),
|
|
439
|
-
nearby?.leftOverlap ? 1 : 0,
|
|
440
|
-
nearby?.rightOverlap ? 1 : 0,
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
return { vector: finalizeVector(vector, options.observation?.vectorType), schema: [] };
|
|
445
229
|
}
|
|
446
230
|
|
|
447
231
|
function rayBatchContextForSensors(sensors, getRayBatchContext) {
|
|
@@ -449,48 +233,18 @@ function rayBatchContextForSensors(sensors, getRayBatchContext) {
|
|
|
449
233
|
return getRayBatchContext();
|
|
450
234
|
}
|
|
451
235
|
|
|
452
|
-
function
|
|
453
|
-
return vectorType === 'float32' ? Float32Array.from(vector) : vector;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function pushSchema(schema, ...entries) {
|
|
457
|
-
if (schema) schema.push(...entries);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function normalizeLapProgress(object) {
|
|
461
|
-
return ratio(object.self.lapProgressMeters, object.track.lengthMeters || 1);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function normalizeRacePosition(object) {
|
|
465
|
-
const totalCars = Number(object.race.totalCars);
|
|
466
|
-
const position = Number(object.race.position);
|
|
467
|
-
if (!Number.isFinite(totalCars) || totalCars <= 1 || !Number.isFinite(position)) return 0;
|
|
468
|
-
return ratio(position - 1, totalCars - 1);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
function ratio(value, max) {
|
|
472
|
-
const finite = Number.isFinite(value) ? value : max;
|
|
473
|
-
return Math.max(0, Math.min(1, finite / Math.max(1e-9, max)));
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function clampRatio(value) {
|
|
477
|
-
return Math.max(-1, Math.min(1, Number.isFinite(value) ? value : 0));
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
function defaultSensorOptions(options) {
|
|
236
|
+
export function defaultSensorOptions(options) {
|
|
481
237
|
return {
|
|
482
238
|
rays: normalizeRayOptions(options.sensors.rays),
|
|
483
239
|
nearbyCars: options.sensors.nearbyCars,
|
|
484
240
|
};
|
|
485
241
|
}
|
|
486
242
|
|
|
487
|
-
function effectiveSensorOptions(options, driverId, defaultSensors = defaultSensorOptions(options), hasSensorOverrides = true) {
|
|
243
|
+
export function effectiveSensorOptions(options, driverId, defaultSensors = defaultSensorOptions(options), hasSensorOverrides = true) {
|
|
488
244
|
if (!hasSensorOverrides || !options.sensorsByDriver?.[driverId]) return defaultSensors;
|
|
245
|
+
const rayOverrides = options.sensorsByDriver?.[driverId]?.rays ?? {};
|
|
489
246
|
return {
|
|
490
|
-
rays: normalizeRayOptions(
|
|
491
|
-
...options.sensors.rays,
|
|
492
|
-
...(options.sensorsByDriver?.[driverId]?.rays ?? {}),
|
|
493
|
-
}),
|
|
247
|
+
rays: normalizeRayOptions(mergeDriverRayOptions(options.sensors.rays, rayOverrides)),
|
|
494
248
|
nearbyCars: {
|
|
495
249
|
...options.sensors.nearbyCars,
|
|
496
250
|
...(options.sensorsByDriver?.[driverId]?.nearbyCars ?? {}),
|
|
@@ -498,6 +252,25 @@ function effectiveSensorOptions(options, driverId, defaultSensors = defaultSenso
|
|
|
498
252
|
};
|
|
499
253
|
}
|
|
500
254
|
|
|
255
|
+
function mergeDriverRayOptions(baseRays, rayOverrides = {}) {
|
|
256
|
+
const merged = {
|
|
257
|
+
...baseRays,
|
|
258
|
+
...rayOverrides,
|
|
259
|
+
};
|
|
260
|
+
if (Object.hasOwn(rayOverrides, 'rays')) return merged;
|
|
261
|
+
if (Object.hasOwn(rayOverrides, 'layout') || Object.hasOwn(rayOverrides, 'anglesDegrees')) {
|
|
262
|
+
delete merged.rays;
|
|
263
|
+
return merged;
|
|
264
|
+
}
|
|
265
|
+
if (Object.hasOwn(rayOverrides, 'lengthMeters') || Object.hasOwn(rayOverrides, 'defaultLengthMeters')) {
|
|
266
|
+
merged.rays = (baseRays.rays ?? []).map((ray) => ({
|
|
267
|
+
id: ray.id,
|
|
268
|
+
angleDegrees: ray.angleDegrees,
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
return merged;
|
|
272
|
+
}
|
|
273
|
+
|
|
501
274
|
function emptyObservation(driverId) {
|
|
502
275
|
return {
|
|
503
276
|
object: {
|
|
@@ -8,7 +8,7 @@ import { normalizeRayOptions } from './sensors.js';
|
|
|
8
8
|
|
|
9
9
|
const DEFAULT_SEED = 1971;
|
|
10
10
|
const DEFAULT_FRAME_SKIP = 1;
|
|
11
|
-
const DEFAULT_MAX_STEPS =
|
|
11
|
+
const DEFAULT_MAX_STEPS = 1_000_000;
|
|
12
12
|
const RESULT_STATE_OUTPUTS = new Set(['full', 'minimal', 'none']);
|
|
13
13
|
const RESET_OBSERVATION_SCOPES = new Set(['all', 'reset']);
|
|
14
14
|
|
|
@@ -10,19 +10,26 @@ export function createRolloutRecorder() {
|
|
|
10
10
|
transitions.length = 0;
|
|
11
11
|
},
|
|
12
12
|
toJSON() {
|
|
13
|
-
return transitions.map((entry) => (
|
|
13
|
+
return transitions.map((entry) => cloneTrainingValue(entry));
|
|
14
14
|
},
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function createRolloutTransition(previousResult, action, nextResult) {
|
|
19
19
|
return {
|
|
20
|
-
observation: previousResult.observation,
|
|
21
|
-
action,
|
|
22
|
-
reward: nextResult.reward,
|
|
23
|
-
nextObservation: nextResult.observation,
|
|
20
|
+
observation: cloneTrainingValue(previousResult.observation),
|
|
21
|
+
action: cloneTrainingValue(action),
|
|
22
|
+
reward: cloneTrainingValue(nextResult.reward),
|
|
23
|
+
nextObservation: cloneTrainingValue(nextResult.observation),
|
|
24
24
|
terminated: nextResult.terminated,
|
|
25
25
|
truncated: nextResult.truncated,
|
|
26
|
-
info: nextResult.info,
|
|
26
|
+
info: cloneTrainingValue(nextResult.info),
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
function cloneTrainingValue(value) {
|
|
31
|
+
if (value == null || typeof value !== 'object') return value;
|
|
32
|
+
if (ArrayBuffer.isView(value)) return Array.from(value);
|
|
33
|
+
if (Array.isArray(value)) return value.map(cloneTrainingValue);
|
|
34
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, cloneTrainingValue(entry)]));
|
|
35
|
+
}
|
|
@@ -19,8 +19,10 @@ export function createProgressReward(options = {}) {
|
|
|
19
19
|
const currentCar = findSnapshotCar(context.state?.snapshot, driverId);
|
|
20
20
|
const progressMeters = getProgressMeters(previousCar, currentCar);
|
|
21
21
|
const self = context.current?.object?.self ?? {};
|
|
22
|
-
const speedKph = Number(self.speedKph ?? 0);
|
|
23
|
-
const isOnTrack =
|
|
22
|
+
const speedKph = Number(self.speedKph ?? currentCar?.speedKph ?? 0);
|
|
23
|
+
const isOnTrack = context.metrics?.offTrack != null
|
|
24
|
+
? !context.metrics.offTrack
|
|
25
|
+
: self.onTrack ?? isSnapshotCarOnTrack(currentCar);
|
|
24
26
|
const collisionCount = countDriverCollisions(context.events, driverId);
|
|
25
27
|
const steering = Math.abs(Number(context.action?.steering ?? 0));
|
|
26
28
|
const brake = Math.abs(Number(context.action?.brake ?? 0));
|
|
@@ -49,6 +51,15 @@ function getProgressMeters(previousCar, currentCar) {
|
|
|
49
51
|
return current - previous;
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
function isSnapshotCarOnTrack(car) {
|
|
55
|
+
if (!car) return true;
|
|
56
|
+
if (car.inPitLane) return true;
|
|
57
|
+
if (Array.isArray(car.wheels) && car.wheels.length > 0) {
|
|
58
|
+
return car.wheels.every((wheel) => wheel.onTrack || wheel.inPitLane);
|
|
59
|
+
}
|
|
60
|
+
return ['track', 'kerb', 'pit-entry', 'pit-lane', 'pit-exit', 'pit-box'].includes(car.surface ?? 'track');
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
function countDriverCollisions(events = [], driverId) {
|
|
53
64
|
if (!driverId || !Array.isArray(events)) return 0;
|
|
54
65
|
return events.filter((event) => isCollisionEvent(event) && eventIncludesDriver(event, driverId)).length;
|
|
@@ -38,16 +38,14 @@ export function createPaddockEnvironment(options = {}) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function createSimulationWithEnvironmentScenario(options) {
|
|
41
|
-
const simulationOptions = {
|
|
42
|
-
...options,
|
|
43
|
-
trackQueryIndex: shouldUseTrackQueryIndex(options),
|
|
44
|
-
};
|
|
45
41
|
runWarmupWithGuard({
|
|
46
|
-
options
|
|
42
|
+
options,
|
|
47
43
|
surface: 'environment',
|
|
48
44
|
execute: ({ warmup }) => {
|
|
49
|
-
const warmupSimulation = createRaceSimulation(disableWarmupOptions(
|
|
45
|
+
const warmupSimulation = createRaceSimulation(disableWarmupOptions(options, warmup.surface));
|
|
46
|
+
initializeControlledPitIntentForSimulation(warmupSimulation, options);
|
|
50
47
|
applyEnvironmentScenario(warmupSimulation, options);
|
|
48
|
+
applyControlledRunoffResponse(warmupSimulation, options);
|
|
51
49
|
for (let index = 0; index < warmup.steps; index += 1) warmupSimulation.step(FIXED_STEP);
|
|
52
50
|
const warmupSnapshot = warmupSimulation.snapshotObservation?.() ?? warmupSimulation.snapshot();
|
|
53
51
|
buildEnvironmentObservation({
|
|
@@ -57,15 +55,13 @@ function createSimulationWithEnvironmentScenario(options) {
|
|
|
57
55
|
});
|
|
58
56
|
},
|
|
59
57
|
});
|
|
60
|
-
const sim = createRaceSimulation(disableWarmupOptions(
|
|
58
|
+
const sim = createRaceSimulation(disableWarmupOptions(options, 'environment'));
|
|
59
|
+
initializeControlledPitIntentForSimulation(sim, options);
|
|
61
60
|
applyEnvironmentScenario(sim, options);
|
|
61
|
+
applyControlledRunoffResponse(sim, options);
|
|
62
62
|
return sim;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function shouldUseTrackQueryIndex(options) {
|
|
66
|
-
return options.trackQueryIndex !== false;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
65
|
export function createEnvironmentRuntime(host) {
|
|
70
66
|
const episodeState = createEpisodeState();
|
|
71
67
|
|
|
@@ -120,7 +116,14 @@ export function createEnvironmentRuntime(host) {
|
|
|
120
116
|
}
|
|
121
117
|
episodeState.step += 1;
|
|
122
118
|
advanceDriverEpisodes(episodeState, options.controlledDrivers);
|
|
123
|
-
const result = buildResult({
|
|
119
|
+
const result = buildResult({
|
|
120
|
+
host,
|
|
121
|
+
episodeState,
|
|
122
|
+
events: stepEvents,
|
|
123
|
+
actionErrors: errors,
|
|
124
|
+
actions,
|
|
125
|
+
rewardEnabled: true,
|
|
126
|
+
});
|
|
124
127
|
episodeState.lastResult = result;
|
|
125
128
|
emitExternalRenderFrame(host, result, { source: 'step', actions });
|
|
126
129
|
host.afterStep(result);
|
|
@@ -150,8 +153,9 @@ export function createEnvironmentRuntime(host) {
|
|
|
150
153
|
Object.keys(normalizedPlacements).forEach((driverId) => {
|
|
151
154
|
sim.clearCarControls?.(driverId);
|
|
152
155
|
sim.setAutomaticPitIntentEnabled?.(driverId, false);
|
|
153
|
-
sim.setPitIntent?.(driverId, 0);
|
|
154
156
|
const car = sim.cars?.find?.((item) => item.id === driverId);
|
|
157
|
+
resetEnvironmentPitStop(car);
|
|
158
|
+
sim.setPitIntent?.(driverId, 0);
|
|
155
159
|
if (car) sim.applyRunoffResponse?.(car);
|
|
156
160
|
});
|
|
157
161
|
resetDriverEpisodes(episodeState, Object.keys(normalizedPlacements));
|
|
@@ -208,6 +212,7 @@ function mergePlainObjects(base, overrides, path = []) {
|
|
|
208
212
|
if (!isPlainObject(base) || !isPlainObject(overrides)) return overrides;
|
|
209
213
|
const merged = { ...base };
|
|
210
214
|
Object.entries(overrides).forEach(([key, value]) => {
|
|
215
|
+
if (value === undefined) return;
|
|
211
216
|
const nextPath = [...path, key];
|
|
212
217
|
merged[key] = isPlainObject(value) &&
|
|
213
218
|
isPlainObject(base[key]) &&
|
|
@@ -235,6 +240,10 @@ function isNoopPitIntent(pitIntent) {
|
|
|
235
240
|
function initializeControlledPitIntent(host) {
|
|
236
241
|
const sim = host.getSimulation?.();
|
|
237
242
|
const options = host.getOptions?.();
|
|
243
|
+
initializeControlledPitIntentForSimulation(sim, options);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function initializeControlledPitIntentForSimulation(sim, options) {
|
|
238
247
|
options?.controlledDrivers?.forEach?.((driverId) => {
|
|
239
248
|
sim?.setAutomaticPitIntentEnabled?.(driverId, false);
|
|
240
249
|
sim?.setPitIntent?.(driverId, 0);
|
|
@@ -243,6 +252,24 @@ function initializeControlledPitIntent(host) {
|
|
|
243
252
|
});
|
|
244
253
|
}
|
|
245
254
|
|
|
255
|
+
function resetEnvironmentPitStop(car) {
|
|
256
|
+
const stop = car?.pitStop;
|
|
257
|
+
if (!stop) return;
|
|
258
|
+
stop.status = 'pending';
|
|
259
|
+
stop.phase = null;
|
|
260
|
+
stop.intent = 0;
|
|
261
|
+
stop.queueingForService = false;
|
|
262
|
+
stop.route = null;
|
|
263
|
+
stop.routeProgress = 0;
|
|
264
|
+
stop.routeStartRaceDistance = null;
|
|
265
|
+
stop.routeEndRaceDistance = null;
|
|
266
|
+
stop.serviceRemaining = 0;
|
|
267
|
+
stop.penaltyServiceRemaining = 0;
|
|
268
|
+
stop.penaltyServiceTotal = 0;
|
|
269
|
+
stop.servingPenaltyIds = [];
|
|
270
|
+
stop.serviceProfile = null;
|
|
271
|
+
}
|
|
272
|
+
|
|
246
273
|
function buildResult({
|
|
247
274
|
host,
|
|
248
275
|
episodeState,
|
|
@@ -251,6 +278,7 @@ function buildResult({
|
|
|
251
278
|
actions = {},
|
|
252
279
|
controlledDrivers = null,
|
|
253
280
|
stateOutput = null,
|
|
281
|
+
rewardEnabled = false,
|
|
254
282
|
}) {
|
|
255
283
|
const options = host.getOptions();
|
|
256
284
|
const sim = host.getSimulation();
|
|
@@ -276,18 +304,23 @@ function buildResult({
|
|
|
276
304
|
...options,
|
|
277
305
|
controlledDrivers: resultDrivers,
|
|
278
306
|
}, episode);
|
|
279
|
-
const
|
|
307
|
+
const driverEpisode = resultDrivers === options.controlledDrivers
|
|
308
|
+
? episode
|
|
309
|
+
: evaluateEpisode(observationSnapshot, options, episodeState, options.controlledDrivers);
|
|
310
|
+
const driverEpisodeInfo = buildDriverEpisodeInfo(episodeState, options, driverEpisode);
|
|
280
311
|
const { state, rewardSnapshot } = buildResultState(sim, observationSnapshot, resolvedStateOutput);
|
|
281
|
-
const reward =
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
312
|
+
const reward = rewardEnabled
|
|
313
|
+
? computeReward({
|
|
314
|
+
options: { ...options, controlledDrivers: resultDrivers },
|
|
315
|
+
observation,
|
|
316
|
+
events,
|
|
317
|
+
snapshot: rewardSnapshot,
|
|
318
|
+
actions,
|
|
319
|
+
previousSnapshot: episodeState.previousSnapshot,
|
|
320
|
+
metrics,
|
|
321
|
+
driverEpisodeInfo: rewardEpisodeInfo,
|
|
322
|
+
})
|
|
323
|
+
: null;
|
|
291
324
|
episodeState.lastObservationSnapshot = observationSnapshot;
|
|
292
325
|
episodeState.lastRewardSnapshot = rewardSnapshot;
|
|
293
326
|
return {
|
|
@@ -304,7 +337,7 @@ function buildResult({
|
|
|
304
337
|
elapsedSeconds: observationSnapshot.time,
|
|
305
338
|
seed: options.seed,
|
|
306
339
|
trackSeed: options.trackSeed,
|
|
307
|
-
controlledDrivers: [...
|
|
340
|
+
controlledDrivers: [...resultDrivers],
|
|
308
341
|
actionErrors,
|
|
309
342
|
endReason: episode.endReason,
|
|
310
343
|
drivers: driverEpisodeInfo,
|
|
@@ -312,6 +345,13 @@ function buildResult({
|
|
|
312
345
|
};
|
|
313
346
|
}
|
|
314
347
|
|
|
348
|
+
function applyControlledRunoffResponse(sim, options) {
|
|
349
|
+
options?.controlledDrivers?.forEach?.((driverId) => {
|
|
350
|
+
const car = sim?.cars?.find?.((item) => item.id === driverId);
|
|
351
|
+
if (car) sim?.applyRunoffResponse?.(car);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
315
355
|
function buildResultState(sim, observationSnapshot, stateOutput) {
|
|
316
356
|
if (stateOutput === 'none') return { state: null, rewardSnapshot: observationSnapshot };
|
|
317
357
|
if (stateOutput === 'minimal') return { state: { snapshot: observationSnapshot }, rewardSnapshot: observationSnapshot };
|