@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.
- package/CHANGELOG.md +39 -0
- package/INSTALL_AND_UPDATE.md +3 -1
- package/README.md +142 -9
- package/docs/architecture.md +49 -15
- package/docs/concepts.md +20 -3
- package/docs/data_contract.md +185 -27
- package/docs/data_model.md +1 -1
- package/docs/learnings.md +1 -1
- package/docs/rules.md +140 -19
- package/docs/system_specs.md +39 -16
- package/docs/training.md +103 -1
- package/examples/train-basic-policy.mjs +5 -6
- package/examples/trainingData.mjs +133 -0
- package/package.json +5 -2
- package/src/api/PaddockSimulatorController.js +32 -2
- package/src/app/F1SimulatorApp.js +1101 -128
- package/src/app/domBindings.js +10 -1
- package/src/config/defaultOptions.js +4 -4
- package/src/environment/actions.js +25 -2
- package/src/environment/evaluation.js +184 -0
- package/src/environment/index.d.ts +281 -1
- package/src/environment/index.js +13 -0
- package/src/environment/observationOptions.js +12 -0
- package/src/environment/observations.js +189 -17
- package/src/environment/options.js +7 -0
- package/src/environment/recorder.js +28 -0
- package/src/environment/runtime.js +41 -6
- package/src/environment/scenarios.js +220 -0
- package/src/environment/sensors.js +145 -35
- package/src/environment/specs.js +92 -8
- package/src/environment/workerProtocol.js +52 -0
- package/src/index.d.ts +366 -3
- package/src/index.js +21 -0
- package/src/rendering/proceduralTrackAsset.js +336 -23
- package/src/simulation/collisionGeometry.js +214 -0
- package/src/simulation/driverController.js +431 -65
- package/src/simulation/raceSimulation.js +2132 -171
- package/src/simulation/rules/collisionSteward.js +69 -0
- package/src/simulation/rules/penaltyLedger.js +153 -0
- package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
- package/src/simulation/rules/tireRequirementSteward.js +29 -0
- package/src/simulation/rules/trackLimitsSteward.js +85 -0
- package/src/simulation/rulesConfig.js +352 -0
- package/src/simulation/trackModel.js +1154 -76
- package/src/simulation/units.js +6 -4
- package/src/simulation/vehicleGeometry.js +247 -0
- package/src/simulation/vehiclePhysics.js +82 -50
- package/src/simulation/wheelSurface.js +338 -0
- package/src/styles.css +300 -42
- package/src/ui/bannerTemplates.js +69 -0
- package/src/ui/componentTemplates.js +36 -96
- package/src/ui/raceControlStatusBanner.js +39 -0
- package/src/ui/shellTemplate.js +1 -1
- 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 {
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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 =
|
|
216
|
-
const halfWidth =
|
|
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
|
|
package/src/environment/specs.js
CHANGED
|
@@ -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:
|
|
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
|
+
}
|