@inventure71/paddockjs 1.0.0 → 2.0.1
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 +58 -0
- package/INSTALL_AND_UPDATE.md +4 -3
- package/README.md +219 -12
- package/docs/architecture.md +126 -45
- package/docs/concepts.md +9 -5
- package/docs/custom_model_controller.md +329 -0
- package/docs/data_contract.md +317 -22
- package/docs/data_model.md +1 -1
- package/docs/index.md +10 -0
- package/docs/learnings.md +22 -0
- package/docs/rules.md +71 -14
- package/docs/sense_contract.md +59 -0
- package/docs/system_specs.md +75 -13
- package/docs/training.md +280 -32
- 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 +6 -33
- package/src/app/BrowserExpertAdapter.js +76 -9
- package/src/app/BrowserExpertExternalRenderer.js +303 -0
- package/src/app/F1SimulatorApp.js +291 -1630
- 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/config/restartOptions.js +34 -0
- package/src/environment/actions.js +13 -5
- package/src/environment/controllerLoop.js +261 -0
- package/src/environment/episode.js +98 -2
- package/src/environment/evaluation.js +54 -32
- package/src/environment/index.d.ts +376 -10
- package/src/environment/index.js +1 -0
- package/src/environment/metrics.js +159 -0
- package/src/environment/observationOptions.js +22 -1
- package/src/environment/observationVector.js +200 -0
- package/src/environment/observations.js +199 -141
- package/src/environment/options.js +37 -5
- package/src/environment/recorder.js +13 -6
- package/src/environment/rewards.js +13 -2
- package/src/environment/runtime.js +272 -23
- package/src/environment/scenarios.js +57 -9
- 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 +75 -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 +66 -0
- package/src/environment/sensors/surfaceRays.js +360 -0
- package/src/environment/sensors/trackRays.js +424 -0
- package/src/environment/sensors.js +1 -367
- package/src/environment/specs.js +159 -26
- package/src/environment/workerProtocol.js +13 -1
- package/src/index.d.ts +171 -6
- package/src/index.js +6 -32
- 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 +239 -0
- package/src/simulation/pit/pitIntent.js +92 -0
- package/src/simulation/pit/pitKinematics.js +24 -0
- package/src/simulation/pit/pitOccupancy.js +47 -0
- package/src/simulation/pit/pitPenaltyService.js +71 -0
- package/src/simulation/pit/pitQueue.js +60 -0
- package/src/simulation/pit/pitRouteMovement.js +167 -0
- package/src/simulation/pit/pitRouting.js +319 -0
- package/src/simulation/pit/pitService.js +43 -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 +94 -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 +67 -0
- package/src/simulation/race/raceOrder.js +113 -0
- package/src/simulation/race/raceProgress.js +129 -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 +92 -0
- package/src/simulation/race/stalledDnf.js +77 -0
- package/src/simulation/raceSimulation.js +231 -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 +144 -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 +193 -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/trackQueryGrid.js +94 -0
- package/src/simulation/track/trackQueryIndex.js +659 -0
- 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/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 +181 -0
- package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
- package/src/simulation/vehicle/pitWheelSurface.js +69 -0
- package/src/simulation/vehicle/runoffResponse.js +87 -0
- package/src/simulation/vehicle/surfacePriority.js +28 -0
- package/src/simulation/vehicle/vehicleGeometry.js +247 -0
- package/src/simulation/vehicle/vehicleKinematics.js +37 -0
- package/src/simulation/vehicle/vehiclePhysics.js +364 -0
- package/src/simulation/vehicle/vehicleSnapshots.js +327 -0
- package/src/simulation/vehicle/vehicleState.js +220 -0
- package/src/simulation/vehicle/wheelSurface.js +152 -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
|
@@ -1,27 +1,74 @@
|
|
|
1
|
-
import { metersToSimUnits,
|
|
1
|
+
import { metersToSimUnits, simUnitsToMeters } from '../simulation/units.js';
|
|
2
2
|
import { normalizeAngle } from '../simulation/simMath.js';
|
|
3
|
-
import { pointAt } from '../simulation/trackModel.js';
|
|
3
|
+
import { pointAt } from '../simulation/track/trackModel.js';
|
|
4
4
|
import { normalizeLookaheadMeters } from './observationOptions.js';
|
|
5
|
-
import {
|
|
5
|
+
import { buildBodySenses } from './sensors/bodySenses.js';
|
|
6
|
+
import { buildBoundarySenses } from './sensors/boundarySenses.js';
|
|
7
|
+
import { buildContactPatchSenses } from './sensors/contactSenses.js';
|
|
8
|
+
import { enrichOpponentRadar } from './sensors/opponentRadar.js';
|
|
9
|
+
import { buildNearbyCars, buildRaySensors, createRayBatchContext, normalizeRayOptions } from './sensors.js';
|
|
10
|
+
import { buildObservationVector } from './observationVector.js';
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
const EMPTY_EVENTS = Object.freeze([]);
|
|
13
|
+
|
|
14
|
+
export function buildEnvironmentObservation({ snapshot, options, events = [], controlledDrivers = options.controlledDrivers }) {
|
|
8
15
|
const carsById = new Map(snapshot.cars.map((entry) => [entry.id, entry]));
|
|
9
|
-
const eventsByDriver = groupEventsByDriver(events,
|
|
10
|
-
|
|
16
|
+
const eventsByDriver = groupEventsByDriver(events, controlledDrivers);
|
|
17
|
+
const defaultSensors = defaultSensorOptions(options);
|
|
18
|
+
const hasSensorOverrides = Object.keys(options.sensorsByDriver ?? {}).length > 0;
|
|
19
|
+
let rayBatchContext = null;
|
|
20
|
+
const getRayBatchContext = () => {
|
|
21
|
+
rayBatchContext ??= createRayBatchContext(snapshot);
|
|
22
|
+
return rayBatchContext;
|
|
23
|
+
};
|
|
24
|
+
const observations = {};
|
|
25
|
+
controlledDrivers.forEach((driverId) => {
|
|
11
26
|
const car = carsById.get(driverId);
|
|
12
|
-
if (!car)
|
|
27
|
+
if (!car) {
|
|
28
|
+
observations[driverId] = formatObservation(emptyObservation(driverId), options.observation);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const driverEvents = eventsByDriver?.get(driverId) ?? EMPTY_EVENTS;
|
|
33
|
+
const sensors = effectiveSensorOptions(options, car.id, defaultSensors, hasSensorOverrides);
|
|
34
|
+
const output = options.observation?.output ?? 'full';
|
|
35
|
+
const includeSchema = options.observation?.includeSchema !== false;
|
|
36
|
+
const wantsObject = output === 'full' || output === 'object';
|
|
37
|
+
const wantsVector = output === 'full' || output === 'vector';
|
|
38
|
+
const object = wantsObject
|
|
39
|
+
? buildDriverObservationObject(car, snapshot, options, driverEvents, sensors, getRayBatchContext)
|
|
40
|
+
: null;
|
|
41
|
+
const { vector, schema } = wantsVector
|
|
42
|
+
? object
|
|
43
|
+
? buildDriverVector(object, sensors, {
|
|
44
|
+
includeSchema,
|
|
45
|
+
vectorType: options.observation?.vectorType,
|
|
46
|
+
})
|
|
47
|
+
: includeSchema
|
|
48
|
+
? buildDriverVector(buildDriverObservationObject(car, snapshot, options, driverEvents, sensors, getRayBatchContext), sensors, {
|
|
49
|
+
includeSchema,
|
|
50
|
+
vectorType: options.observation?.vectorType,
|
|
51
|
+
})
|
|
52
|
+
: buildDriverVectorDirect(car, snapshot, options, driverEvents, sensors, getRayBatchContext)
|
|
53
|
+
: { vector: undefined, schema: [] };
|
|
54
|
+
observations[driverId] = formatObservation({ object, vector, schema, events: driverEvents }, options.observation);
|
|
55
|
+
});
|
|
56
|
+
return observations;
|
|
57
|
+
}
|
|
13
58
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
59
|
+
function formatObservation(observation, options = {}) {
|
|
60
|
+
const output = options.output ?? 'full';
|
|
61
|
+
const includeSchema = options.includeSchema !== false;
|
|
62
|
+
const formatted = { events: observation.events ?? [] };
|
|
63
|
+
if ((output === 'full' || output === 'object') && observation.object) formatted.object = observation.object;
|
|
64
|
+
if ((output === 'full' || output === 'vector') && observation.vector) formatted.vector = observation.vector;
|
|
65
|
+
if (includeSchema) formatted.schema = observation.schema;
|
|
66
|
+
return formatted;
|
|
20
67
|
}
|
|
21
68
|
|
|
22
69
|
function groupEventsByDriver(events, controlledDrivers) {
|
|
70
|
+
if (!events.length) return null;
|
|
23
71
|
const byDriver = new Map(controlledDrivers.map((driverId) => [driverId, []]));
|
|
24
|
-
if (!events.length) return byDriver;
|
|
25
72
|
const controlledSet = new Set(controlledDrivers);
|
|
26
73
|
events.forEach((event) => {
|
|
27
74
|
const driverIds = new Set([
|
|
@@ -37,22 +84,28 @@ function groupEventsByDriver(events, controlledDrivers) {
|
|
|
37
84
|
return byDriver;
|
|
38
85
|
}
|
|
39
86
|
|
|
40
|
-
function buildDriverObservationObject(car, snapshot, options, events, sensors) {
|
|
87
|
+
function buildDriverObservationObject(car, snapshot, options, events, sensors, getRayBatchContext = null) {
|
|
88
|
+
const sensorCar = withEnvironmentControlFlag(car, options);
|
|
89
|
+
const onTrack = isCarLegallyOnTrack(car);
|
|
90
|
+
const body = buildBodySenses(car);
|
|
91
|
+
const trackHeadingError = car.trackHeadingError ?? estimateTrackHeadingError(car, snapshot);
|
|
92
|
+
const trackRelation = {
|
|
93
|
+
...buildBoundarySenses(car, snapshot, onTrack),
|
|
94
|
+
headingErrorRadians: trackHeadingError,
|
|
95
|
+
};
|
|
96
|
+
const nearbyCars = sensors.nearbyCars.enabled
|
|
97
|
+
? enrichOpponentRadar(car, buildNearbyCars(car, snapshot, sensors.nearbyCars), snapshot)
|
|
98
|
+
: [];
|
|
41
99
|
return {
|
|
100
|
+
profile: options.observation?.profile ?? 'default',
|
|
42
101
|
self: {
|
|
43
|
-
|
|
44
|
-
speedKph: car.speedKph,
|
|
45
|
-
speedMetersPerSecond: simSpeedToMetersPerSecond(car.speed ?? 0),
|
|
46
|
-
headingRadians: car.heading,
|
|
47
|
-
steeringAngleRadians: car.steeringAngle ?? 0,
|
|
48
|
-
throttle: car.throttle ?? 0,
|
|
49
|
-
brake: car.brake ?? 0,
|
|
102
|
+
...body,
|
|
50
103
|
lap: car.lap,
|
|
51
104
|
completedLaps: car.lapTelemetry?.completedLaps ?? 0,
|
|
52
105
|
lapProgressMeters: simUnitsToMeters(car.progress ?? 0),
|
|
53
|
-
trackOffsetMeters:
|
|
54
|
-
trackHeadingErrorRadians:
|
|
55
|
-
onTrack
|
|
106
|
+
trackOffsetMeters: trackRelation.lateralOffsetMeters,
|
|
107
|
+
trackHeadingErrorRadians: trackHeadingError,
|
|
108
|
+
onTrack,
|
|
56
109
|
surface: car.surface ?? 'track',
|
|
57
110
|
inPitLane: Boolean(car.inPitLane),
|
|
58
111
|
pitLanePart: car.pitLanePart ?? null,
|
|
@@ -66,6 +119,8 @@ function buildDriverObservationObject(car, snapshot, options, events, sensors) {
|
|
|
66
119
|
pitStopPenaltyServiceRemainingSeconds: car.pitStop?.penaltyServiceRemainingSeconds ?? null,
|
|
67
120
|
pitStopsCompleted: car.pitStop?.stopsCompleted ?? 0,
|
|
68
121
|
},
|
|
122
|
+
trackRelation,
|
|
123
|
+
contactPatches: buildContactPatchSenses(car),
|
|
69
124
|
race: {
|
|
70
125
|
position: car.rank,
|
|
71
126
|
totalCars: snapshot.cars.length,
|
|
@@ -80,8 +135,8 @@ function buildDriverObservationObject(car, snapshot, options, events, sensors) {
|
|
|
80
135
|
curvature: car.trackState?.curvature ?? pointAt(snapshot.track, car.progress ?? 0).curvature ?? 0,
|
|
81
136
|
lookahead: buildTrackLookahead(car, snapshot, options),
|
|
82
137
|
},
|
|
83
|
-
rays: sensors.rays.enabled ? buildRaySensors(
|
|
84
|
-
nearbyCars
|
|
138
|
+
rays: sensors.rays.enabled ? buildRaySensors(sensorCar, snapshot, sensors.rays, rayBatchContextForSensors(sensors, getRayBatchContext)) : [],
|
|
139
|
+
nearbyCars,
|
|
85
140
|
events,
|
|
86
141
|
};
|
|
87
142
|
}
|
|
@@ -92,7 +147,10 @@ function estimateTrackHeadingError(car, snapshot) {
|
|
|
92
147
|
}
|
|
93
148
|
|
|
94
149
|
function buildTrackLookahead(car, snapshot, options) {
|
|
95
|
-
const distances =
|
|
150
|
+
const distances = Array.isArray(options.observation?.lookaheadMeters)
|
|
151
|
+
? options.observation.lookaheadMeters
|
|
152
|
+
: normalizeLookaheadMeters(options.observation?.lookaheadMeters);
|
|
153
|
+
if (!distances.length) return [];
|
|
96
154
|
const base = pointAt(snapshot.track, car.progress ?? 0);
|
|
97
155
|
return distances.map((distanceMeters) => {
|
|
98
156
|
const sample = pointAt(snapshot.track, (car.progress ?? 0) + metersToSimUnits(distanceMeters));
|
|
@@ -112,128 +170,81 @@ function isCarLegallyOnTrack(car) {
|
|
|
112
170
|
return ['track', 'kerb', 'pit-entry', 'pit-lane', 'pit-exit', 'pit-box'].includes(car.surface ?? 'track');
|
|
113
171
|
}
|
|
114
172
|
|
|
115
|
-
function buildDriverVector(object, sensors) {
|
|
116
|
-
|
|
117
|
-
{ name: 'self.speedKph', unit: 'kph', scale: 'fixed:400' },
|
|
118
|
-
{ name: 'self.speedMetersPerSecond', unit: 'm/s', scale: 'fixed:120' },
|
|
119
|
-
{ name: 'self.steeringAngleRadians', unit: 'rad', scale: 'fixed:pi' },
|
|
120
|
-
{ name: 'self.throttle', scale: '0..1' },
|
|
121
|
-
{ name: 'self.brake', scale: '0..1' },
|
|
122
|
-
{ name: 'self.lapProgressRatio', scale: '0..1' },
|
|
123
|
-
{ name: 'self.trackOffsetMeters', unit: 'm', scale: 'fixed:meters' },
|
|
124
|
-
{ name: 'self.trackHeadingErrorRadians', unit: 'rad', scale: 'fixed:pi' },
|
|
125
|
-
{ name: 'self.onTrack', scale: 'boolean' },
|
|
126
|
-
{ name: 'self.inPitLane', scale: 'boolean' },
|
|
127
|
-
{ name: 'self.tireEnergy', scale: '0..100' },
|
|
128
|
-
{ name: 'self.pitIntent', scale: '0..2' },
|
|
129
|
-
{ name: 'self.pitStopActive', scale: 'boolean' },
|
|
130
|
-
{ name: 'race.positionNormalized', scale: '0..1' },
|
|
131
|
-
{ name: 'race.raceModeGreen', scale: 'boolean' },
|
|
132
|
-
{ name: 'race.raceModeSafetyCar', scale: 'boolean' },
|
|
133
|
-
{ name: 'race.redFlag', scale: 'boolean' },
|
|
134
|
-
{ name: 'race.pitLaneOpen', scale: 'boolean' },
|
|
135
|
-
{ name: 'track.curvature', scale: 'track-curvature' },
|
|
136
|
-
];
|
|
137
|
-
const vector = [
|
|
138
|
-
object.self.speedKph / 400,
|
|
139
|
-
object.self.speedMetersPerSecond / 120,
|
|
140
|
-
object.self.steeringAngleRadians / Math.PI,
|
|
141
|
-
object.self.throttle,
|
|
142
|
-
object.self.brake,
|
|
143
|
-
normalizeLapProgress(object),
|
|
144
|
-
object.self.trackOffsetMeters,
|
|
145
|
-
object.self.trackHeadingErrorRadians / Math.PI,
|
|
146
|
-
object.self.onTrack ? 1 : 0,
|
|
147
|
-
object.self.inPitLane ? 1 : 0,
|
|
148
|
-
(object.self.tireEnergy ?? 0) / 100,
|
|
149
|
-
(object.self.pitIntent ?? 0) / 2,
|
|
150
|
-
object.self.pitStopStatus && object.self.pitStopStatus !== 'pending' && object.self.pitStopStatus !== 'completed' ? 1 : 0,
|
|
151
|
-
normalizeRacePosition(object),
|
|
152
|
-
object.race.raceMode === 'green' ? 1 : 0,
|
|
153
|
-
object.race.raceMode === 'safety-car' ? 1 : 0,
|
|
154
|
-
object.race.redFlag ? 1 : 0,
|
|
155
|
-
object.race.pitLaneOpen ? 1 : 0,
|
|
156
|
-
object.track.curvature ?? 0,
|
|
157
|
-
];
|
|
158
|
-
object.track.lookahead.forEach((sample, index) => {
|
|
159
|
-
schema.push(
|
|
160
|
-
{ name: `track.lookahead[${index}].curvature`, scale: 'track-curvature' },
|
|
161
|
-
{ name: `track.lookahead[${index}].headingDeltaRadians`, unit: 'rad', scale: 'fixed:pi' },
|
|
162
|
-
);
|
|
163
|
-
vector.push(sample.curvature ?? 0, (sample.headingDeltaRadians ?? 0) / Math.PI);
|
|
164
|
-
});
|
|
165
|
-
object.rays.forEach((ray, index) => {
|
|
166
|
-
schema.push(
|
|
167
|
-
{ name: `rays[${index}].track.distanceRatio`, scale: '0..1' },
|
|
168
|
-
{ name: `rays[${index}].track.hit`, scale: 'boolean' },
|
|
169
|
-
{ name: `rays[${index}].track.kindExit`, scale: 'boolean' },
|
|
170
|
-
{ name: `rays[${index}].track.kindEntry`, scale: 'boolean' },
|
|
171
|
-
{ name: `rays[${index}].car.distanceRatio`, scale: '0..1' },
|
|
172
|
-
{ name: `rays[${index}].car.hit`, scale: 'boolean' },
|
|
173
|
-
{ name: `rays[${index}].car.relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
|
|
174
|
-
);
|
|
175
|
-
vector.push(
|
|
176
|
-
ratio(ray.track.distanceMeters, ray.lengthMeters),
|
|
177
|
-
ray.track.hit ? 1 : 0,
|
|
178
|
-
ray.track.kind === 'exit' ? 1 : 0,
|
|
179
|
-
ray.track.kind === 'entry' ? 1 : 0,
|
|
180
|
-
ratio(ray.car.distanceMeters, ray.lengthMeters),
|
|
181
|
-
ray.car.hit ? 1 : 0,
|
|
182
|
-
ray.car.relativeSpeedKph / 200,
|
|
183
|
-
);
|
|
184
|
-
});
|
|
185
|
-
const nearbyLimit = sensors.nearbyCars.enabled ? (sensors.nearbyCars.maxCars ?? object.nearbyCars.length) : 0;
|
|
186
|
-
const nearbyRadius = sensors.nearbyCars.radiusMeters ?? 150;
|
|
187
|
-
for (let index = 0; index < nearbyLimit; index += 1) {
|
|
188
|
-
const nearby = object.nearbyCars[index] ?? null;
|
|
189
|
-
schema.push(
|
|
190
|
-
{ name: `nearbyCars[${index}].present`, scale: 'boolean' },
|
|
191
|
-
{ name: `nearbyCars[${index}].relativeForwardRatio`, scale: '-1..1' },
|
|
192
|
-
{ name: `nearbyCars[${index}].relativeRightRatio`, scale: '-1..1' },
|
|
193
|
-
{ name: `nearbyCars[${index}].relativeDistanceRatio`, scale: '0..1' },
|
|
194
|
-
{ name: `nearbyCars[${index}].relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
|
|
195
|
-
{ name: `nearbyCars[${index}].relativeHeadingRadians`, unit: 'rad', scale: 'fixed:pi' },
|
|
196
|
-
{ name: `nearbyCars[${index}].ahead`, scale: 'boolean' },
|
|
197
|
-
{ name: `nearbyCars[${index}].sameLap`, scale: 'boolean' },
|
|
198
|
-
);
|
|
199
|
-
vector.push(
|
|
200
|
-
nearby ? 1 : 0,
|
|
201
|
-
clampRatio((nearby?.relativeForwardMeters ?? 0) / nearbyRadius),
|
|
202
|
-
clampRatio((nearby?.relativeRightMeters ?? 0) / nearbyRadius),
|
|
203
|
-
ratio(nearby?.relativeDistanceMeters ?? nearbyRadius, nearbyRadius),
|
|
204
|
-
(nearby?.relativeSpeedKph ?? 0) / 200,
|
|
205
|
-
(nearby?.relativeHeadingRadians ?? 0) / Math.PI,
|
|
206
|
-
nearby?.ahead ? 1 : 0,
|
|
207
|
-
nearby?.sameLap ? 1 : 0,
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
return { vector, schema };
|
|
173
|
+
function buildDriverVector(object, sensors, { includeSchema = true, vectorType = 'array' } = {}) {
|
|
174
|
+
return buildObservationVector(object, sensors, { includeSchema, vectorType });
|
|
211
175
|
}
|
|
212
176
|
|
|
213
|
-
function
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
177
|
+
function buildDriverVectorDirect(car, snapshot, options, events, sensors, getRayBatchContext = null) {
|
|
178
|
+
const profile = options.observation?.profile ?? 'default';
|
|
179
|
+
const includePhysicalDriverSenses = profile === 'physical-driver';
|
|
180
|
+
const onTrack = isCarLegallyOnTrack(car);
|
|
181
|
+
const body = buildBodySenses(car);
|
|
182
|
+
const trackHeadingError = car.trackHeadingError ?? estimateTrackHeadingError(car, snapshot);
|
|
183
|
+
const sensorCar = withEnvironmentControlFlag(car, options);
|
|
184
|
+
const trackRelation = {
|
|
185
|
+
...buildBoundarySenses(car, snapshot, onTrack),
|
|
186
|
+
headingErrorRadians: trackHeadingError,
|
|
187
|
+
};
|
|
188
|
+
const contactPatches = includePhysicalDriverSenses ? buildContactPatchSenses(car) : [];
|
|
189
|
+
const rays = sensors.rays.enabled ? buildRaySensors(sensorCar, snapshot, sensors.rays, rayBatchContextForSensors(sensors, getRayBatchContext)) : [];
|
|
190
|
+
const nearbyCars = sensors.nearbyCars.enabled
|
|
191
|
+
? enrichOpponentRadar(car, buildNearbyCars(car, snapshot, sensors.nearbyCars), snapshot)
|
|
192
|
+
: [];
|
|
193
|
+
const trackLengthMeters = simUnitsToMeters(snapshot.track.length ?? 0);
|
|
194
|
+
const trackCurvature = car.trackState?.curvature ?? pointAt(snapshot.track, car.progress ?? 0).curvature ?? 0;
|
|
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,
|
|
228
|
+
});
|
|
220
229
|
}
|
|
221
230
|
|
|
222
|
-
function
|
|
223
|
-
|
|
224
|
-
return
|
|
231
|
+
function rayBatchContextForSensors(sensors, getRayBatchContext) {
|
|
232
|
+
if (!getRayBatchContext || !sensors.rays?.channels?.includes('car')) return null;
|
|
233
|
+
return getRayBatchContext();
|
|
225
234
|
}
|
|
226
235
|
|
|
227
|
-
function
|
|
228
|
-
return
|
|
236
|
+
export function defaultSensorOptions(options) {
|
|
237
|
+
return {
|
|
238
|
+
rays: normalizeRayOptions(options.sensors.rays),
|
|
239
|
+
nearbyCars: options.sensors.nearbyCars,
|
|
240
|
+
};
|
|
229
241
|
}
|
|
230
242
|
|
|
231
|
-
function effectiveSensorOptions(options, driverId) {
|
|
243
|
+
export function effectiveSensorOptions(options, driverId, defaultSensors = defaultSensorOptions(options), hasSensorOverrides = true) {
|
|
244
|
+
if (!hasSensorOverrides || !options.sensorsByDriver?.[driverId]) return defaultSensors;
|
|
245
|
+
const rayOverrides = options.sensorsByDriver?.[driverId]?.rays ?? {};
|
|
232
246
|
return {
|
|
233
|
-
rays:
|
|
234
|
-
...options.sensors.rays,
|
|
235
|
-
...(options.sensorsByDriver?.[driverId]?.rays ?? {}),
|
|
236
|
-
},
|
|
247
|
+
rays: normalizeRayOptions(mergeDriverRayOptions(options.sensors.rays, rayOverrides)),
|
|
237
248
|
nearbyCars: {
|
|
238
249
|
...options.sensors.nearbyCars,
|
|
239
250
|
...(options.sensorsByDriver?.[driverId]?.nearbyCars ?? {}),
|
|
@@ -241,6 +252,25 @@ function effectiveSensorOptions(options, driverId) {
|
|
|
241
252
|
};
|
|
242
253
|
}
|
|
243
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
|
+
|
|
244
274
|
function emptyObservation(driverId) {
|
|
245
275
|
return {
|
|
246
276
|
object: {
|
|
@@ -249,9 +279,18 @@ function emptyObservation(driverId) {
|
|
|
249
279
|
speedKph: 0,
|
|
250
280
|
speedMetersPerSecond: 0,
|
|
251
281
|
headingRadians: 0,
|
|
282
|
+
yawRateRadiansPerSecond: 0,
|
|
252
283
|
steeringAngleRadians: 0,
|
|
253
284
|
throttle: 0,
|
|
254
285
|
brake: 0,
|
|
286
|
+
lateralG: 0,
|
|
287
|
+
longitudinalG: 0,
|
|
288
|
+
gripUsage: 0,
|
|
289
|
+
slipAngleRadians: 0,
|
|
290
|
+
tractionLimited: false,
|
|
291
|
+
stabilityState: 'stable',
|
|
292
|
+
destroyed: false,
|
|
293
|
+
destroyReason: null,
|
|
255
294
|
lap: 0,
|
|
256
295
|
completedLaps: 0,
|
|
257
296
|
lapProgressMeters: 0,
|
|
@@ -271,6 +310,16 @@ function emptyObservation(driverId) {
|
|
|
271
310
|
pitStopPenaltyServiceRemainingSeconds: null,
|
|
272
311
|
pitStopsCompleted: 0,
|
|
273
312
|
},
|
|
313
|
+
trackRelation: {
|
|
314
|
+
lateralOffsetMeters: 0,
|
|
315
|
+
headingErrorRadians: 0,
|
|
316
|
+
legalWidthMeters: 0,
|
|
317
|
+
leftBoundaryMeters: 0,
|
|
318
|
+
rightBoundaryMeters: 0,
|
|
319
|
+
onLegalSurface: false,
|
|
320
|
+
surface: 'missing',
|
|
321
|
+
},
|
|
322
|
+
contactPatches: [],
|
|
274
323
|
race: { position: 0, totalCars: 0, raceMode: 'missing', pitLaneOpen: false, redFlag: false, totalLaps: 0 },
|
|
275
324
|
track: { lengthMeters: 0, widthMeters: 0, curvature: 0, lookahead: [] },
|
|
276
325
|
rays: [],
|
|
@@ -282,3 +331,12 @@ function emptyObservation(driverId) {
|
|
|
282
331
|
events: [],
|
|
283
332
|
};
|
|
284
333
|
}
|
|
334
|
+
|
|
335
|
+
function withEnvironmentControlFlag(car, options) {
|
|
336
|
+
if (!car || car.environmentControlled) return car;
|
|
337
|
+
if (!Array.isArray(options?.controlledDrivers) || !options.controlledDrivers.includes(car.id)) return car;
|
|
338
|
+
return {
|
|
339
|
+
...car,
|
|
340
|
+
environmentControlled: true,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { CHAMPIONSHIP_ENTRY_BLUEPRINTS } from '../data/championship.js';
|
|
2
2
|
import { normalizeSimulatorDrivers } from '../data/normalizeDrivers.js';
|
|
3
|
-
import {
|
|
3
|
+
import { normalizePhysicsMode } from '../simulation/vehicle/vehiclePhysics.js';
|
|
4
|
+
import { normalizeWarmupOptions } from '../simulation/warmup/runtimeWarmup.js';
|
|
5
|
+
import { normalizeObservationOptions } from './observationOptions.js';
|
|
4
6
|
import { resolveScenarioPlacementConfig } from './scenarios.js';
|
|
7
|
+
import { normalizeRayOptions } from './sensors.js';
|
|
5
8
|
|
|
6
9
|
const DEFAULT_SEED = 1971;
|
|
7
10
|
const DEFAULT_FRAME_SKIP = 1;
|
|
8
11
|
const DEFAULT_MAX_STEPS = 10000;
|
|
12
|
+
const RESULT_STATE_OUTPUTS = new Set(['full', 'minimal', 'none']);
|
|
13
|
+
const RESET_OBSERVATION_SCOPES = new Set(['all', 'reset']);
|
|
9
14
|
|
|
10
15
|
const DEFAULT_SENSOR_OPTIONS = {
|
|
11
16
|
rays: {
|
|
@@ -37,6 +42,7 @@ export function resolveEnvironmentOptions(options = {}) {
|
|
|
37
42
|
...options,
|
|
38
43
|
seed,
|
|
39
44
|
trackSeed,
|
|
45
|
+
trackGeneration: options.trackGeneration ?? {},
|
|
40
46
|
totalLaps: options.totalLaps,
|
|
41
47
|
drivers: normalizedDrivers,
|
|
42
48
|
};
|
|
@@ -60,16 +66,19 @@ export function resolveEnvironmentOptions(options = {}) {
|
|
|
60
66
|
}
|
|
61
67
|
});
|
|
62
68
|
|
|
69
|
+
const externalRenderer = normalizeExternalRenderer(options.externalRenderer);
|
|
70
|
+
|
|
63
71
|
return {
|
|
64
72
|
...resolved,
|
|
73
|
+
physicsMode: normalizePhysicsMode(options.physicsMode),
|
|
74
|
+
warmup: normalizeWarmupOptions(options.warmup, 'environment'),
|
|
65
75
|
drivers: participantDrivers,
|
|
66
76
|
controlledDrivers,
|
|
67
77
|
frameSkip: normalizePositiveInteger(options.frameSkip, DEFAULT_FRAME_SKIP, 'frameSkip'),
|
|
68
78
|
actionPolicy: options.actionPolicy === 'report' ? 'report' : 'strict',
|
|
69
79
|
scenario,
|
|
70
80
|
observation: {
|
|
71
|
-
...(options.observation ?? {}),
|
|
72
|
-
lookaheadMeters: normalizeLookaheadMeters(options.observation?.lookaheadMeters),
|
|
81
|
+
...normalizeObservationOptions(options.observation ?? {}),
|
|
73
82
|
},
|
|
74
83
|
sensors: mergeSensorOptions(options.sensors),
|
|
75
84
|
sensorsByDriver: options.sensorsByDriver ?? {},
|
|
@@ -77,7 +86,21 @@ export function resolveEnvironmentOptions(options = {}) {
|
|
|
77
86
|
maxSteps: normalizePositiveInteger(options.episode?.maxSteps, DEFAULT_MAX_STEPS, 'episode.maxSteps'),
|
|
78
87
|
endOnRaceFinish: options.episode?.endOnRaceFinish !== false,
|
|
79
88
|
},
|
|
89
|
+
result: normalizeResultOptions(options.result),
|
|
80
90
|
reward: typeof options.reward === 'function' ? options.reward : null,
|
|
91
|
+
externalRenderer,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function normalizeResultOptions(result = {}) {
|
|
96
|
+
const stateOutput = RESULT_STATE_OUTPUTS.has(result.stateOutput) ? result.stateOutput : 'full';
|
|
97
|
+
const resetDriversObservationScope = RESET_OBSERVATION_SCOPES.has(result.resetDriversObservationScope)
|
|
98
|
+
? result.resetDriversObservationScope
|
|
99
|
+
: 'all';
|
|
100
|
+
return {
|
|
101
|
+
...result,
|
|
102
|
+
stateOutput,
|
|
103
|
+
resetDriversObservationScope,
|
|
81
104
|
};
|
|
82
105
|
}
|
|
83
106
|
|
|
@@ -145,13 +168,22 @@ function normalizePositiveInteger(value, fallback, label) {
|
|
|
145
168
|
|
|
146
169
|
function mergeSensorOptions(sensors = {}) {
|
|
147
170
|
return {
|
|
148
|
-
rays: {
|
|
171
|
+
rays: normalizeRayOptions({
|
|
149
172
|
...DEFAULT_SENSOR_OPTIONS.rays,
|
|
150
173
|
...(sensors.rays ?? {}),
|
|
151
|
-
},
|
|
174
|
+
}),
|
|
152
175
|
nearbyCars: {
|
|
153
176
|
...DEFAULT_SENSOR_OPTIONS.nearbyCars,
|
|
154
177
|
...(sensors.nearbyCars ?? {}),
|
|
155
178
|
},
|
|
156
179
|
};
|
|
157
180
|
}
|
|
181
|
+
|
|
182
|
+
function normalizeExternalRenderer(value) {
|
|
183
|
+
if (value == null) return null;
|
|
184
|
+
if (typeof value === 'function') return { onFrame: value };
|
|
185
|
+
if (typeof value === 'object' && typeof value.onFrame === 'function') {
|
|
186
|
+
return { onFrame: value.onFrame };
|
|
187
|
+
}
|
|
188
|
+
throw new Error('PaddockJS environment externalRenderer must be a function or an object with onFrame(frame).');
|
|
189
|
+
}
|
|
@@ -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;
|