@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
@@ -1,39 +1,58 @@
1
1
  import { nearestTrackState } from '../simulation/trackModel.js';
2
2
  import { metersToSimUnits, simUnitsToMeters } from '../simulation/units.js';
3
- import { VEHICLE_LIMITS } from '../simulation/vehiclePhysics.js';
3
+ import { VEHICLE_GEOMETRY } from '../simulation/vehicleGeometry.js';
4
4
 
5
5
  const TRACK_RAY_STEP_METERS = 1;
6
6
  const TRACK_RAY_REFINE_STEPS = 12;
7
+ const ANALYTIC_TRACK_RAY_MAX_CURVATURE = 0.00005;
8
+ const PIT_CONNECTOR_RAY_FALLBACK_METERS = 55;
7
9
  export const DEFAULT_RAY_ANGLES_DEGREES = [-135, -60, -20, 0, 20, 60, 135, 180];
8
10
 
9
11
  export function buildNearbyCars(car, snapshot, { maxCars = 6, radiusMeters = 150 } = {}) {
10
- return snapshot.cars
11
- .filter((other) => other.id !== car.id)
12
- .map((other) => {
13
- const dx = other.x - car.x;
14
- const dy = other.y - car.y;
15
- const distanceMeters = simUnitsToMeters(Math.hypot(dx, dy));
16
- const forward = Math.cos(car.heading) * dx + Math.sin(car.heading) * dy;
17
- const right = -Math.sin(car.heading) * dx + Math.cos(car.heading) * dy;
18
- return {
19
- id: other.id,
20
- relativeForwardMeters: simUnitsToMeters(forward),
21
- relativeRightMeters: simUnitsToMeters(right),
22
- relativeDistanceMeters: distanceMeters,
23
- relativeSpeedKph: other.speedKph - car.speedKph,
24
- relativeHeadingRadians: normalizeRelativeHeading(other.heading - car.heading),
25
- ahead: forward > 0,
26
- sameLap: other.lap === car.lap,
27
- };
28
- })
29
- .filter((entry) => entry.relativeDistanceMeters <= radiusMeters)
30
- .sort((a, b) => a.relativeDistanceMeters - b.relativeDistanceMeters)
31
- .slice(0, maxCars);
12
+ const limit = Math.max(0, Math.floor(maxCars));
13
+ if (limit === 0) return [];
14
+ const radius = metersToSimUnits(radiusMeters);
15
+ const radiusSquared = radius * radius;
16
+ const forwardX = Math.cos(car.heading);
17
+ const forwardY = Math.sin(car.heading);
18
+ const rightX = -Math.sin(car.heading);
19
+ const rightY = Math.cos(car.heading);
20
+ const nearest = [];
21
+
22
+ snapshot.cars.forEach((other, order) => {
23
+ if (other.id === car.id) return;
24
+ const dx = other.x - car.x;
25
+ const dy = other.y - car.y;
26
+ const distanceSquared = dx * dx + dy * dy;
27
+ if (distanceSquared > radiusSquared) return;
28
+
29
+ const forward = forwardX * dx + forwardY * dy;
30
+ const right = rightX * dx + rightY * dy;
31
+ const entry = {
32
+ id: other.id,
33
+ relativeForwardMeters: simUnitsToMeters(forward),
34
+ relativeRightMeters: simUnitsToMeters(right),
35
+ relativeDistanceMeters: simUnitsToMeters(Math.sqrt(distanceSquared)),
36
+ relativeSpeedKph: other.speedKph - car.speedKph,
37
+ relativeHeadingRadians: normalizeRelativeHeading(other.heading - car.heading),
38
+ ahead: forward > 0,
39
+ sameLap: other.lap === car.lap,
40
+ distanceSquared,
41
+ order,
42
+ };
43
+ insertNearestCar(nearest, entry, limit);
44
+ });
45
+
46
+ return nearest.map(({ distanceSquared, order, ...entry }) => entry);
32
47
  }
33
48
 
