@inventure71/paddockjs 4.0.0 → 4.2.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/INSTALL_AND_UPDATE.md +14 -1
  3. package/README.md +33 -1
  4. package/docs/architecture.md +3 -1
  5. package/docs/component_inventory.md +5 -4
  6. package/docs/composable-layouts.md +2 -0
  7. package/docs/data_contract.md +5 -1
  8. package/docs/getting-started.md +33 -0
  9. package/docs/index.md +4 -2
  10. package/docs/loading.md +141 -0
  11. package/docs/system_specs.md +15 -2
  12. package/docs/testing-reliability-guide.md +62 -0
  13. package/package.json +16 -9
  14. package/src/data/index.d.ts +64 -1
  15. package/src/environment/index.d.ts +75 -12
  16. package/src/index.d.ts +72 -6
  17. package/src/placeholder/index.d.ts +11 -0
  18. package/src/placeholder/index.js +61 -0
  19. package/src/placeholder.css +72 -0
  20. package/src/simulation/collisionGeometry.js +7 -9
  21. package/src/simulation/driver/driverController.js +2 -2
  22. package/src/simulation/driver/edgeRecovery.js +2 -2
  23. package/src/simulation/driver/racingControls.js +35 -25
  24. package/src/simulation/driver/racingLinePlan.js +6 -5
  25. package/src/simulation/driver/rejoinControls.js +3 -3
  26. package/src/simulation/driver/trafficScan.js +48 -22
  27. package/src/simulation/participants/participantInteractions.js +24 -15
  28. package/src/simulation/race/raceStep.js +19 -10
  29. package/src/simulation/race/safetyCar.js +2 -1
  30. package/src/simulation/rules/pitLaneSpeedingSteward.js +1 -1
  31. package/src/simulation/rules/rulesReview.js +19 -9
  32. package/src/simulation/rules/trackLimitsSteward.js +2 -2
  33. package/src/simulation/timing/sectorPerformance.js +11 -3
  34. package/src/simulation/track/spatialQueries.js +80 -11
  35. package/src/simulation/track/trackModel.js +10 -1
  36. package/src/simulation/vehicle/contactResolution.js +4 -4
  37. package/src/simulation/vehicle/vehicleGeometry.js +87 -49
  38. package/src/simulation/vehicle/vehiclePhysics.js +7 -2
  39. package/src/simulation/vehicle/vehicleSnapshots.js +5 -2
  40. package/src/simulation/vehicle/vehicleState.js +3 -2
  41. package/src/ui/raceCanvasTemplate.js +3 -2
  42. package/src/ui/raceControlsTemplate.js +1 -0
  43. package/src/ui/telemetryDrawerTemplate.js +1 -0
  44. package/src/ui/telemetryTemplates.js +1 -0
  45. package/examples/train-basic-policy.mjs +0 -276
  46. package/examples/trainingData.mjs +0 -133
