@inventure71/paddockjs 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/INSTALL_AND_UPDATE.md +3 -1
  3. package/README.md +142 -9
  4. package/docs/architecture.md +49 -15
  5. package/docs/concepts.md +20 -3
  6. package/docs/data_contract.md +185 -27
  7. package/docs/data_model.md +1 -1
  8. package/docs/learnings.md +1 -1
  9. package/docs/rules.md +140 -19
  10. package/docs/system_specs.md +39 -16
  11. package/docs/training.md +103 -1
  12. package/examples/train-basic-policy.mjs +5 -6
  13. package/examples/trainingData.mjs +133 -0
  14. package/package.json +5 -2
  15. package/src/api/PaddockSimulatorController.js +32 -2
  16. package/src/app/F1SimulatorApp.js +1101 -128
  17. package/src/app/domBindings.js +10 -1
  18. package/src/config/defaultOptions.js +4 -4
  19. package/src/environment/actions.js +25 -2
  20. package/src/environment/evaluation.js +184 -0
  21. package/src/environment/index.d.ts +281 -1
  22. package/src/environment/index.js +13 -0
  23. package/src/environment/observationOptions.js +12 -0
  24. package/src/environment/observations.js +189 -17
  25. package/src/environment/options.js +7 -0
  26. package/src/environment/recorder.js +28 -0
  27. package/src/environment/runtime.js +41 -6
  28. package/src/environment/scenarios.js +220 -0
  29. package/src/environment/sensors.js +145 -35
  30. package/src/environment/specs.js +92 -8
  31. package/src/environment/workerProtocol.js +52 -0
  32. package/src/index.d.ts +366 -3
  33. package/src/index.js +21 -0
  34. package/src/rendering/proceduralTrackAsset.js +336 -23
  35. package/src/simulation/collisionGeometry.js +214 -0
  36. package/src/simulation/driverController.js +431 -65
  37. package/src/simulation/raceSimulation.js +2132 -171
  38. package/src/simulation/rules/collisionSteward.js +69 -0
  39. package/src/simulation/rules/penaltyLedger.js +153 -0
  40. package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
  41. package/src/simulation/rules/tireRequirementSteward.js +29 -0
  42. package/src/simulation/rules/trackLimitsSteward.js +85 -0
  43. package/src/simulation/rulesConfig.js +352 -0
  44. package/src/simulation/trackModel.js +1154 -76
  45. package/src/simulation/units.js +6 -4
  46. package/src/simulation/vehicleGeometry.js +247 -0
  47. package/src/simulation/vehiclePhysics.js +82 -50
  48. package/src/simulation/wheelSurface.js +338 -0
  49. package/src/styles.css +300 -42
  50. package/src/ui/bannerTemplates.js +69 -0
  51. package/src/ui/componentTemplates.js +36 -96
  52. package/src/ui/raceControlStatusBanner.js +39 -0
  53. package/src/ui/shellTemplate.js +1 -1
  54. package/src/ui/templateUtils.js +23 -0