34
49
  export function buildRaySensors(car, snapshot, rayOptions = {}) {
35
50
  const angles = rayOptions.anglesDegrees ?? DEFAULT_RAY_ANGLES_DEGREES;
36
51
  const lengthMeters = rayOptions.lengthMeters ?? 120;
52
+ const origin = getCarRayOrigin(car);
53
+ const trackContext = rayOptions.detectTrack === false
54
+ ? null
55
+ : createTrackRayContext(car, snapshot, origin);
37
56
 
38
57
  return angles.map((angleDegrees) => ({
39
58
  angleDegrees,
@@ -41,28 +60,49 @@ export function buildRaySensors(car, snapshot, rayOptions = {}) {
41
60
  lengthMeters,
42
61
  track: rayOptions.detectTrack === false
43
62
  ? createTrackMiss(lengthMeters)
44
- : estimateTrackHit(car, snapshot, angleDegrees, lengthMeters),
63
+ : estimateTrackHit(car, snapshot, angleDegrees, lengthMeters, trackContext),
45
64
  car: rayOptions.detectCars === false
46
65
  ? { hit: false, distanceMeters: lengthMeters, driverId: null, relativeSpeedKph: 0 }
47
- : estimateCarHit(car, snapshot, angleDegrees, lengthMeters),
66
+ : estimateCarHit(car, snapshot, angleDegrees, lengthMeters, origin),
48
67
  }));
49
68
  }
50
69
 
51
- function estimateTrackHit(car, snapshot, angleDegrees, lengthMeters) {
70
+ function createTrackRayContext(car, snapshot, origin) {
71
+ if (!Array.isArray(snapshot.track?.samples) || snapshot.track.samples.length === 0) {
72
+ return { origin, originState: null };
73
+ }
74
+ return {
75
+ origin,
76
+ originState: nearestTrackState(snapshot.track, origin, car.progress),
77
+ };
78
+ }
79
+
80
+ function estimateTrackHit(car, snapshot, angleDegrees, lengthMeters, context = null) {
52
81
  if (!Array.isArray(snapshot.track?.samples) || snapshot.track.samples.length === 0) {
53
82
  return estimateLocalTrackHit(car, snapshot, angleDegrees, lengthMeters);
54
83
  }
55
84
 
56
- const origin = getCarRayOrigin(car);
85
+ const origin = context?.origin ?? getCarRayOrigin(car);
57
86
  const ray = getCarRayVector(car, angleDegrees);
58
87
  const maxDistance = metersToSimUnits(lengthMeters);
59
88
  const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
89
+ const originState = context?.originState ?? nearestTrackState(snapshot.track, origin, car.progress);
90
+ const includePitLane = Boolean(car.inPitLane || car.pitLanePart || originState.inPitLane);
91
+ const analyticHit = estimateAnalyticMainTrackHit({
92
+ car,
93
+ track: snapshot.track,
94
+ originState,
95
+ ray,
96
+ lengthMeters,
97
+ includePitLane,
98
+ });
99
+ if (analyticHit) return analyticHit;
60
100
  let previousDistance = 0;
61
101
  let previousInside = null;
62
102
 
63
103
  for (let distance = 0; distance <= maxDistance; distance += step) {
64
104
  const state = nearestTrackState(snapshot.track, pointOnRay(origin, ray, distance), car.progress);
65
- const inside = isInsideTrackBorder(state, snapshot.track);
105
+ const inside = isInsideTrackBorder(state, snapshot.track, includePitLane);
66
106
  if (previousInside == null) {
67
107
  previousInside = inside;
68
108
  previousDistance = distance;
@@ -79,6 +119,7 @@ function estimateTrackHit(car, snapshot, angleDegrees, lengthMeters) {
79
119
  previousDistance,
80
120
  distance,
81
121
  kind,
122
+ includePitLane,
82
123
  );
83
124
  return {
84
125
  hit: true,
@@ -93,6 +134,64 @@ function estimateTrackHit(car, snapshot, angleDegrees, lengthMeters) {
93
134
  return createTrackMiss(lengthMeters);
94
135
  }
95
136
 
137
+ function insertNearestCar(nearest, entry, limit) {
138
+ let insertAt = nearest.findIndex((candidate) => (
139
+ entry.distanceSquared < candidate.distanceSquared ||
140
+ (entry.distanceSquared === candidate.distanceSquared && entry.order < candidate.order)
141
+ ));
142
+ if (insertAt < 0) insertAt = nearest.length;
143
+ if (insertAt >= limit) return;
144
+ nearest.splice(insertAt, 0, entry);
145
+ if (nearest.length > limit) nearest.pop();
146
+ }
147
+
148
+ function estimateAnalyticMainTrackHit({ car, track, originState, ray, lengthMeters, includePitLane }) {
149
+ if (includePitLane || isNearPitConnector(track, originState)) return null;
150
+ if (Math.abs(originState.curvature ?? 0) > ANALYTIC_TRACK_RAY_MAX_CURVATURE) return null;
151
+
152
+ const lateral = ray.x * originState.normalX + ray.y * originState.normalY;
153
+ if (Math.abs(lateral) < 0.08) return createTrackMiss(lengthMeters);
154
+
155
+ const trackHalfWidth = track.width / 2;
156
+ const offset = originState.signedOffset ?? car.signedOffset ?? 0;
157
+ const inside = Math.abs(offset) <= trackHalfWidth;
158
+ const targetEdge = getLocalTrackTransitionTarget({
159
+ inside,
160
+ offsetMeters: offset,
161
+ lateral,
162
+ trackHalfWidthMeters: trackHalfWidth,
163
+ });
164
+ if (targetEdge == null) return createTrackMiss(lengthMeters);
165
+
166
+ const distance = (targetEdge - offset) / lateral;
167
+ const maxDistance = metersToSimUnits(lengthMeters);
168
+ if (distance < 0 || distance > maxDistance) return createTrackMiss(lengthMeters);
169
+
170
+ return {
171
+ hit: true,
172
+ distanceMeters: simUnitsToMeters(distance),
173
+ kind: inside ? 'exit' : 'entry',
174
+ };
175
+ }
176
+
177
+ function isNearPitConnector(track, state) {
178
+ const pitLane = track.pitLane;
179
+ if (!pitLane?.enabled || !Number.isFinite(state?.distance)) return false;
180
+ const window = metersToSimUnits(PIT_CONNECTOR_RAY_FALLBACK_METERS);
181
+ const entryDistance = pitLane.entry?.trackDistance ?? pitLane.entry?.distanceFromStart;
182
+ const exitDistance = pitLane.exit?.trackDistance ?? pitLane.exit?.distanceFromStart;
183
+ return wrappedTrackDistance(state.distance, entryDistance, track.length) <= window ||
184
+ wrappedTrackDistance(state.distance, exitDistance, track.length) <= window;
185
+ }
186
+
187
+ function wrappedTrackDistance(first, second, totalLength) {
188
+ if (!Number.isFinite(first) || !Number.isFinite(second) || !Number.isFinite(totalLength) || totalLength <= 0) {
189
+ return Infinity;
190
+ }
191
+ const delta = Math.abs(first - second);
192
+ return Math.min(delta, totalLength - delta);
193
+ }
194
+
96
195
  function estimateLocalTrackHit(car, snapshot, angleDegrees, lengthMeters) {
97
196
  const trackHalfWidthMeters = simUnitsToMeters(snapshot.track.width / 2);
98
197
  const offsetMeters = simUnitsToMeters(car.signedOffset ?? 0);
@@ -122,13 +221,13 @@ function getLocalTrackTransitionTarget({ inside, offsetMeters, lateral, trackHal
122
221
  return null;
123
222
  }
124
223
 
125
- function refineTrackTransitionDistance(track, origin, ray, progressHint, lowDistance, highDistance, kind) {
224
+ function refineTrackTransitionDistance(track, origin, ray, progressHint, lowDistance, highDistance, kind, includePitLane = true) {
126
225
  let low = lowDistance;
127
226
  let high = highDistance;
128
227
  for (let index = 0; index < TRACK_RAY_REFINE_STEPS; index += 1) {
129
228
  const middle = (low + high) / 2;
130
229
  const state = nearestTrackState(track, pointOnRay(origin, ray, middle), progressHint);
131
- const inside = isInsideTrackBorder(state, track);
230
+ const inside = isInsideTrackBorder(state, track, includePitLane);
132
231
  if (kind === 'entry') {
133
232
  if (inside) high = middle;
134
233
  else low = middle;
@@ -141,7 +240,8 @@ function refineTrackTransitionDistance(track, origin, ray, progressHint, lowDist
141
240
  return high;
142
241
  }
143
242
 
144
- function isInsideTrackBorder(state, track) {
243
+ function isInsideTrackBorder(state, track, includePitLane = true) {
244
+ if (includePitLane && state.inPitLane) return true;
145
245
  return state.crossTrackError <= track.width / 2;
146
246
  }
147
247
 
@@ -153,13 +253,13 @@ function createTrackMiss(lengthMeters) {
153
253
  };
154
254
  }
155
255
 
156
- function estimateCarHit(car, snapshot, angleDegrees, lengthMeters) {
157
- const origin = getCarRayOrigin(car);
256
+ function estimateCarHit(car, snapshot, angleDegrees, lengthMeters, origin = getCarRayOrigin(car)) {
158
257
  const ray = getCarRayVector(car, angleDegrees);
159
258
  const maxDistance = metersToSimUnits(lengthMeters);
160
259
  let closest = null;
161
260
  snapshot.cars.forEach((other) => {
162
261
  if (other.id === car.id) return;
262
+ if (!carRayBroadphaseHit(origin, ray, maxDistance, other)) return;
163
263
  const hitDistance = intersectCarFootprint(origin, ray, other);
164
264
  if (hitDistance == null || hitDistance > maxDistance) return;
165
265
  const distanceMeters = simUnitsToMeters(hitDistance);
@@ -178,6 +278,16 @@ function estimateCarHit(car, snapshot, angleDegrees, lengthMeters) {
178
278
  return publicHit;
179
279
  }
180
280
 
281
+ function carRayBroadphaseHit(origin, ray, maxDistance, other) {
282
+ const dx = other.x - origin.x;
283
+ const dy = other.y - origin.y;
284
+ const projection = dx * ray.x + dy * ray.y;
285
+ const radius = Math.hypot(VEHICLE_GEOMETRY.bodyLength, VEHICLE_GEOMETRY.bodyWidth) / 2;
286
+ if (projection < -radius || projection > maxDistance + radius) return false;
287
+ const perpendicularSquared = Math.max(0, dx * dx + dy * dy - projection * projection);
288
+ return perpendicularSquared <= radius * radius;
289
+ }
290
+
181
291
  export function getCarRayOrigin(car) {
182
292
  return {
183
293
  x: car.x,
@@ -212,8 +322,8 @@ function intersectCarFootprint(origin, ray, other) {
212
322
  x: dot(ray, forward),
213
323
  y: dot(ray, right),
214
324
  };
215
- const halfLength = VEHICLE_LIMITS.carLength / 2;
216
- const halfWidth = VEHICLE_LIMITS.carWidth / 2;
325
+ const halfLength = VEHICLE_GEOMETRY.bodyLength / 2;
326
+ const halfWidth = VEHICLE_GEOMETRY.bodyWidth / 2;
217
327
  return intersectAxisAlignedBoxRay(localOrigin, localRay, halfLength, halfWidth);
218
328
  }
219
329
 
@@ -1,4 +1,10 @@
1
+ import { normalizeLookaheadMeters } from './observationOptions.js';
2
+
1
3
  export function buildActionSpec(options) {
4
+ const compounds = Array.isArray(options.rules?.modules?.tireStrategy?.compounds) &&
5
+ options.rules.modules.tireStrategy.compounds.length
6
+ ? [...options.rules.modules.tireStrategy.compounds]
7
+ : ['S', 'M', 'H'];
2
8
  return {
3
9
  version: 1,
4
10
  controlledDrivers: [...options.controlledDrivers],
@@ -8,6 +14,8 @@ export function buildActionSpec(options) {
8
14
  steering: { min: -1, max: 1, unit: 'normalized' },
9
15
  throttle: { min: 0, max: 1, unit: 'normalized' },
10
16
  brake: { min: 0, max: 1, unit: 'normalized' },
17
+ pitIntent: { values: [0, 1, 2], unit: 'request', optional: true },
18
+ pitCompound: { values: compounds, unit: 'compound', optional: true },
11
19
  },
12
20
  },
13
21
  };
@@ -16,8 +24,9 @@ export function buildActionSpec(options) {
16
24
  export function buildObservationSpec(options) {
17
25
  const rayOptions = options.sensors.rays;
18
26
  const nearbyOptions = options.sensors.nearbyCars;
27
+ const lookaheadMeters = normalizeLookaheadMeters(options.observation?.lookaheadMeters);
19
28
  return {
20
- version: 1,
29
+ version: 2,
21
30
  controlledDrivers: [...options.controlledDrivers],
22
31
  object: {
23
32
  self: [
@@ -35,12 +44,24 @@ export function buildObservationSpec(options) {
35
44
  { name: 'trackHeadingErrorRadians', unit: 'rad' },
36
45
  { name: 'onTrack', unit: 'boolean' },
37
46
  { name: 'surface', unit: 'label' },
47
+ { name: 'inPitLane', unit: 'boolean' },
48
+ { name: 'pitLanePart', unit: 'nullable:label' },
49
+ { name: 'pitBoxId', unit: 'nullable:id' },
38
50
  { name: 'tireEnergy', unit: 'nullable:number' },
51
+ { name: 'pitIntent', unit: '0:none|1:if-free|2:committed' },
52
+ { name: 'pitTargetCompound', unit: 'nullable:compound' },
53
+ { name: 'pitStopStatus', unit: 'nullable:label' },
54
+ { name: 'pitStopPhase', unit: 'nullable:label' },
55
+ { name: 'pitStopServiceRemainingSeconds', unit: 'nullable:seconds' },
56
+ { name: 'pitStopPenaltyServiceRemainingSeconds', unit: 'nullable:seconds' },
57
+ { name: 'pitStopsCompleted', unit: 'count' },
39
58
  ],
40
59
  race: [
41
60
  { name: 'position', unit: 'rank' },
42
61
  { name: 'totalCars', unit: 'count' },
43
62
  { name: 'raceMode', unit: 'label' },
63
+ { name: 'pitLaneOpen', unit: 'boolean' },
64
+ { name: 'redFlag', unit: 'boolean' },
44
65
  { name: 'totalLaps', unit: 'count' },
45
66
  ],
46
67
  rays: {
@@ -64,16 +85,79 @@ export function buildObservationSpec(options) {
64
85
  maxCars: nearbyOptions.maxCars,
65
86
  radiusMeters: nearbyOptions.radiusMeters,
66
87
  },
88
+ track: {
89
+ lengthMeters: { unit: 'm' },
90
+ widthMeters: { unit: 'm' },
91
+ curvature: { unit: '1/sim-unit' },
92
+ lookaheadMeters: [...lookaheadMeters],
93
+ lookahead: {
94
+ distanceMeters: { unit: 'm' },
95
+ curvature: { unit: '1/sim-unit' },
96
+ headingDeltaRadians: { unit: 'rad' },
97
+ },
98
+ },
67
99
  events: { type: 'array' },
68
100
  },
69
101
  vector: {
70
- schema: [
71
- { name: 'self.speedKph', unit: 'kph', scale: 'fixed:400' },
72
- { name: 'self.trackOffsetMeters', unit: 'm', scale: 'fixed:meters' },
73
- { name: 'self.trackHeadingErrorRadians', unit: 'rad', scale: 'fixed:pi' },
74
- { name: 'self.onTrack', scale: 'boolean' },
75
- { name: 'race.position', scale: 'fixed:field-position' },
76
- ],
102
+ schema: buildVectorSchema({ rayOptions, nearbyOptions, lookaheadMeters }),
77
103
  },
78
104
  };
79
105
  }
106
+
107
+ function buildVectorSchema({ rayOptions, nearbyOptions, lookaheadMeters }) {
108
+ const schema = [
109
+ { name: 'self.speedKph', unit: 'kph', scale: 'fixed:400' },
110
+ { name: 'self.speedMetersPerSecond', unit: 'm/s', scale: 'fixed:120' },
111
+ { name: 'self.steeringAngleRadians', unit: 'rad', scale: 'fixed:pi' },
112
+ { name: 'self.throttle', scale: '0..1' },
113
+ { name: 'self.brake', scale: '0..1' },
114
+ { name: 'self.lapProgressRatio', scale: '0..1' },
115
+ { name: 'self.trackOffsetMeters', unit: 'm', scale: 'fixed:meters' },
116
+ { name: 'self.trackHeadingErrorRadians', unit: 'rad', scale: 'fixed:pi' },
117
+ { name: 'self.onTrack', scale: 'boolean' },
118
+ { name: 'self.inPitLane', scale: 'boolean' },
119
+ { name: 'self.tireEnergy', scale: '0..100' },
120
+ { name: 'self.pitIntent', scale: '0..2' },
121
+ { name: 'self.pitStopActive', scale: 'boolean' },
122
+ { name: 'race.positionNormalized', scale: '0..1' },
123
+ { name: 'race.raceModeGreen', scale: 'boolean' },
124
+ { name: 'race.raceModeSafetyCar', scale: 'boolean' },
125
+ { name: 'race.redFlag', scale: 'boolean' },
126
+ { name: 'race.pitLaneOpen', scale: 'boolean' },
127
+ { name: 'track.curvature', scale: 'track-curvature' },
128
+ ];
129
+ lookaheadMeters.forEach((_, index) => {
130
+ schema.push(
131
+ { name: `track.lookahead[${index}].curvature`, scale: 'track-curvature' },
132
+ { name: `track.lookahead[${index}].headingDeltaRadians`, unit: 'rad', scale: 'fixed:pi' },
133
+ );
134
+ });
135
+ if (rayOptions.enabled) {
136
+ rayOptions.anglesDegrees.forEach((_, index) => {
137
+ schema.push(
138
+ { name: `rays[${index}].track.distanceRatio`, scale: '0..1' },
139
+ { name: `rays[${index}].track.hit`, scale: 'boolean' },
140
+ { name: `rays[${index}].track.kindExit`, scale: 'boolean' },
141
+ { name: `rays[${index}].track.kindEntry`, scale: 'boolean' },
142
+ { name: `rays[${index}].car.distanceRatio`, scale: '0..1' },
143
+ { name: `rays[${index}].car.hit`, scale: 'boolean' },
144
+ { name: `rays[${index}].car.relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
145
+ );
146
+ });
147
+ }
148
+ if (nearbyOptions.enabled) {
149
+ for (let index = 0; index < nearbyOptions.maxCars; index += 1) {
150
+ schema.push(
151
+ { name: `nearbyCars[${index}].present`, scale: 'boolean' },
152
+ { name: `nearbyCars[${index}].relativeForwardRatio`, scale: '-1..1' },
153
+ { name: `nearbyCars[${index}].relativeRightRatio`, scale: '-1..1' },
154
+ { name: `nearbyCars[${index}].relativeDistanceRatio`, scale: '0..1' },
155
+ { name: `nearbyCars[${index}].relativeSpeedKph`, unit: 'kph', scale: 'fixed:200' },
156
+ { name: `nearbyCars[${index}].relativeHeadingRadians`, unit: 'rad', scale: 'fixed:pi' },
157
+ { name: `nearbyCars[${index}].ahead`, scale: 'boolean' },
158
+ { name: `nearbyCars[${index}].sameLap`, scale: 'boolean' },
159
+ );
160
+ }
161
+ }
162
+ return schema;
163
+ }
@@ -0,0 +1,52 @@
1
+ export function createEnvironmentWorkerProtocol(env) {
2
+ return {
3
+ handle(message = {}) {
4
+ try {
5
+ return handleEnvironmentMessage(env, message);
6
+ } catch (error) {
7
+ return createErrorResponse(message, error);
8
+ }
9
+ },
10
+ };
11
+ }
12
+
13
+ export function handleEnvironmentMessage(env, message = {}) {
14
+ const id = message.id ?? null;
15
+ switch (message.type) {
16
+ case 'reset':
17
+ return createSuccessResponse(id, 'reset:result', env.reset(message.options ?? {}));
18
+ case 'step':
19
+ return createSuccessResponse(id, 'step:result', env.step(message.actions ?? {}));
20
+ case 'getActionSpec':
21
+ return createSuccessResponse(id, 'getActionSpec:result', env.getActionSpec());
22
+ case 'getObservationSpec':
23
+ return createSuccessResponse(id, 'getObservationSpec:result', env.getObservationSpec());
24
+ case 'getObservation':
25
+ return createSuccessResponse(id, 'getObservation:result', env.getObservation());
26
+ case 'getState':
27
+ return createSuccessResponse(id, 'getState:result', env.getState());
28
+ case 'destroy':
29
+ env.destroy();
30
+ return createSuccessResponse(id, 'destroy:result', null);
31
+ default:
32
+ throw new Error(`Unsupported PaddockJS environment worker message type: ${message.type}`);
33
+ }
34
+ }
35
+
36
+ function createSuccessResponse(id, type, result) {
37
+ return {
38
+ id,
39
+ ok: true,
40
+ type,
41
+ result,
42
+ };
43
+ }
44
+
45
+ function createErrorResponse(message, error) {
46
+ return {
47
+ id: message?.id ?? null,
48
+ ok: false,
49
+ type: 'error',
50
+ error: error instanceof Error ? error.message : String(error),
51
+ };
52
+ }