@inventure71/paddockjs 2.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 +6 -0
- package/INSTALL_AND_UPDATE.md +1 -1
- package/README.md +30 -5
- package/docs/architecture.md +15 -9
- package/docs/concepts.md +6 -4
- package/docs/data_contract.md +28 -15
- package/docs/data_model.md +1 -1
- package/docs/index.md +9 -0
- package/docs/rules.md +37 -7
- package/docs/sense_contract.md +59 -0
- package/docs/system_specs.md +56 -7
- package/docs/training.md +16 -10
- package/package.json +1 -1
- package/src/api/PaddockSimulatorController.js +2 -33
- package/src/app/BrowserExpertAdapter.js +9 -300
- package/src/app/BrowserExpertExternalRenderer.js +303 -0
- package/src/app/F1SimulatorApp.js +1 -42
- package/src/config/restartOptions.js +34 -0
- package/src/environment/controllerLoop.js +17 -2
- package/src/environment/episode.js +14 -2
- package/src/environment/index.d.ts +16 -2
- package/src/environment/observationVector.js +200 -0
- package/src/environment/observations.js +58 -285
- package/src/environment/recorder.js +13 -6
- package/src/environment/rewards.js +13 -2
- package/src/environment/runtime.js +62 -14
- 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 +90 -26
- package/src/environment/sensors/trackRays.js +185 -22
- package/src/environment/specs.js +78 -40
- package/src/index.d.ts +2 -0
- package/src/index.js +1 -32
- 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/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/raceStep.js +1 -1
- 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/track/trackQueryGrid.js +94 -0
- package/src/simulation/track/trackQueryIndex.js +16 -302
- 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/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 +2 -1
- package/src/simulation/vehicle/vehicleSnapshots.js +11 -0
- package/src/simulation/vehicle/vehicleState.js +36 -2
- package/src/simulation/vehicle/wheelSurface.js +3 -1
|
@@ -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: {
|
|
@@ -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;
|
|
@@ -47,7 +47,9 @@ function createSimulationWithEnvironmentScenario(options) {
|
|
|
47
47
|
surface: 'environment',
|
|
48
48
|
execute: ({ warmup }) => {
|
|
49
49
|
const warmupSimulation = createRaceSimulation(disableWarmupOptions(simulationOptions, warmup.surface));
|
|
50
|
+
initializeControlledPitIntentForSimulation(warmupSimulation, options);
|
|
50
51
|
applyEnvironmentScenario(warmupSimulation, options);
|
|
52
|
+
applyControlledRunoffResponse(warmupSimulation, options);
|
|
51
53
|
for (let index = 0; index < warmup.steps; index += 1) warmupSimulation.step(FIXED_STEP);
|
|
52
54
|
const warmupSnapshot = warmupSimulation.snapshotObservation?.() ?? warmupSimulation.snapshot();
|
|
53
55
|
buildEnvironmentObservation({
|
|
@@ -58,7 +60,9 @@ function createSimulationWithEnvironmentScenario(options) {
|
|
|
58
60
|
},
|
|
59
61
|
});
|
|
60
62
|
const sim = createRaceSimulation(disableWarmupOptions(simulationOptions, 'environment'));
|
|
63
|
+
initializeControlledPitIntentForSimulation(sim, options);
|
|
61
64
|
applyEnvironmentScenario(sim, options);
|
|
65
|
+
applyControlledRunoffResponse(sim, options);
|
|
62
66
|
return sim;
|
|
63
67
|
}
|
|
64
68
|
|
|
@@ -120,7 +124,14 @@ export function createEnvironmentRuntime(host) {
|
|
|
120
124
|
}
|
|
121
125
|
episodeState.step += 1;
|
|
122
126
|
advanceDriverEpisodes(episodeState, options.controlledDrivers);
|
|
123
|
-
const result = buildResult({
|
|
127
|
+
const result = buildResult({
|
|
128
|
+
host,
|
|
129
|
+
episodeState,
|
|
130
|
+
events: stepEvents,
|
|
131
|
+
actionErrors: errors,
|
|
132
|
+
actions,
|
|
133
|
+
rewardEnabled: true,
|
|
134
|
+
});
|
|
124
135
|
episodeState.lastResult = result;
|
|
125
136
|
emitExternalRenderFrame(host, result, { source: 'step', actions });
|
|
126
137
|
host.afterStep(result);
|
|
@@ -150,8 +161,9 @@ export function createEnvironmentRuntime(host) {
|
|
|
150
161
|
Object.keys(normalizedPlacements).forEach((driverId) => {
|
|
151
162
|
sim.clearCarControls?.(driverId);
|
|
152
163
|
sim.setAutomaticPitIntentEnabled?.(driverId, false);
|
|
153
|
-
sim.setPitIntent?.(driverId, 0);
|
|
154
164
|
const car = sim.cars?.find?.((item) => item.id === driverId);
|
|
165
|
+
resetEnvironmentPitStop(car);
|
|
166
|
+
sim.setPitIntent?.(driverId, 0);
|
|
155
167
|
if (car) sim.applyRunoffResponse?.(car);
|
|
156
168
|
});
|
|
157
169
|
resetDriverEpisodes(episodeState, Object.keys(normalizedPlacements));
|
|
@@ -208,6 +220,7 @@ function mergePlainObjects(base, overrides, path = []) {
|
|
|
208
220
|
if (!isPlainObject(base) || !isPlainObject(overrides)) return overrides;
|
|
209
221
|
const merged = { ...base };
|
|
210
222
|
Object.entries(overrides).forEach(([key, value]) => {
|
|
223
|
+
if (value === undefined) return;
|
|
211
224
|
const nextPath = [...path, key];
|
|
212
225
|
merged[key] = isPlainObject(value) &&
|
|
213
226
|
isPlainObject(base[key]) &&
|
|
@@ -235,6 +248,10 @@ function isNoopPitIntent(pitIntent) {
|
|
|
235
248
|
function initializeControlledPitIntent(host) {
|
|
236
249
|
const sim = host.getSimulation?.();
|
|
237
250
|
const options = host.getOptions?.();
|
|
251
|
+
initializeControlledPitIntentForSimulation(sim, options);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function initializeControlledPitIntentForSimulation(sim, options) {
|
|
238
255
|
options?.controlledDrivers?.forEach?.((driverId) => {
|
|
239
256
|
sim?.setAutomaticPitIntentEnabled?.(driverId, false);
|
|
240
257
|
sim?.setPitIntent?.(driverId, 0);
|
|
@@ -243,6 +260,24 @@ function initializeControlledPitIntent(host) {
|
|
|
243
260
|
});
|
|
244
261
|
}
|
|
245
262
|
|
|
263
|
+
function resetEnvironmentPitStop(car) {
|
|
264
|
+
const stop = car?.pitStop;
|
|
265
|
+
if (!stop) return;
|
|
266
|
+
stop.status = 'pending';
|
|
267
|
+
stop.phase = null;
|
|
268
|
+
stop.intent = 0;
|
|
269
|
+
stop.queueingForService = false;
|
|
270
|
+
stop.route = null;
|
|
271
|
+
stop.routeProgress = 0;
|
|
272
|
+
stop.routeStartRaceDistance = null;
|
|
273
|
+
stop.routeEndRaceDistance = null;
|
|
274
|
+
stop.serviceRemaining = 0;
|
|
275
|
+
stop.penaltyServiceRemaining = 0;
|
|
276
|
+
stop.penaltyServiceTotal = 0;
|
|
277
|
+
stop.servingPenaltyIds = [];
|
|
278
|
+
stop.serviceProfile = null;
|
|
279
|
+
}
|
|
280
|
+
|
|
246
281
|
function buildResult({
|
|
247
282
|
host,
|
|
248
283
|
episodeState,
|
|
@@ -251,6 +286,7 @@ function buildResult({
|
|
|
251
286
|
actions = {},
|
|
252
287
|
controlledDrivers = null,
|
|
253
288
|
stateOutput = null,
|
|
289
|
+
rewardEnabled = false,
|
|
254
290
|
}) {
|
|
255
291
|
const options = host.getOptions();
|
|
256
292
|
const sim = host.getSimulation();
|
|
@@ -276,18 +312,23 @@ function buildResult({
|
|
|
276
312
|
...options,
|
|
277
313
|
controlledDrivers: resultDrivers,
|
|
278
314
|
}, episode);
|
|
279
|
-
const
|
|
315
|
+
const driverEpisode = resultDrivers === options.controlledDrivers
|
|
316
|
+
? episode
|
|
317
|
+
: evaluateEpisode(observationSnapshot, options, episodeState, options.controlledDrivers);
|
|
318
|
+
const driverEpisodeInfo = buildDriverEpisodeInfo(episodeState, options, driverEpisode);
|
|
280
319
|
const { state, rewardSnapshot } = buildResultState(sim, observationSnapshot, resolvedStateOutput);
|
|
281
|
-
const reward =
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
320
|
+
const reward = rewardEnabled
|
|
321
|
+
? computeReward({
|
|
322
|
+
options: { ...options, controlledDrivers: resultDrivers },
|
|
323
|
+
observation,
|
|
324
|
+
events,
|
|
325
|
+
snapshot: rewardSnapshot,
|
|
326
|
+
actions,
|
|
327
|
+
previousSnapshot: episodeState.previousSnapshot,
|
|
328
|
+
metrics,
|
|
329
|
+
driverEpisodeInfo: rewardEpisodeInfo,
|
|
330
|
+
})
|
|
331
|
+
: null;
|
|
291
332
|
episodeState.lastObservationSnapshot = observationSnapshot;
|
|
292
333
|
episodeState.lastRewardSnapshot = rewardSnapshot;
|
|
293
334
|
return {
|
|
@@ -304,7 +345,7 @@ function buildResult({
|
|
|
304
345
|
elapsedSeconds: observationSnapshot.time,
|
|
305
346
|
seed: options.seed,
|
|
306
347
|
trackSeed: options.trackSeed,
|
|
307
|
-
controlledDrivers: [...
|
|
348
|
+
controlledDrivers: [...resultDrivers],
|
|
308
349
|
actionErrors,
|
|
309
350
|
endReason: episode.endReason,
|
|
310
351
|
drivers: driverEpisodeInfo,
|
|
@@ -312,6 +353,13 @@ function buildResult({
|
|
|
312
353
|
};
|
|
313
354
|
}
|
|
314
355
|
|
|
356
|
+
function applyControlledRunoffResponse(sim, options) {
|
|
357
|
+
options?.controlledDrivers?.forEach?.((driverId) => {
|
|
358
|
+
const car = sim?.cars?.find?.((item) => item.id === driverId);
|
|
359
|
+
if (car) sim?.applyRunoffResponse?.(car);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
315
363
|
function buildResultState(sim, observationSnapshot, stateOutput) {
|
|
316
364
|
if (stateOutput === 'none') return { state: null, rewardSnapshot: observationSnapshot };
|
|
317
365
|
if (stateOutput === 'minimal') return { state: { snapshot: observationSnapshot }, rewardSnapshot: observationSnapshot };
|
|
@@ -182,7 +182,9 @@ export function applyEnvironmentPlacements(sim, track, carsById, placements = {}
|
|
|
182
182
|
Object.entries(placements).forEach(([driverId, placement]) => {
|
|
183
183
|
const currentCar = carsById.get(driverId);
|
|
184
184
|
if (!currentCar) return;
|
|
185
|
-
states[driverId] = placementToCarState(track, currentCar, placement
|
|
185
|
+
states[driverId] = placementToCarState(track, currentCar, placement, {
|
|
186
|
+
physicsMode: sim.physicsMode,
|
|
187
|
+
});
|
|
186
188
|
});
|
|
187
189
|
if (typeof sim.setCarStates === 'function') {
|
|
188
190
|
sim.setCarStates(states);
|
|
@@ -193,13 +195,14 @@ export function applyEnvironmentPlacements(sim, track, carsById, placements = {}
|
|
|
193
195
|
});
|
|
194
196
|
}
|
|
195
197
|
|
|
196
|
-
function placementToCarState(track, currentCar, placement) {
|
|
198
|
+
function placementToCarState(track, currentCar, placement, { physicsMode = 'arcade' } = {}) {
|
|
197
199
|
const currentDistanceMeters = currentCar.distanceMeters ?? simUnitsToMeters(currentCar.raceDistance ?? currentCar.progress ?? 0);
|
|
198
200
|
const raceDistance = metersToSimUnits(Math.max(0, placement.distanceMeters ?? currentDistanceMeters));
|
|
199
201
|
const offset = metersToSimUnits(placement.offsetMeters ?? 0);
|
|
200
202
|
const base = pointAt(track, raceDistance);
|
|
201
203
|
const position = offsetTrackPoint(base, offset);
|
|
202
204
|
const heading = normalizeAngle(base.heading + (placement.headingErrorRadians ?? 0));
|
|
205
|
+
const speed = placement.speedKph == null ? null : kphToSimSpeed(placement.speedKph);
|
|
203
206
|
const partial = {
|
|
204
207
|
x: position.x,
|
|
205
208
|
y: position.y,
|
|
@@ -217,11 +220,27 @@ function placementToCarState(track, currentCar, placement) {
|
|
|
217
220
|
canAttack: true,
|
|
218
221
|
throttle: 0,
|
|
219
222
|
brake: 0,
|
|
223
|
+
longitudinalAcceleration: 0,
|
|
224
|
+
lateralAcceleration: 0,
|
|
225
|
+
lateralG: 0,
|
|
226
|
+
longitudinalG: 0,
|
|
227
|
+
gripUsage: 0,
|
|
228
|
+
slipAngleRadians: 0,
|
|
229
|
+
tractionLimited: false,
|
|
220
230
|
steeringAngle: 0,
|
|
221
231
|
yawRate: 0,
|
|
222
232
|
stabilityState: 'stable',
|
|
223
233
|
};
|
|
224
|
-
if (
|
|
234
|
+
if (speed != null) {
|
|
235
|
+
partial.speed = speed;
|
|
236
|
+
if (physicsMode === 'simulator') {
|
|
237
|
+
partial.velocityX = Math.cos(heading) * speed;
|
|
238
|
+
partial.velocityY = Math.sin(heading) * speed;
|
|
239
|
+
} else {
|
|
240
|
+
partial.velocityX = null;
|
|
241
|
+
partial.velocityY = null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
225
244
|
return partial;
|
|
226
245
|
}
|
|
227
246
|
|