@@ -6,6 +6,7 @@ import { generateCenterlineControls, generateFallbackCenterlineControls, generat
6
6
  import { isValidProceduralTrackModel } from './trackValidation.js';
7
7
  import { recalculateSampleGeometry } from './sampleGeometry.js';
8
8
  import { rotateSamplesToStandingStart, straightenStandingStartSamples } from './startStraight.js';
9
+ import { prepareSampleIndexLut } from './spatialQueries.js';
9
10
  import { createTrackSectors } from './trackSectors.js';
10
11
  import { deriveDrsZones, normalizeDrsZone } from './drsZones.js';
11
12
  import { createPitLaneModel } from './pitLaneLayout.js';
@@ -15,7 +16,14 @@ import { attachTrackSnapshotJsonSerializer } from './trackSnapshotJson.js';
15
16
 
16
17
  export { WORLD, TRACK } from './trackConstants.js';
17
18
  export { isInDrsZone } from './drsZones.js';
18
- export { nearestTrackState, offsetTrackPoint, pointAt } from './spatialQueries.js';
19
+ export {
20
+ nearestTrackState,
21
+ offsetTrackPoint,
22
+ pointAt,
23
+ pointAtInto,
24
+ sampleHeadingAt,
25
+ sampleHeadingCurvatureAtInto,
26
+ } from './spatialQueries.js';
19
27
 
20
28
  const PROCEDURAL_TRACK_CACHE = new Map();
21
29
  const TRACK_MODEL_CACHE = new WeakMap();
@@ -57,6 +65,7 @@ export function buildTrackModel(track = TRACK) {
57
65
  ? null
58
66
  : createPitLaneModel(model);
59
67
  attachTrackQueryIndex(model, createTrackQueryIndex(model));
68
+ prepareSampleIndexLut(model);
60
69
  attachTrackSnapshotJsonSerializer(model);
61
70
  if (canReuseCachedModel) {
62
71
  TRACK_MODEL_CACHE.set(track, freezeTrackModel(model));
@@ -6,7 +6,7 @@ import {
6
6
  } from '../collisionGeometry.js';
7
7
  import { canCollide, isCollidable } from '../participants/participantInteractions.js';
8
8
  import { clamp, normalizeAngle } from '../simMath.js';
9
- import { VEHICLE_LIMITS } from './vehiclePhysics.js';
9
+ import { VEHICLE_LIMITS, isSimulatorPhysicsMode } from './vehiclePhysics.js';
10
10
  import { shiftPreviousRenderPose } from '../pit/pitRouting.js';
11
11
 
12
12
  const MAX_COLLISION_CORRECTION = 4.5;
@@ -114,8 +114,8 @@ export function resolveCollisionsForSimulation(sim) {
114
114
  }
115
115
 
116
116
  if (oneCarFixed) {
117
- if (!firstPitControlled) dampPitContactVelocity(first, 0.985, sim.physicsMode === 'advanced');
118
- if (!secondPitControlled) dampPitContactVelocity(second, 0.985, sim.physicsMode === 'advanced');
117
+ if (!firstPitControlled) dampPitContactVelocity(first, 0.985, isSimulatorPhysicsMode(sim.physicsMode));
118
+ if (!secondPitControlled) dampPitContactVelocity(second, 0.985, isSimulatorPhysicsMode(sim.physicsMode));
119
119
  } else {
120
120
  applyContactVelocityResponse(sim, first, second, collision.axis);
121
121
  }
@@ -167,7 +167,7 @@ export function resolveCollisionsForSimulation(sim) {
167
167
  export function applyContactVelocityResponse(sim, first, second, axis, options = {}) {
168
168
  recordContactVelocityResponse(options.stats);
169
169
  if (
170
- sim.physicsMode === 'advanced' ||
170
+ isSimulatorPhysicsMode(sim.physicsMode) ||
171
171
  Number.isFinite(first.velocityX) ||
172
172
  Number.isFinite(second.velocityX)
173
173
  ) {
@@ -274,33 +274,22 @@ function sweptPoseSignature(pose) {
274
274
  ].join(':');
275
275
  }
276
276
 
277
- function geometryPose(car) {
278
- const { x, y, heading } = currentPoseState(car);
279
- return {
280
- x,
281
- y,
282
- heading,
283
- previousX: finiteOr(car.previousX, x),
284
- previousY: finiteOr(car.previousY, y),
285
- previousHeading: normalizeAngle(finiteOr(car.previousHeading, heading)),
286
- };
287
- }
288
-
289
277
  function currentGeometryStateMatches(car, state) {
290
278
  const pose = state?.pose;
291
279
  if (!pose) return false;
292
- const currentPose = currentPoseState(car);
293
280
  return (
294
- pose.x === currentPose.x &&
295
- pose.y === currentPose.y &&
296
- pose.heading === currentPose.heading
281
+ pose.x === finiteOr(car.x, 0) &&
282
+ pose.y === finiteOr(car.y, 0) &&
283
+ pose.heading === normalizeAngle(finiteOr(car.heading, 0))
297
284
  );
298
285
  }
299
286
 
300
287
  function geometryStateMatches(car, state) {
301
288
  const pose = state?.pose;
302
289
  if (!pose) return false;
303
- const { x, y, heading } = currentPoseState(car);
290
+ const x = finiteOr(car.x, 0);
291
+ const y = finiteOr(car.y, 0);
292
+ const heading = normalizeAngle(finiteOr(car.heading, 0));
304
293
  return (
305
294
  pose.x === x &&
306
295
  pose.y === y &&
@@ -312,6 +301,10 @@ function geometryStateMatches(car, state) {
312
301
  }
313
302
 
314
303
  export function createVehicleShapeAabb(shape) {
304
+ return writeVehicleShapeAabb({}, shape);
305
+ }
306
+
307
+ function writeVehicleShapeAabb(target, shape) {
315
308
  let minX = Infinity;
316
309
  let maxX = -Infinity;
317
310
  let minY = Infinity;
@@ -323,12 +316,11 @@ export function createVehicleShapeAabb(shape) {
323
316
  if (corner.y < minY) minY = corner.y;
324
317
  if (corner.y > maxY) maxY = corner.y;
325
318
  }
326
- return {
327
- minX,
328
- maxX,
329
- minY,
330
- maxY,
331
- };
319
+ target.minX = minX;
320
+ target.maxX = maxX;
321
+ target.minY = minY;
322
+ target.maxY = maxY;
323
+ return target;
332
324
  }
333
325
 
334
326
  export function mergeAabbs(aabbs) {
@@ -346,32 +338,73 @@ export function mergeAabbs(aabbs) {
346
338
  return { minX, maxX, minY, maxY };
347
339
  }
348
340
 
349
- export function createVehicleGeometryState(car) {
350
- const pose = geometryPose(car);
351
- const current = writeVehicleGeometry(null, pose);
352
- const previous = writeVehicleGeometry(null, {
353
- x: pose.previousX,
354
- y: pose.previousY,
355
- heading: pose.previousHeading,
356
- });
357
- const bodyAabb = createVehicleShapeAabb(current.body);
358
- const previousBodyAabb = createVehicleShapeAabb(previous.body);
359
- const sweptBodyAabb = mergeAabbs([previousBodyAabb, bodyAabb]);
341
+ function writeGeometryPose(target, car) {
342
+ const pose = target ?? {};
343
+ const x = finiteOr(car.x, 0);
344
+ const y = finiteOr(car.y, 0);
345
+ const heading = normalizeAngle(finiteOr(car.heading, 0));
346
+ pose.x = x;
347
+ pose.y = y;
348
+ pose.heading = heading;
349
+ pose.previousX = finiteOr(car.previousX, x);
350
+ pose.previousY = finiteOr(car.previousY, y);
351
+ pose.previousHeading = normalizeAngle(finiteOr(car.previousHeading, heading));
352
+ return pose;
353
+ }
360
354
 
361
- return {
362
- pose,
363
- currentSignature: currentPoseSignature(pose),
364
- sweptSignature: sweptPoseSignature(pose),
365
- signature: sweptPoseSignature(pose),
366
- current,
367
- previous,
368
- body: current.body,
369
- wheels: current.wheels,
370
- contactPatches: current.contactPatches,
371
- bodyAabb,
372
- previousBodyAabb,
373
- sweptBodyAabb,
355
+ function createGeometryStateShell() {
356
+ const state = {
357
+ pose: null,
358
+ current: null,
359
+ previous: null,
360
+ body: null,
361
+ wheels: null,
362
+ contactPatches: null,
363
+ bodyAabb: {},
364
+ previousBodyAabb: {},
365
+ sweptBodyAabb: {},
374
366
  };
367
+ // Signatures are derived lazily from the pose: nothing on the hot path reads
368
+ // them, so the per-step string formatting cost must not be paid eagerly.
369
+ Object.defineProperties(state, {
370
+ currentSignature: {
371
+ get() { return currentPoseSignature(this.pose); },
372
+ enumerable: true,
373
+ },
374
+ sweptSignature: {
375
+ get() { return sweptPoseSignature(this.pose); },
376
+ enumerable: true,
377
+ },
378
+ signature: {
379
+ get() { return sweptPoseSignature(this.pose); },
380
+ enumerable: true,
381
+ },
382
+ });
383
+ return state;
384
+ }
385
+
386
+ const PREVIOUS_GEOMETRY_POSE_SCRATCH = { x: 0, y: 0, heading: 0 };
387
+
388
+ export function createVehicleGeometryState(car, target = null) {
389
+ const state = target ?? createGeometryStateShell();
390
+ const pose = state.pose = writeGeometryPose(state.pose, car);
391
+ const current = state.current = writeVehicleGeometry(state.current, pose);
392
+ PREVIOUS_GEOMETRY_POSE_SCRATCH.x = pose.previousX;
393
+ PREVIOUS_GEOMETRY_POSE_SCRATCH.y = pose.previousY;
394
+ PREVIOUS_GEOMETRY_POSE_SCRATCH.heading = pose.previousHeading;
395
+ const previous = state.previous = writeVehicleGeometry(state.previous, PREVIOUS_GEOMETRY_POSE_SCRATCH);
396
+ const bodyAabb = writeVehicleShapeAabb(state.bodyAabb, current.body);
397
+ const previousBodyAabb = writeVehicleShapeAabb(state.previousBodyAabb, previous.body);
398
+ const sweptBodyAabb = state.sweptBodyAabb;
399
+ sweptBodyAabb.minX = Math.min(bodyAabb.minX, previousBodyAabb.minX);
400
+ sweptBodyAabb.maxX = Math.max(bodyAabb.maxX, previousBodyAabb.maxX);
401
+ sweptBodyAabb.minY = Math.min(bodyAabb.minY, previousBodyAabb.minY);
402
+ sweptBodyAabb.maxY = Math.max(bodyAabb.maxY, previousBodyAabb.maxY);
403
+
404
+ state.body = current.body;
405
+ state.wheels = current.wheels;
406
+ state.contactPatches = current.contactPatches;
407
+ return state;
375
408
  }
376
409
 
377
410
  export function createCurrentVehicleGeometryState(car, target = null) {
@@ -388,8 +421,13 @@ export function createCurrentVehicleGeometryState(car, target = null) {
388
421
 
389
422
  export function getVehicleGeometryState(car) {
390
423
  if (geometryStateMatches(car, car.geometryState)) return car.geometryState;
391
- car.geometryState = createVehicleGeometryState(car);
392
- return car.geometryState;
424
+ // Double-buffer pooled states: consumers may still hold the state returned
425
+ // for the previous pose within a step, so alternate between two reusable
426
+ // shells instead of allocating a fresh geometry tree on every pose change.
427
+ const next = createVehicleGeometryState(car, car._geometryStateSpare ?? null);
428
+ car._geometryStateSpare = car.geometryState ?? null;
429
+ car.geometryState = next;
430
+ return next;
393
431
  }
394
432
 
395
433
  export function getCurrentVehicleGeometryState(car) {
@@ -50,7 +50,11 @@ const SIMULATOR_SURFACE_MODEL = {
50
50
  };
51
51
 
52
52
  export function normalizePhysicsMode(value) {
53
- return value === 'advanced' ? 'advanced' : DEFAULT_PHYSICS_MODE;
53
+ return PHYSICS_MODES.includes(value) ? value : DEFAULT_PHYSICS_MODE;
54
+ }
55
+
56
+ export function isSimulatorPhysicsMode(value) {
57
+ return value === 'advanced';
54
58
  }
55
59
 
56
60
  function accelerationLimit(speed, surfaceGrip) {
@@ -120,7 +124,8 @@ function setPhysicsTelemetry(car, {
120
124
  }
121
125
 
122
126
  export function integrateVehiclePhysics(car, controls, dt, options = {}) {
123
- if (normalizePhysicsMode(options.physicsMode) === 'advanced') {
127
+ const mode = normalizePhysicsMode(options.physicsMode);
128
+ if (mode === 'advanced') {
124
129
  return integrateSimulatorVehiclePhysics(car, controls, dt, options);
125
130
  }
126
131
  return integrateArcadeVehiclePhysics(car, controls, dt, options);
@@ -1,4 +1,7 @@
1
- import { serializeParticipantInteraction } from '../participants/participantInteractions.js';
1
+ import {
2
+ serializeParticipantInteraction,
3
+ serializeParticipantInteractionCached,
4
+ } from '../participants/participantInteractions.js';
2
5
 
3
6
  export function finiteOrNull(value) {
4
7
  return Number.isFinite(value) ? value : null;
@@ -185,7 +188,7 @@ export function serializeRenderCarInto(target, car, dependencies) {
185
188
  target.dnfOrder = car.dnfOrder ?? null;
186
189
  target.outOfRace = Boolean(car.outOfRace || car.destroyed);
187
190
  target.pitStop = serializeRenderPitStop(car.pitStop);
188
- target.interaction = serializeParticipantInteraction(car.interaction);
191
+ target.interaction = serializeParticipantInteractionCached(car);
189
192
  return target;
190
193
  }
191
194
 
@@ -2,7 +2,7 @@ import { buildDriverPersonality } from '../driverController.js';
2
2
  import { clamp, normalizeAngle, seededRange } from '../simMath.js';
3
3
  import { offsetTrackPoint, pointAt } from '../track/trackModel.js';
4
4
  import { kphToSimSpeed, metersToSimUnits } from '../units.js';
5
- import { VEHICLE_LIMITS } from './vehiclePhysics.js';
5
+ import { VEHICLE_LIMITS, isSimulatorPhysicsMode } from './vehiclePhysics.js';
6
6
  import { applyWheelSurfaceState } from './wheelSurface.js';
7
7
  import { nearestTrackStateForCar } from '../track/trackStatePolicy.js';
8
8
  import { clearCarDnf } from '../race/retirements.js';
@@ -73,6 +73,7 @@ export function createCar(driver, index, random, track, { standingStart = false,
73
73
  downforceCoefficient: vehicle.downforceCoefficient ?? 6.1 + seededRange(random, -0.18, 0.18),
74
74
  tireGrip: vehicle.tireGrip ?? 2.22 + racecraft * 0.28 + seededRange(random, -0.03, 0.03),
75
75
  tireCare: vehicle.tireCare ?? 1,
76
+ driverModel: driver.driverModel ?? vehicle.driverModel ?? null,
76
77
  pace,
77
78
  racecraft,
78
79
  personality,
@@ -170,7 +171,7 @@ export function applyExternalCarState(car, partial, context) {
170
171
  }
171
172
  car.speed = clamp(Number.isFinite(car.speed) ? car.speed : 0, 0, VEHICLE_LIMITS.maxSpeed);
172
173
  car.heading = normalizeAngle(Number.isFinite(car.heading) ? car.heading : 0);
173
- syncExternalVelocityState(car, nextPartial, physicsMode === 'advanced');
174
+ syncExternalVelocityState(car, nextPartial, isSimulatorPhysicsMode(physicsMode));
174
175
  const centerState = nearestTrackStateForCar(track, car, car, car.progress ?? car.raceDistance);
175
176
  applyWheelSurfaceState(car, track, { centerState });
176
177
  if (
@@ -39,8 +39,9 @@ export function createRaceCanvasMarkup({
39
39
  }
40
40
  const timingPanelId = includeTimingTower ? createTimingPanelId() : '';
41
41
  const showPhysicsModeIndicator = debug.physicsModeIndicator === true;
42
- const physicsModeLabel = physicsMode === 'advanced' ? 'Advanced physics mode' : 'Arcade physics mode';
43
- const physicsModeClass = physicsMode === 'advanced' ? 'advanced' : 'arcade';
42
+ const simulatorPhysics = physicsMode === 'advanced';
43
+ const physicsModeLabel = `${simulatorPhysics ? 'Advanced' : 'Arcade'} physics mode`;
44
+ const physicsModeClass = simulatorPhysics ? 'advanced' : 'arcade';
44
45
 
45
46
  const body = `
46
47
  <div class="track-canvas" data-track-canvas></div>
@@ -49,5 +49,6 @@ export function createSafetyCarControlMarkup({ compact = false } = {}) {
49
49
  ariaLabel: 'Safety car control',
50
50
  body,
51
51
  unsupportedLabel: 'Safety car control',
52
+ loadingLabel: 'Safety car control',
52
53
  });
53
54
  }
@@ -64,5 +64,6 @@ export function createRaceTelemetryDrawerMarkup(options, {
64
64
  attributes: 'data-race-telemetry-drawer',
65
65
  body,
66
66
  unsupportedLabel: 'Race telemetry drawer',
67
+ loadingLabel: 'Race telemetry drawer',
67
68
  });
68
69
  }
@@ -158,5 +158,6 @@ export function createTelemetryPanelMarkup(options, { includeOverview = options.
158
158
  ariaLabel: 'Selected car telemetry stack',
159
159
  body,
160
160
  unsupportedLabel: 'Telemetry stack',
161
+ loadingLabel: 'Telemetry stack',
161
162
  });
162
163
  }
@@ -1,276 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { createPaddockEnvironment, createProgressReward } from '@inventure71/paddockjs/environment';
4
- import { TRAINING_DEMO_DRIVERS, TRAINING_DEMO_ENTRIES } from './trainingData.mjs';
5
-
6
- const DEFAULT_OPTIONS = Object.freeze({
7
- generations: 4,
8
- candidates: 5,
9
- episodes: 1,
10
- steps: 240,
11
- frameSkip: 4,
12
- seed: 71,
13
- traffic: false,
14
- evalOnly: false,
15
- });
16
-
17
- const INITIAL_POLICY = Object.freeze({
18
- steeringHeading: -1.2,
19
- steeringOffset: -0.35,
20
- steeringRayBalance: 0.45,
21
- throttleBias: 0.72,
22
- throttleFrontClearance: 0.28,
23
- throttleSpeedDamping: 0.18,
24
- brakeBias: 0,
25
- brakeFrontDanger: 0.65,
26
- brakeOffTrack: 0.2,
27
- });
28
-
29
- const POLICY_KEYS = Object.keys(INITIAL_POLICY);
30
-
31
- main();
32
-
33
- function main() {
34
- const options = parseCliArgs(process.argv.slice(2));
35
- if (options.help) {
36
- printHelp();
37
- return;
38
- }
39
-
40
- const driverId = TRAINING_DEMO_DRIVERS[0].id;
41
- const rng = createSeededRandom(options.seed);
42
- const reward = createProgressReward();
43
- const evaluate = (policy, seedOffset = 0) => evaluatePolicy({
44
- policy,
45
- driverId,
46
- reward,
47
- options,
48
- seedOffset,
49
- });
50
-
51
- if (options.evalOnly) {
52
- const baseline = evaluate(INITIAL_POLICY);
53
- printSummary({ options, driverId, bestPolicy: INITIAL_POLICY, best: baseline });
54
- return;
55
- }
56
-
57
- let bestPolicy = { ...INITIAL_POLICY };
58
- let best = evaluate(bestPolicy);
59
-
60
- console.log(formatGenerationLine(0, best));
61
-
62
- for (let generation = 1; generation <= options.generations; generation += 1) {
63
- const mutationScale = 0.45 / Math.sqrt(generation);
64
- const candidates = [{ policy: bestPolicy, scoreSeedOffset: generation * 1000 }];
65
-
66
- for (let index = 1; index < options.candidates; index += 1) {
67
- candidates.push({
68
- policy: mutatePolicy(bestPolicy, rng, mutationScale),
69
- scoreSeedOffset: generation * 1000 + index * 100,
70
- });
71
- }
72
-
73
- for (const candidate of candidates) {
74
- const score = evaluate(candidate.policy, candidate.scoreSeedOffset);
75
- if (score.averageReward > best.averageReward) {
76
- best = score;
77
- bestPolicy = candidate.policy;
78
- }
79
- }
80
-
81
- console.log(formatGenerationLine(generation, best));
82
- }
83
-
84
- printSummary({ options, driverId, bestPolicy, best });
85
- }
86
-
87
- function evaluatePolicy({ policy, driverId, reward, options, seedOffset = 0 }) {
88
- let totalReward = 0;
89
- let totalDistanceMeters = 0;
90
- let totalCollisions = 0;
91
- let totalOffTrackSteps = 0;
92
- let completedEpisodes = 0;
93
-
94
- for (let episode = 0; episode < options.episodes; episode += 1) {
95
- const env = createTrainingEnvironment({ driverId, reward, options, episode, seedOffset });
96
- let result = env.reset();
97
- let episodeReward = 0;
98
-
99
- for (let step = 0; step < options.steps && !result.done; step += 1) {
100
- const observation = result.observation[driverId];
101
- const action = chooseAction(policy, observation);
102
- result = env.step({ [driverId]: action });
103
- episodeReward += result.reward?.[driverId] ?? 0;
104
- totalCollisions += result.events.filter((event) => event.type === 'collision').length;
105
- if (result.observation[driverId]?.object?.self?.onTrack === false) totalOffTrackSteps += 1;
106
- }
107
-
108
- const car = result.state.snapshot.cars.find((entry) => entry.id === driverId);
109
- totalReward += episodeReward;
110
- totalDistanceMeters += Number(car?.distanceMeters ?? 0);
111
- completedEpisodes += 1;
112
- env.destroy();
113
- }
114
-
115
- return {
116
- averageReward: totalReward / Math.max(1, completedEpisodes),
117
- averageDistanceMeters: totalDistanceMeters / Math.max(1, completedEpisodes),
118
- collisions: totalCollisions,
119
- offTrackSteps: totalOffTrackSteps,
120
- };
121
- }
122
-
123
- function createTrainingEnvironment({ driverId, reward, options, episode, seedOffset }) {
124
- const drivers = options.traffic ? TRAINING_DEMO_DRIVERS.slice(0, 8) : TRAINING_DEMO_DRIVERS.slice(0, 1);
125
- return createPaddockEnvironment({
126
- drivers,
127
- entries: TRAINING_DEMO_ENTRIES,
128
- controlledDrivers: [driverId],
129
- seed: options.seed + seedOffset + episode,
130
- trackSeed: options.seed + 2026 + seedOffset + episode,
131
- totalLaps: 3,
132
- frameSkip: options.frameSkip,
133
- scenario: {
134
- participants: options.traffic ? 'all' : 'controlled-only',
135
- nonControlled: 'ai',
136
- },
137
- episode: {
138
- maxSteps: options.steps,
139
- endOnRaceFinish: true,
140
- },
141
- rules: {
142
- standingStart: false,
143
- },
144
- reward,
145
- });
146
- }
147
-
148
- function chooseAction(policy, observation) {
149
- const self = observation.object.self;
150
- const frontRay = getRayDistance(observation, 0);
151
- const leftRay = getRayDistance(observation, -60);
152
- const rightRay = getRayDistance(observation, 60);
153
- const frontDanger = clamp01((35 - frontRay) / 35);
154
- const rayBalanceMeters = rightRay - leftRay;
155
-
156
- const steering = clamp(
157
- policy.steeringHeading * self.trackHeadingErrorRadians +
158
- policy.steeringOffset * (self.trackOffsetMeters / 12) +
159
- policy.steeringRayBalance * (rayBalanceMeters / 120),
160
- -1,
161
- 1,
162
- );
163
- const throttle = clamp01(
164
- policy.throttleBias +
165
- policy.throttleFrontClearance * (frontRay / 120) -
166
- policy.throttleSpeedDamping * (self.speedKph / 320) -
167
- (self.onTrack ? 0 : 0.35),
168
- );
169
- const brake = clamp01(
170
- policy.brakeBias +
171
- policy.brakeFrontDanger * frontDanger +
172
- (self.onTrack ? 0 : policy.brakeOffTrack),
173
- );
174
-
175
- return { steering, throttle, brake };
176
- }
177
-
178
- function getRayDistance(observation, angleDegrees) {
179
- const ray = observation.object.rays.find((entry) => entry.angleDegrees === angleDegrees);
180
- return Number(ray?.track?.distanceMeters ?? ray?.lengthMeters ?? 120);
181
- }
182
-
183
- function mutatePolicy(policy, rng, scale) {
184
- return Object.fromEntries(POLICY_KEYS.map((key) => [
185
- key,
186
- policy[key] + randomNormal(rng) * scale,
187
- ]));
188
- }
189
-
190
- function parseCliArgs(argv) {
191
- const options = { ...DEFAULT_OPTIONS };
192
-
193
- for (const arg of argv) {
194
- const [name, rawValue] = arg.split('=');
195
- if (name === '--help' || name === '-h') options.help = true;
196
- else if (name === '--eval-only') options.evalOnly = true;
197
- else if (name === '--traffic') options.traffic = true;
198
- else if (name === '--no-traffic') options.traffic = false;
199
- else if (name === '--generations') options.generations = parsePositiveInt(rawValue, name);
200
- else if (name === '--candidates') options.candidates = parsePositiveInt(rawValue, name);
201
- else if (name === '--episodes') options.episodes = parsePositiveInt(rawValue, name);
202
- else if (name === '--steps') options.steps = parsePositiveInt(rawValue, name);
203
- else if (name === '--frame-skip') options.frameSkip = parsePositiveInt(rawValue, name);
204
- else if (name === '--seed') options.seed = parsePositiveInt(rawValue, name);
205
- else throw new Error(`Unknown option: ${name}`);
206
- }
207
-
208
- return options;
209
- }
210
-
211
- function parsePositiveInt(value, optionName) {
212
- const parsed = Number.parseInt(value, 10);
213
- if (!Number.isFinite(parsed) || parsed <= 0) {
214
- throw new Error(`${optionName} requires a positive integer value.`);
215
- }
216
- return parsed;
217
- }
218
-
219
- function createSeededRandom(seed) {
220
- let state = seed >>> 0;
221
- return function random() {
222
- state += 0x6D2B79F5;
223
- let value = state;
224
- value = Math.imul(value ^ (value >>> 15), value | 1);
225
- value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
226
- return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
227
- };
228
- }
229
-
230
- function randomNormal(rng) {
231
- const a = Math.max(Number.EPSILON, rng());
232
- const b = Math.max(Number.EPSILON, rng());
233
- return Math.sqrt(-2 * Math.log(a)) * Math.cos(2 * Math.PI * b);
234
- }
235
-
236
- function clamp(value, min, max) {
237
- return Math.min(max, Math.max(min, value));
238
- }
239
-
240
- function clamp01(value) {
241
- return clamp(value, 0, 1);
242
- }
243
-
244
- function formatGenerationLine(generation, score) {
245
- return [
246
- `generation=${generation}`,
247
- `reward=${score.averageReward.toFixed(2)}`,
248
- `distanceMeters=${score.averageDistanceMeters.toFixed(1)}`,
249
- `collisions=${score.collisions}`,
250
- `offTrackSteps=${score.offTrackSteps}`,
251
- ].join(' ');
252
- }
253
-
254
- function printSummary({ options, driverId, bestPolicy, best }) {
255
- console.log(JSON.stringify({
256
- driverId,
257
- options,
258
- best,
259
- bestPolicy,
260
- }, null, 2));
261
- }
262
-
263
- function printHelp() {
264
- console.log(`Usage: node examples/train-basic-policy.mjs [options]
265
-
266
- Options:
267
- --generations=N Random-search generations. Default: ${DEFAULT_OPTIONS.generations}
268
- --candidates=N Candidate policies per generation. Default: ${DEFAULT_OPTIONS.candidates}
269
- --episodes=N Episodes per policy evaluation. Default: ${DEFAULT_OPTIONS.episodes}
270
- --steps=N Maximum manual environment steps per episode. Default: ${DEFAULT_OPTIONS.steps}
271
- --frame-skip=N Simulator frames advanced by each step(action). Default: ${DEFAULT_OPTIONS.frameSkip}
272
- --seed=N Deterministic random seed. Default: ${DEFAULT_OPTIONS.seed}
273
- --traffic Keep built-in AI traffic on track during training.
274
- --eval-only Evaluate the starter policy without training.
275
- `);
276
- }