@@ -6,7 +6,9 @@ export function querySimulatorDom(root) {
6
6
  startLightsLabel: root.querySelector('[data-start-lights-label]'),
7
7
  towerLap: root.querySelector('[data-tower-lap-readout]'),
8
8
  towerTotalLaps: root.querySelector('[data-tower-total-laps]'),
9
- towerSafetyBanner: root.querySelector('[data-tower-safety-banner]'),
9
+ towerRaceControlBanner: root.querySelector('[data-tower-race-control-banner]'),
10
+ towerRaceControlKicker: root.querySelector('[data-tower-race-control-kicker]'),
11
+ towerRaceControlTitle: root.querySelector('[data-tower-race-control-title]'),
10
12
  timingGapLabel: root.querySelector('[data-timing-gap-label]'),
11
13
  lap: root.querySelector('[data-lap-readout]'),
12
14
  drs: root.querySelector('[data-drs-readout]'),
@@ -45,12 +47,17 @@ export function querySimulatorDom(root) {
45
47
  telemetryDrawerWorkbench: root.querySelector('[data-race-telemetry-drawer]'),
46
48
  telemetryDrawer: root.querySelector('[data-telemetry-drawer]'),
47
49
  telemetryDrawerToggle: root.querySelector('[data-telemetry-drawer-toggle]'),
50
+ stewardMessage: root.querySelector('[data-steward-message]'),
51
+ stewardMessageKicker: root.querySelector('[data-steward-message-kicker]'),
52
+ stewardMessageTitle: root.querySelector('[data-steward-message-title]'),
53
+ stewardMessageDetail: root.querySelector('[data-steward-message-detail]'),
48
54
  raceDataPanel: root.querySelector('[data-race-data-panel]'),
49
55
  raceDataKicker: root.querySelector('[data-race-data-kicker]'),
50
56
  raceDataTitle: root.querySelector('[data-race-data-title]'),
51
57
  raceDataNumber: root.querySelector('[data-race-data-number]'),
52
58
  raceDataSubtitle: root.querySelector('[data-race-data-subtitle]'),
53
59
  raceDataOpen: root.querySelector('[data-race-data-open]'),
60
+ raceDataDismiss: root.querySelector('[data-race-data-dismiss]'),
54
61
  finishPanel: root.querySelector('[data-race-finish-panel]'),
55
62
  finishWinner: root.querySelector('[data-race-finish-winner]'),
56
63
  finishClassification: root.querySelector('[data-race-finish-classification]'),
@@ -65,6 +72,8 @@ export function querySimulatorDom(root) {
65
72
  timingGapModeButtons: root.querySelectorAll('[data-timing-gap-mode]'),
66
73
  cameraButtons: root.querySelectorAll('[data-camera-mode]'),
67
74
  overviewModeButtons: root.querySelectorAll('[data-overview-mode]'),
75
+ bannerMuteButtons: root.querySelectorAll('[data-race-data-banners-muted]'),
76
+ simulationSpeedButtons: root.querySelectorAll('[data-simulation-speed]'),
68
77
  zoomInButton: root.querySelector('[data-zoom-in]'),
69
78
  zoomOutButton: root.querySelector('[data-zoom-out]'),
70
79
  readouts,
@@ -13,7 +13,7 @@ export const DEFAULT_F1_SIMULATOR_OPTIONS = {
13
13
  showBackLink: true,
14
14
  ui: {
15
15
  layoutPreset: 'standard',
16
- cameraControls: 'embedded',
16
+ cameraControls: 'external',
17
17
  showFps: true,
18
18
  showTimingTower: true,
19
19
  showTelemetry: true,
@@ -46,7 +46,7 @@ export const PADDOCK_SIMULATOR_PRESETS = {
46
46
  dashboard: {
47
47
  ui: {
48
48
  layoutPreset: 'standard',
49
- cameraControls: 'embedded',
49
+ cameraControls: 'external',
50
50
  showFps: true,
51
51
  showTimingTower: true,
52
52
  showTelemetry: true,
@@ -74,7 +74,7 @@ export const PADDOCK_SIMULATOR_PRESETS = {
74
74
  'compact-race': {
75
75
  ui: {
76
76
  layoutPreset: 'standard',
77
- cameraControls: 'embedded',
77
+ cameraControls: 'external',
78
78
  showFps: false,
79
79
  showTimingTower: false,
80
80
  showTelemetry: false,
@@ -110,7 +110,7 @@ export const PADDOCK_THEME_CSS_VARIABLES = {
110
110
  raceViewMinHeight: '--paddock-race-view-min-height',
111
111
  };
112
112
 
113
- const SUPPORTED_CAMERA_MODES = new Set(['overview', 'leader', 'selected', 'show-all']);
113
+ const SUPPORTED_CAMERA_MODES = new Set(['overview', 'leader', 'selected', 'show-all', 'pit']);
114
114
 
115
115
  export function resolveF1SimulatorOptions(options = {}) {
116
116
  const presetName = Object.hasOwn(PADDOCK_SIMULATOR_PRESETS, options.preset)
@@ -4,6 +4,7 @@ import { VEHICLE_LIMITS } from '../simulation/vehiclePhysics.js';
4
4
  export function resolveActionMap(actions = {}, controlledDrivers = [], { policy = 'strict' } = {}) {
5
5
  const errors = [];
6
6
  const controlsByDriver = {};
7
+ const pitIntentByDriver = {};
7
8
 
8
9
  controlledDrivers.forEach((driverId) => {
9
10
  const action = actions?.[driverId];
@@ -13,9 +14,11 @@ export function resolveActionMap(actions = {}, controlledDrivers = [], { policy
13
14
  }
14
15
  const normalized = normalizeAction(action, driverId, policy, errors);
15
16
  if (normalized) controlsByDriver[driverId] = normalized;
17
+ const pitIntent = normalizePitIntentAction(action, driverId, policy, errors);
18
+ if (pitIntent != null) pitIntentByDriver[driverId] = pitIntent;
16
19
  });
17
20
 
18
- return { controlsByDriver, errors };
21
+ return { controlsByDriver, pitIntentByDriver, errors };
19
22
  }
20
23
 
21
24
  export function normalizeAction(action, driverId = 'unknown', policy = 'strict', errors = []) {
@@ -39,7 +42,27 @@ function finiteActionValue(value, message, policy, errors) {
39
42
  return null;
40
43
  }
41
44
 
42
- function handleActionError(message, policy, errors) {
45
+ function normalizePitIntentAction(action, driverId, policy, errors) {
46
+ if (!Object.hasOwn(action, 'pitIntent')) return null;
47
+ const number = Number(action.pitIntent);
48
+ if (Number.isInteger(number) && number >= 0 && number <= 2) {
49
+ const pitCompound = normalizePitCompoundAction(action, driverId, policy, errors);
50
+ if (pitCompound === false) return null;
51
+ return pitCompound == null ? number : { intent: number, targetCompound: pitCompound };
52
+ }
53
+ handleActionError(`Invalid pitIntent action for controlled driver: ${driverId}`, policy, errors);
54
+ return null;
55
+ }
56
+
57
+ function normalizePitCompoundAction(action, driverId, policy, errors) {
58
+ if (!Object.hasOwn(action, 'pitCompound') && !Object.hasOwn(action, 'pitTargetCompound')) return null;
59
+ const value = action.pitCompound ?? action.pitTargetCompound;
60
+ if (typeof value === 'string' && value.trim()) return value.trim();
61
+ handleActionError(`Invalid pitCompound action for controlled driver: ${driverId}`, policy, errors);
62
+ return false;
63
+ }
64
+
65
+ export function handleActionError(message, policy, errors) {
43
66
  if (policy === 'strict') throw new Error(message);
44
67
  errors.push(message);
45
68
  }
@@ -0,0 +1,184 @@
1
+ import { simUnitsToMeters } from '../simulation/units.js';
2
+ import { createPaddockEnvironment } from './runtime.js';
3
+
4
+ export const DEFAULT_EVALUATION_CASES = Object.freeze([
5
+ {
6
+ name: 'cornering',
7
+ seed: 7101,
8
+ trackSeed: 2601,
9
+ maxSteps: 420,
10
+ scenario: { preset: 'cornering' },
11
+ },
12
+ {
13
+ name: 'off-track-recovery',
14
+ seed: 7102,
15
+ trackSeed: 2602,
16
+ maxSteps: 360,
17
+ scenario: { preset: 'off-track-recovery' },
18
+ },
19
+ {
20
+ name: 'overtaking-pack',
21
+ seed: 7103,
22
+ trackSeed: 2603,
23
+ maxSteps: 600,
24
+ scenario: { preset: 'overtaking-pack' },
25
+ },
26
+ {
27
+ name: 'pit-entry',
28
+ seed: 7104,
29
+ trackSeed: 2604,
30
+ maxSteps: 480,
31
+ scenario: { preset: 'pit-entry' },
32
+ },
33
+ ]);
34
+
35
+ export function runEnvironmentEvaluation({
36
+ baseOptions,
37
+ policy,
38
+ cases = DEFAULT_EVALUATION_CASES,
39
+ createEnvironment = createPaddockEnvironment,
40
+ } = {}) {
41
+ if (!baseOptions || typeof baseOptions !== 'object') {
42
+ throw new Error('PaddockJS environment evaluation requires baseOptions.');
43
+ }
44
+ if (!policy) {
45
+ throw new Error('PaddockJS environment evaluation requires a policy function or object with predict().');
46
+ }
47
+
48
+ return {
49
+ cases: cases.map((evaluationCase) => runEvaluationCase({
50
+ baseOptions,
51
+ policy,
52
+ evaluationCase,
53
+ createEnvironment,
54
+ })),
55
+ };
56
+ }
57
+
58
+ export function createEvaluationTracker(initialResult) {
59
+ const initialCars = carsById(initialResult.state.snapshot);
60
+ const metrics = Object.fromEntries(initialResult.info.controlledDrivers.map((driverId) => {
61
+ const car = initialCars.get(driverId);
62
+ return [driverId, {
63
+ distanceMeters: 0,
64
+ lapProgressMeters: lapProgressMetersFromSnapshotCar(car),
65
+ offTrackSteps: car && car.surface !== 'track' ? 1 : 0,
66
+ contactCount: 0,
67
+ recoverySuccess: false,
68
+ passCount: 0,
69
+ lapTimeSeconds: null,
70
+ startedOffTrack: Boolean(car && car.surface !== 'track'),
71
+ startPosition: car?.rank ?? null,
72
+ bestPosition: car?.rank ?? null,
73
+ startDistanceMeters: cumulativeDistanceMetersFromSnapshotCar(car),
74
+ }];
75
+ }));
76
+
77
+ return {
78
+ update(result) {
79
+ const currentCars = carsById(result.state.snapshot);
80
+ result.info.controlledDrivers.forEach((driverId) => {
81
+ const car = currentCars.get(driverId);
82
+ const entry = metrics[driverId];
83
+ if (!car || !entry) return;
84
+ entry.distanceMeters = Math.max(0, cumulativeDistanceMetersFromSnapshotCar(car) - entry.startDistanceMeters);
85
+ entry.lapProgressMeters = lapProgressMetersFromSnapshotCar(car);
86
+ if (car.surface !== 'track') entry.offTrackSteps += 1;
87
+ if (entry.startedOffTrack && car.surface === 'track') entry.recoverySuccess = true;
88
+ if (Number.isFinite(car.rank)) {
89
+ entry.bestPosition = entry.bestPosition == null ? car.rank : Math.min(entry.bestPosition, car.rank);
90
+ entry.passCount = entry.startPosition == null ? 0 : Math.max(0, entry.startPosition - entry.bestPosition);
91
+ }
92
+ const completedLaps = car.lapTelemetry?.completedLaps ?? 0;
93
+ if (completedLaps > 0 && entry.lapTimeSeconds == null) {
94
+ entry.lapTimeSeconds = result.info.elapsedSeconds;
95
+ }
96
+ });
97
+ result.events.forEach((event) => {
98
+ if (event.type !== 'collision') return;
99
+ Object.keys(metrics).forEach((driverId) => {
100
+ if (event.driverId === driverId || event.carId === driverId || event.otherCarId === driverId || event.driverIds?.includes?.(driverId)) {
101
+ metrics[driverId].contactCount += 1;
102
+ }
103
+ });
104
+ });
105
+ },
106
+ finish() {
107
+ return Object.fromEntries(Object.entries(metrics).map(([driverId, entry]) => [
108
+ driverId,
109
+ {
110
+ distanceMeters: entry.distanceMeters,
111
+ lapProgressMeters: entry.lapProgressMeters,
112
+ offTrackSteps: entry.offTrackSteps,
113
+ contactCount: entry.contactCount,
114
+ recoverySuccess: entry.recoverySuccess,
115
+ passCount: entry.passCount,
116
+ lapTimeSeconds: entry.lapTimeSeconds,
117
+ },
118
+ ]));
119
+ },
120
+ };
121
+ }
122
+
123
+ function cumulativeDistanceMetersFromSnapshotCar(car) {
124
+ const distance = Number(car?.distanceMeters ?? car?.raceDistanceMeters);
125
+ if (Number.isFinite(distance)) return distance;
126
+ return simUnitsToMeters(car?.raceDistance ?? 0);
127
+ }
128
+
129
+ function lapProgressMetersFromSnapshotCar(car) {
130
+ const progressMeters = Number(car?.lapProgressMeters ?? car?.progressMeters);
131
+ if (Number.isFinite(progressMeters)) return progressMeters;
132
+ return simUnitsToMeters(car?.progress ?? 0);
133
+ }
134
+
135
+ function runEvaluationCase({ baseOptions, policy, evaluationCase, createEnvironment }) {
136
+ const env = createEnvironment({
137
+ ...baseOptions,
138
+ seed: evaluationCase.seed ?? baseOptions.seed,
139
+ trackSeed: evaluationCase.trackSeed ?? baseOptions.trackSeed,
140
+ scenario: {
141
+ ...(baseOptions.scenario ?? {}),
142
+ ...(evaluationCase.scenario ?? {}),
143
+ },
144
+ });
145
+ let result = env.reset();
146
+ const tracker = createEvaluationTracker(result);
147
+ const maxSteps = Math.max(1, Math.floor(evaluationCase.maxSteps ?? baseOptions.episode?.maxSteps ?? 1000));
148
+ let steps = 0;
149
+ while (!result.done && steps < maxSteps) {
150
+ const actions = Object.fromEntries(result.info.controlledDrivers.map((driverId) => [
151
+ driverId,
152
+ predictAction(policy, result.observation[driverId], {
153
+ driverId,
154
+ caseName: evaluationCase.name,
155
+ step: steps,
156
+ result,
157
+ }),
158
+ ]));
159
+ result = env.step(actions);
160
+ tracker.update(result);
161
+ steps += 1;
162
+ }
163
+ env.destroy?.();
164
+ return {
165
+ name: evaluationCase.name,
166
+ seed: evaluationCase.seed ?? baseOptions.seed,
167
+ trackSeed: evaluationCase.trackSeed ?? baseOptions.trackSeed,
168
+ steps,
169
+ done: result.done,
170
+ endReason: result.info.endReason,
171
+ metrics: tracker.finish(),
172
+ };
173
+ }
174
+
175
+ function predictAction(policy, observation, context) {
176
+ const action = typeof policy === 'function'
177
+ ? policy(observation, context)
178
+ : policy.predict(observation, context);
179
+ return action ?? { steering: 0, throttle: 0, brake: 1 };
180
+ }
181
+
182
+ function carsById(snapshot) {
183
+ return new Map(snapshot.cars.map((car) => [car.id, car]));
184
+ }
@@ -1,10 +1,20 @@
1
1
  export type TireCompound = 'S' | 'M' | 'H';
2
+ export type PaddockPitIntent = 0 | 1 | 2;
3
+ export type PaddockScenarioPreset = 'cornering' | 'off-track-recovery' | 'overtaking-pack' | 'pit-entry';
2
4
 
3
5
  export interface TeamData {
4
6
  id?: string;
5
7
  name?: string;
6
8
  color?: string;
7
9
  icon?: string;
10
+ pitCrew?: PaddockPitCrewStats;
11
+ pitCrewStats?: PaddockPitCrewStats;
12
+ }
13
+
14
+ export interface PaddockPitCrewStats {
15
+ speed?: number;
16
+ consistency?: number;
17
+ reliability?: number;
8
18
  }
9
19
 
10
20
  export interface SimulatorDriver {
@@ -37,6 +47,49 @@ export interface RaceEvent {
37
47
  [key: string]: unknown;
38
48
  }
39
49
 
50
+ export type PaddockRulesetName = 'paddock' | 'custom' | 'grandPrix2025' | 'fia2025';
51
+
52
+ export interface PaddockPenaltyEntry {
53
+ id: string;
54
+ type: string;
55
+ at: number;
56
+ lap: number;
57
+ driverId: string;
58
+ strictness: number;
59
+ status: PaddockPenaltyStatus;
60
+ penaltySeconds: number;
61
+ pendingPenaltySeconds?: number;
62
+ consequences: PaddockPenaltyConsequence[];
63
+ serviceType?: 'driveThrough' | 'stopGo' | null;
64
+ serviceRequired?: boolean;
65
+ serviceServedAt?: number | null;
66
+ appliedAt?: number;
67
+ cancelledAt?: number;
68
+ unserved?: boolean;
69
+ positionDrop?: number;
70
+ gridDrop?: number;
71
+ disqualified?: boolean;
72
+ reason?: string;
73
+ otherCarId?: string;
74
+ aheadDriverId?: string;
75
+ atFaultDriverId?: string;
76
+ sharedFault?: boolean;
77
+ impactSpeedKph?: number;
78
+ impactSpeedThresholdKph?: number;
79
+ [key: string]: unknown;
80
+ }
81
+
82
+ export type PaddockPenaltyConsequence =
83
+ | { type: 'warning' }
84
+ | { type: 'time'; seconds: number }
85
+ | { type: 'driveThrough'; conversionSeconds?: number }
86
+ | { type: 'stopGo'; seconds?: number; conversionSeconds?: number }
87
+ | { type: 'positionDrop'; positions: number }
88
+ | { type: 'gridDrop'; positions: number }
89
+ | { type: 'disqualification' };
90
+
91
+ export type PaddockPenaltyStatus = 'issued' | 'served' | 'applied' | 'cancelled';
92
+
40
93
  export interface RaceSnapshot {
41
94
  time: number;
42
95
  totalLaps: number;
@@ -48,6 +101,7 @@ export interface RaceSnapshot {
48
101
  track: Record<string, unknown>;
49
102
  cars: Array<Record<string, unknown>>;
50
103
  events: RaceEvent[];
104
+ penalties: PaddockPenaltyEntry[];
51
105
  [key: string]: unknown;
52
106
  }
53
107
 
@@ -55,10 +109,88 @@ export interface PaddockAction {
55
109
  steering: number;
56
110
  throttle: number;
57
111
  brake: number;
112
+ pitIntent?: PaddockPitIntent;
113
+ pitCompound?: TireCompound | string;
114
+ pitTargetCompound?: TireCompound | string;
58
115
  }
59
116
 
60
117
  export type PaddockActionMap = Record<string, PaddockAction>;
61
118
 
119
+ export interface PaddockRaceRules {
120
+ ruleset?: PaddockRulesetName;
121
+ profile?: PaddockRulesetName;
122
+ modules?: {
123
+ pitStops?: {
124
+ enabled?: boolean;
125
+ pitLaneSpeedLimitKph?: number;
126
+ defaultStopSeconds?: number;
127
+ variability?: {
128
+ enabled?: boolean;
129
+ perfect?: boolean;
130
+ speedImpactSeconds?: number;
131
+ consistencyJitterSeconds?: number;
132
+ issueChance?: number;
133
+ issueMaxDelaySeconds?: number;
134
+ };
135
+ doubleStacking?: boolean;
136
+ maxConcurrentPitLaneCars?: number;
137
+ minimumPitLaneGapMeters?: number;
138
+ tirePitRequestThresholdPercent?: number;
139
+ tirePitCommitThresholdPercent?: number;
140
+ };
141
+ tireStrategy?: {
142
+ enabled?: boolean;
143
+ compounds?: TireCompound[];
144
+ mandatoryDistinctDryCompounds?: number | null;
145
+ };
146
+ penalties?: {
147
+ enabled?: boolean;
148
+ stewardStrictness?: number;
149
+ trackLimits?: {
150
+ strictness?: number;
151
+ warningsBeforePenalty?: number;
152
+ relaxedMarginMeters?: number;
153
+ timePenaltySeconds?: number;
154
+ consequences?: PaddockPenaltyConsequence[];
155
+ };
156
+ collision?: {
157
+ strictness?: number;
158
+ timePenaltySeconds?: number;
159
+ consequences?: PaddockPenaltyConsequence[];
160
+ minimumSeverity?: number;
161
+ relaxedSeverityMargin?: number;
162
+ minimumImpactSpeedKph?: number;
163
+ relaxedImpactSpeedKph?: number;
164
+ };
165
+ tireRequirement?: {
166
+ strictness?: number;
167
+ timePenaltySeconds?: number;
168
+ consequences?: PaddockPenaltyConsequence[];
169
+ };
170
+ pitLaneSpeeding?: {
171
+ strictness?: number;
172
+ speedLimitKph?: number;
173
+ marginKph?: number;
174
+ relaxedMarginKph?: number;
175
+ timePenaltySeconds?: number;
176
+ consequences?: PaddockPenaltyConsequence[];
177
+ };
178
+ };
179
+ weather?: { enabled?: boolean };
180
+ reliability?: { enabled?: boolean };
181
+ fuelLoad?: { enabled?: boolean };
182
+ };
183
+ drsDetectionSeconds?: number;
184
+ safetyCarSpeed?: number;
185
+ safetyCarLeadDistance?: number;
186
+ safetyCarGap?: number;
187
+ collisionRestitution?: number;
188
+ standingStart?: boolean;
189
+ startLightCount?: number;
190
+ startLightInterval?: number;
191
+ startLightsOutHold?: number;
192
+ }
193
+
62
194
  export interface PaddockSensorRayResult {
63
195
  angleDegrees: number;
64
196
  angleRadians: number;
@@ -87,6 +219,12 @@ export interface PaddockNearbyCarObservation {
87
219
  sameLap: boolean;
88
220
  }
89
221
 
222
+ export interface PaddockTrackLookaheadObservation {
223
+ distanceMeters: number;
224
+ curvature: number;
225
+ headingDeltaRadians: number;
226
+ }
227
+
90
228
  export interface PaddockDriverObservationObject {
91
229
  self: {
92
230
  id: string;
@@ -103,14 +241,32 @@ export interface PaddockDriverObservationObject {
103
241
  trackHeadingErrorRadians: number;
104
242
  onTrack: boolean;
105
243
  surface: string;
244
+ inPitLane: boolean;
245
+ pitLanePart: 'entry' | 'fast-lane' | 'working-lane' | 'exit' | 'service-box' | 'garage-box' | null;
246
+ pitBoxId: string | null;
106
247
  tireEnergy: number | null;
248
+ pitIntent: PaddockPitIntent;
249
+ pitTargetCompound: TireCompound | string | null;
250
+ pitStopStatus: 'pending' | 'entering' | 'queued' | 'servicing' | 'exiting' | 'completed' | null;
251
+ pitStopPhase: 'entry' | 'queue' | 'queue-release' | 'penalty' | 'service' | 'exit' | null;
252
+ pitStopServiceRemainingSeconds: number | null;
253
+ pitStopPenaltyServiceRemainingSeconds: number | null;
254
+ pitStopsCompleted: number;
107
255
  };
108
256
  race: {
109
257
  position: number;
110
258
  totalCars: number;
111
259
  raceMode: string;
260
+ pitLaneOpen: boolean;
261
+ redFlag: boolean;
112
262
  totalLaps: number;
113
263
  };
264
+ track: {
265
+ lengthMeters: number;
266
+ widthMeters: number;
267
+ curvature: number;
268
+ lookahead: PaddockTrackLookaheadObservation[];
269
+ };
114
270
  rays: PaddockSensorRayResult[];
115
271
  nearbyCars: PaddockNearbyCarObservation[];
116
272
  events: RaceEvent[];
@@ -137,10 +293,14 @@ export interface PaddockEnvironmentOptions {
137
293
  trackSeed?: number;
138
294
  totalLaps?: number;
139
295
  frameSkip?: number;
296
+ rules?: PaddockRaceRules;
140
297
  actionPolicy?: 'strict' | 'report';
141
298
  scenario?: {
142
299
  participants?: 'all' | 'controlled-only' | string[];
143
300
  nonControlled?: 'ai';
301
+ preset?: PaddockScenarioPreset;
302
+ placements?: Record<string, PaddockScenarioPlacement>;
303
+ traffic?: PaddockScenarioTrafficPlacement[];
144
304
  };
145
305
  sensors?: {
146
306
  rays?: {
@@ -157,6 +317,9 @@ export interface PaddockEnvironmentOptions {
157
317
  };
158
318
  };
159
319
  sensorsByDriver?: Record<string, PaddockEnvironmentOptions['sensors']>;
320
+ observation?: {
321
+ lookaheadMeters?: number[];
322
+ };
160
323
  episode?: {
161
324
  maxSteps?: number;
162
325
  endOnRaceFinish?: boolean;
@@ -164,6 +327,23 @@ export interface PaddockEnvironmentOptions {
164
327
  reward?: (payload: PaddockRewardContext) => number;
165
328
  }
166
329
 
330
+ export interface PaddockScenarioPlacement {
331
+ distanceMeters?: number;
332
+ startDistanceMeters?: number;
333
+ offsetMeters?: number;
334
+ speedKph?: number;
335
+ headingErrorRadians?: number;
336
+ }
337
+
338
+ export interface PaddockScenarioTrafficPlacement {
339
+ driverId: string;
340
+ relativeTo: string;
341
+ deltaDistanceMeters?: number;
342
+ offsetMeters?: number;
343
+ speedKph?: number;
344
+ headingErrorRadians?: number;
345
+ }
346
+
167
347
  export interface PaddockRewardContext {
168
348
  driverId: string;
169
349
  previous: RaceSnapshot | null;
@@ -195,12 +375,14 @@ export interface PaddockActionSpec {
195
375
  steering: { min: -1; max: 1; unit: 'normalized' };
196
376
  throttle: { min: 0; max: 1; unit: 'normalized' };
197
377
  brake: { min: 0; max: 1; unit: 'normalized' };
378
+ pitIntent: { values: [0, 1, 2]; unit: 'request'; optional: true };
379
+ pitCompound: { values: string[]; unit: 'compound'; optional: true };
198
380
  };
199
381
  };
200
382
  }
201
383
 
202
384
  export interface PaddockObservationSpec {
203
- version: 1;
385
+ version: 2;
204
386
  controlledDrivers: string[];
205
387
  object: Record<string, unknown>;
206
388
  vector: {
@@ -208,6 +390,81 @@ export interface PaddockObservationSpec {
208
390
  };
209
391
  }
210
392
 
393
+ export interface PaddockRolloutTransition {
394
+ observation: Record<string, PaddockDriverObservation>;
395
+ action: PaddockActionMap;
396
+ reward: null | Record<string, number>;
397
+ nextObservation: Record<string, PaddockDriverObservation>;
398
+ terminated: boolean;
399
+ truncated: boolean;
400
+ info: PaddockEnvironmentResult['info'];
401
+ }
402
+
403
+ export interface PaddockRolloutRecorder {
404
+ recordStep(
405
+ previousResult: PaddockEnvironmentResult,
406
+ action: PaddockActionMap,
407
+ nextResult: PaddockEnvironmentResult
408
+ ): PaddockRolloutTransition;
409
+ clear(): void;
410
+ toJSON(): PaddockRolloutTransition[];
411
+ }
412
+
413
+ export interface PaddockEvaluationCase {
414
+ name: string;
415
+ seed?: number;
416
+ trackSeed?: number;
417
+ maxSteps?: number;
418
+ scenario?: PaddockEnvironmentOptions['scenario'];
419
+ }
420
+
421
+ export interface PaddockEvaluationDriverMetrics {
422
+ distanceMeters: number;
423
+ lapProgressMeters: number;
424
+ offTrackSteps: number;
425
+ contactCount: number;
426
+ recoverySuccess: boolean;
427
+ passCount: number;
428
+ lapTimeSeconds: number | null;
429
+ }
430
+
431
+ export interface PaddockEvaluationCaseReport {
432
+ name: string;
433
+ seed?: number;
434
+ trackSeed?: number;
435
+ steps: number;
436
+ done: boolean;
437
+ endReason: string | null;
438
+ metrics: Record<string, PaddockEvaluationDriverMetrics>;
439
+ }
440
+
441
+ export interface PaddockEvaluationReport {
442
+ cases: PaddockEvaluationCaseReport[];
443
+ }
444
+
445
+ export type PaddockPolicyLike =
446
+ | ((observation: PaddockDriverObservation, context: Record<string, unknown>) => PaddockAction)
447
+ | { predict(observation: PaddockDriverObservation, context: Record<string, unknown>): PaddockAction };
448
+
449
+ export interface PaddockEnvironmentWorkerMessage {
450
+ id?: string | number | null;
451
+ type: 'reset' | 'step' | 'getActionSpec' | 'getObservationSpec' | 'getObservation' | 'getState' | 'destroy' | string;
452
+ options?: Partial<PaddockEnvironmentOptions>;
453
+ actions?: PaddockActionMap;
454
+ }
455
+
456
+ export interface PaddockEnvironmentWorkerResponse {
457
+ id: string | number | null;
458
+ ok: boolean;
459
+ type: string;
460
+ result?: unknown;
461
+ error?: string;
462
+ }
463
+
464
+ export interface PaddockEnvironmentWorkerProtocol {
465
+ handle(message: PaddockEnvironmentWorkerMessage): PaddockEnvironmentWorkerResponse;
466
+ }
467
+
211
468
  export interface PaddockEnvironmentResult {
212
469
  observation: Record<string, PaddockDriverObservation>;
213
470
  reward: null | Record<string, number>;
@@ -239,3 +496,26 @@ export interface PaddockEnvironment {
239
496
 
240
497
  export function createPaddockEnvironment(options: PaddockEnvironmentOptions): PaddockEnvironment;
241
498
  export function createProgressReward(options?: PaddockProgressRewardOptions): (context: PaddockRewardContext) => number;
499
+ export function createRolloutRecorder(): PaddockRolloutRecorder;
500
+ export function createRolloutTransition(
501
+ previousResult: PaddockEnvironmentResult,
502
+ action: PaddockActionMap,
503
+ nextResult: PaddockEnvironmentResult
504
+ ): PaddockRolloutTransition;
505
+ export const ENVIRONMENT_SCENARIO_PRESETS: readonly PaddockScenarioPreset[];
506
+ export const DEFAULT_EVALUATION_CASES: readonly PaddockEvaluationCase[];
507
+ export function createEvaluationTracker(initialResult: PaddockEnvironmentResult): {
508
+ update(result: PaddockEnvironmentResult): void;
509
+ finish(): Record<string, PaddockEvaluationDriverMetrics>;
510
+ };
511
+ export function runEnvironmentEvaluation(options: {
512
+ baseOptions: PaddockEnvironmentOptions;
513
+ policy: PaddockPolicyLike;
514
+ cases?: PaddockEvaluationCase[];
515
+ createEnvironment?: (options: PaddockEnvironmentOptions) => PaddockEnvironment;
516
+ }): PaddockEvaluationReport;
517
+ export function createEnvironmentWorkerProtocol(env: PaddockEnvironment): PaddockEnvironmentWorkerProtocol;
518
+ export function handleEnvironmentMessage(
519
+ env: PaddockEnvironment,
520
+ message: PaddockEnvironmentWorkerMessage
521
+ ): PaddockEnvironmentWorkerResponse;