@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
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { nearestTrackState } from './trackModel.js';
|
|
2
|
+
import { metersToSimUnits } from './units.js';
|
|
3
|
+
import { getVehicleGeometryState } from './vehicleGeometry.js';
|
|
4
|
+
|
|
5
|
+
const SURFACE_PRIORITY = {
|
|
6
|
+
track: 0,
|
|
7
|
+
'pit-entry': 1,
|
|
8
|
+
'pit-exit': 1,
|
|
9
|
+
'pit-lane': 2,
|
|
10
|
+
'pit-box': 3,
|
|
11
|
+
kerb: 4,
|
|
12
|
+
grass: 5,
|
|
13
|
+
gravel: 6,
|
|
14
|
+
barrier: 7,
|
|
15
|
+
};
|
|
16
|
+
const PIT_CONNECTOR_FULL_SAMPLE_WINDOW = metersToSimUnits(35);
|
|
17
|
+
|
|
18
|
+
function finiteSignatureValue(value) {
|
|
19
|
+
return Number.isFinite(value) ? Number(value).toFixed(4) : '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function centerStateSignature(centerState) {
|
|
23
|
+
if (!centerState) return 'auto';
|
|
24
|
+
return [
|
|
25
|
+
finiteSignatureValue(centerState.distance),
|
|
26
|
+
finiteSignatureValue(centerState.signedOffset),
|
|
27
|
+
finiteSignatureValue(centerState.crossTrackError),
|
|
28
|
+
centerState.surface ?? '',
|
|
29
|
+
centerState.inPitLane ? 1 : 0,
|
|
30
|
+
centerState.pitLanePart ?? '',
|
|
31
|
+
centerState.pitBoxId ?? '',
|
|
32
|
+
].join(':');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function priority(surface) {
|
|
36
|
+
return SURFACE_PRIORITY[surface] ?? SURFACE_PRIORITY.barrier;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function worstState(states) {
|
|
40
|
+
return states.reduce((worst, state) => (
|
|
41
|
+
priority(state.surface) > priority(worst.surface) ? state : worst
|
|
42
|
+
), states[0]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function patchSamples(patch) {
|
|
46
|
+
return [patch.center, ...patch.corners];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function wrappedDistanceDelta(first, second, length) {
|
|
50
|
+
if (!Number.isFinite(first) || !Number.isFinite(second) || !Number.isFinite(length) || length <= 0) return Infinity;
|
|
51
|
+
const delta = Math.abs(first - second);
|
|
52
|
+
return Math.min(delta, length - delta);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isNearPitConnector(track, centerState) {
|
|
56
|
+
const pitLane = track.pitLane;
|
|
57
|
+
if (!pitLane?.enabled) return false;
|
|
58
|
+
const distance = centerState.distance;
|
|
59
|
+
const entryDistance = pitLane.entry?.trackDistance ?? pitLane.entry?.distanceFromStart;
|
|
60
|
+
const exitDistance = pitLane.exit?.trackDistance ?? pitLane.exit?.distanceFromStart;
|
|
61
|
+
return wrappedDistanceDelta(distance, entryDistance, track.length) <= PIT_CONNECTOR_FULL_SAMPLE_WINDOW ||
|
|
62
|
+
wrappedDistanceDelta(distance, exitDistance, track.length) <= PIT_CONNECTOR_FULL_SAMPLE_WINDOW;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function mainTrackSurface(track, crossTrackError) {
|
|
66
|
+
const trackEdge = track.width / 2;
|
|
67
|
+
const kerbEdge = trackEdge + (track.kerbWidth ?? 0);
|
|
68
|
+
const gravelEdge = kerbEdge + track.gravelWidth;
|
|
69
|
+
const runoffEdge = gravelEdge + track.runoffWidth;
|
|
70
|
+
if (crossTrackError <= trackEdge) return 'track';
|
|
71
|
+
if (crossTrackError <= kerbEdge) return 'kerb';
|
|
72
|
+
if (crossTrackError <= gravelEdge) return 'gravel';
|
|
73
|
+
if (crossTrackError <= runoffEdge) return 'grass';
|
|
74
|
+
return 'barrier';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function localTrackState(track, centerState, point) {
|
|
78
|
+
const signedOffset =
|
|
79
|
+
(point.x - centerState.x) * centerState.normalX +
|
|
80
|
+
(point.y - centerState.y) * centerState.normalY;
|
|
81
|
+
const crossTrackError = Math.abs(signedOffset);
|
|
82
|
+
const surface = mainTrackSurface(track, crossTrackError);
|
|
83
|
+
return {
|
|
84
|
+
...centerState,
|
|
85
|
+
signedOffset,
|
|
86
|
+
crossTrackError,
|
|
87
|
+
surface,
|
|
88
|
+
onTrack: surface === 'track' || surface === 'kerb',
|
|
89
|
+
inPitLane: false,
|
|
90
|
+
pitLanePart: null,
|
|
91
|
+
pitBoxId: null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function wheelFullyOutside(samples, trackLimit) {
|
|
96
|
+
const rightOutside = samples.every((state) => !state.inPitLane && state.signedOffset > trackLimit);
|
|
97
|
+
const leftOutside = samples.every((state) => !state.inPitLane && state.signedOffset < -trackLimit);
|
|
98
|
+
return {
|
|
99
|
+
rightOutside,
|
|
100
|
+
leftOutside,
|
|
101
|
+
fullyOutsideWhiteLine: rightOutside || leftOutside,
|
|
102
|
+
outsideSide: rightOutside ? 1 : leftOutside ? -1 : 0,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function wheelOutsideFromOffsets(minimumSignedOffset, maximumSignedOffset, trackLimit) {
|
|
107
|
+
const rightOutside = minimumSignedOffset > trackLimit;
|
|
108
|
+
const leftOutside = maximumSignedOffset < -trackLimit;
|
|
109
|
+
return {
|
|
110
|
+
rightOutside,
|
|
111
|
+
leftOutside,
|
|
112
|
+
fullyOutsideWhiteLine: rightOutside || leftOutside,
|
|
113
|
+
outsideSide: rightOutside ? 1 : leftOutside ? -1 : 0,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function stateFromSignedOffset(centerState, track, signedOffset) {
|
|
118
|
+
const crossTrackError = Math.abs(signedOffset);
|
|
119
|
+
const surface = mainTrackSurface(track, crossTrackError);
|
|
120
|
+
return {
|
|
121
|
+
...centerState,
|
|
122
|
+
signedOffset,
|
|
123
|
+
crossTrackError,
|
|
124
|
+
surface,
|
|
125
|
+
onTrack: surface === 'track' || surface === 'kerb',
|
|
126
|
+
inPitLane: false,
|
|
127
|
+
pitLanePart: null,
|
|
128
|
+
pitBoxId: null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function analyticWheelState(patch, centerState, track, trackLimit) {
|
|
133
|
+
const wheelCenterOffset =
|
|
134
|
+
(patch.center.x - centerState.x) * centerState.normalX +
|
|
135
|
+
(patch.center.y - centerState.y) * centerState.normalY;
|
|
136
|
+
const projectedHalfWidth =
|
|
137
|
+
Math.abs(patch.forward.x * centerState.normalX + patch.forward.y * centerState.normalY) * patch.halfLength +
|
|
138
|
+
Math.abs(patch.right.x * centerState.normalX + patch.right.y * centerState.normalY) * patch.halfWidth;
|
|
139
|
+
const minimumSignedOffset = wheelCenterOffset - projectedHalfWidth;
|
|
140
|
+
const maximumSignedOffset = wheelCenterOffset + projectedHalfWidth;
|
|
141
|
+
const signedOffset = Math.abs(minimumSignedOffset) > Math.abs(maximumSignedOffset)
|
|
142
|
+
? minimumSignedOffset
|
|
143
|
+
: maximumSignedOffset;
|
|
144
|
+
const state = stateFromSignedOffset(centerState, track, signedOffset);
|
|
145
|
+
const outside = wheelOutsideFromOffsets(minimumSignedOffset, maximumSignedOffset, trackLimit);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
id: patch.id,
|
|
149
|
+
x: patch.center.x,
|
|
150
|
+
y: patch.center.y,
|
|
151
|
+
signedOffset: state.signedOffset,
|
|
152
|
+
crossTrackError: state.crossTrackError,
|
|
153
|
+
surface: state.surface,
|
|
154
|
+
onTrack: Boolean(state.onTrack),
|
|
155
|
+
inPitLane: false,
|
|
156
|
+
pitLanePart: null,
|
|
157
|
+
pitBoxId: null,
|
|
158
|
+
minimumSignedOffset,
|
|
159
|
+
maximumSignedOffset,
|
|
160
|
+
fullyOutsideWhiteLine: outside.fullyOutsideWhiteLine,
|
|
161
|
+
outsideSide: outside.outsideSide,
|
|
162
|
+
sampledStates: [state],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function patchPitOffsetRange(patch, centerState) {
|
|
167
|
+
const wheelCenterOffset =
|
|
168
|
+
(patch.center.x - centerState.x) * centerState.normalX +
|
|
169
|
+
(patch.center.y - centerState.y) * centerState.normalY;
|
|
170
|
+
const projectedHalfWidth =
|
|
171
|
+
Math.abs(patch.forward.x * centerState.normalX + patch.forward.y * centerState.normalY) * patch.halfLength +
|
|
172
|
+
Math.abs(patch.right.x * centerState.normalX + patch.right.y * centerState.normalY) * patch.halfWidth;
|
|
173
|
+
return {
|
|
174
|
+
minimum: wheelCenterOffset - projectedHalfWidth,
|
|
175
|
+
maximum: wheelCenterOffset + projectedHalfWidth,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function canUseAnalyticPitWheels(geometry, centerState) {
|
|
180
|
+
const roadWidth = Number(centerState?.pitLaneRoadWidth);
|
|
181
|
+
if (!centerState?.inPitLane || !Number.isFinite(roadWidth) || roadWidth <= 0) return false;
|
|
182
|
+
const halfRoadWidth = roadWidth / 2;
|
|
183
|
+
return geometry.contactPatches.every((patch) => {
|
|
184
|
+
const range = patchPitOffsetRange(patch, centerState);
|
|
185
|
+
return range.minimum >= -halfRoadWidth - 0.001 && range.maximum <= halfRoadWidth + 0.001;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function analyticPitWheelState(patch, centerState) {
|
|
190
|
+
const range = patchPitOffsetRange(patch, centerState);
|
|
191
|
+
return {
|
|
192
|
+
id: patch.id,
|
|
193
|
+
x: patch.center.x,
|
|
194
|
+
y: patch.center.y,
|
|
195
|
+
signedOffset: centerState.mainTrackSignedOffset ?? centerState.signedOffset,
|
|
196
|
+
crossTrackError: centerState.mainTrackCrossTrackError ?? centerState.crossTrackError,
|
|
197
|
+
surface: centerState.surface,
|
|
198
|
+
onTrack: true,
|
|
199
|
+
inPitLane: true,
|
|
200
|
+
pitLanePart: centerState.pitLanePart ?? null,
|
|
201
|
+
pitBoxId: centerState.pitBoxId ?? null,
|
|
202
|
+
minimumSignedOffset: centerState.mainTrackSignedOffset ?? centerState.signedOffset,
|
|
203
|
+
maximumSignedOffset: centerState.mainTrackSignedOffset ?? centerState.signedOffset,
|
|
204
|
+
pitLaneMinimumSignedOffset: range.minimum,
|
|
205
|
+
pitLaneMaximumSignedOffset: range.maximum,
|
|
206
|
+
fullyOutsideWhiteLine: false,
|
|
207
|
+
outsideSide: 0,
|
|
208
|
+
sampledStates: [centerState],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function getEffectiveSurface(wheels = []) {
|
|
213
|
+
if (!wheels.length) return 'track';
|
|
214
|
+
return wheels.reduce((surface, wheel) => (
|
|
215
|
+
priority(wheel.surface) > priority(surface) ? wheel.surface : surface
|
|
216
|
+
), 'track');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function isWholeCarOutsideTrackLimits(wheels = [], track, relaxedMargin = 0) {
|
|
220
|
+
if (!wheels.length || !track) {
|
|
221
|
+
return {
|
|
222
|
+
violating: false,
|
|
223
|
+
side: 0,
|
|
224
|
+
outsideBy: 0,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const trackLimit = track.width / 2 + relaxedMargin;
|
|
228
|
+
const right = wheels.every((wheel) => wheel.outsideSide === 1);
|
|
229
|
+
const left = wheels.every((wheel) => wheel.outsideSide === -1);
|
|
230
|
+
if (!right && !left) {
|
|
231
|
+
return {
|
|
232
|
+
violating: false,
|
|
233
|
+
side: 0,
|
|
234
|
+
outsideBy: 0,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const side = right ? 1 : -1;
|
|
238
|
+
const outsideBy = right
|
|
239
|
+
? Math.min(...wheels.map((wheel) => wheel.minimumSignedOffset)) - trackLimit
|
|
240
|
+
: -trackLimit - Math.max(...wheels.map((wheel) => wheel.maximumSignedOffset));
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
violating: outsideBy > 0,
|
|
244
|
+
side,
|
|
245
|
+
outsideBy: Math.max(0, outsideBy),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function calculateWheelSurfaceState({ car, track, centerState: providedCenterState = null }) {
|
|
250
|
+
const geometry = getVehicleGeometryState(car);
|
|
251
|
+
const trackLimit = track.width / 2;
|
|
252
|
+
const centerState = providedCenterState ?? nearestTrackState(track, car, car.progress);
|
|
253
|
+
const useAnalyticPitSampling = canUseAnalyticPitWheels(geometry, centerState);
|
|
254
|
+
const useFullSampling = !useAnalyticPitSampling && Boolean(centerState.inPitLane || isNearPitConnector(track, centerState));
|
|
255
|
+
const wheels = useAnalyticPitSampling
|
|
256
|
+
? geometry.contactPatches.map((patch) => analyticPitWheelState(patch, centerState))
|
|
257
|
+
: useFullSampling
|
|
258
|
+
? geometry.contactPatches.map((patch) => {
|
|
259
|
+
const sampleState = (point) => nearestTrackState(track, point, car.progress);
|
|
260
|
+
const sampledStates = patchSamples(patch).map(sampleState);
|
|
261
|
+
const state = worstState(sampledStates);
|
|
262
|
+
const offsets = sampledStates.map((sample) => sample.signedOffset);
|
|
263
|
+
const outside = wheelFullyOutside(sampledStates, trackLimit);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
id: patch.id,
|
|
267
|
+
x: patch.center.x,
|
|
268
|
+
y: patch.center.y,
|
|
269
|
+
signedOffset: state.signedOffset,
|
|
270
|
+
crossTrackError: state.crossTrackError,
|
|
271
|
+
surface: state.surface,
|
|
272
|
+
onTrack: Boolean(state.onTrack),
|
|
273
|
+
inPitLane: Boolean(state.inPitLane),
|
|
274
|
+
pitLanePart: state.pitLanePart ?? null,
|
|
275
|
+
pitBoxId: state.pitBoxId ?? null,
|
|
276
|
+
minimumSignedOffset: Math.min(...offsets),
|
|
277
|
+
maximumSignedOffset: Math.max(...offsets),
|
|
278
|
+
fullyOutsideWhiteLine: outside.fullyOutsideWhiteLine,
|
|
279
|
+
outsideSide: outside.outsideSide,
|
|
280
|
+
sampledStates,
|
|
281
|
+
};
|
|
282
|
+
})
|
|
283
|
+
: geometry.contactPatches.map((patch) => analyticWheelState(patch, centerState, track, trackLimit));
|
|
284
|
+
const effectiveSurface = getEffectiveSurface(wheels);
|
|
285
|
+
const representative = wheels.reduce((best, wheel) => (
|
|
286
|
+
Math.abs(wheel.signedOffset) > Math.abs(best.signedOffset) ? wheel : best
|
|
287
|
+
), wheels[0]);
|
|
288
|
+
const trackLimits = isWholeCarOutsideTrackLimits(wheels, track);
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
wheels,
|
|
292
|
+
effectiveSurface,
|
|
293
|
+
trackLimits,
|
|
294
|
+
sampleMode: useAnalyticPitSampling ? 'pit-analytic' : useFullSampling ? 'full' : 'analytic',
|
|
295
|
+
representativeState: representative ? {
|
|
296
|
+
...centerState,
|
|
297
|
+
surface: effectiveSurface,
|
|
298
|
+
onTrack: wheels.every((wheel) => wheel.onTrack || wheel.inPitLane),
|
|
299
|
+
inPitLane: wheels.every((wheel) => wheel.inPitLane),
|
|
300
|
+
signedOffset: representative.signedOffset,
|
|
301
|
+
crossTrackError: representative.crossTrackError,
|
|
302
|
+
} : nearestTrackState(track, car, car.progress),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function applyWheelSurfaceState(car, track, options = {}) {
|
|
307
|
+
const geometry = getVehicleGeometryState(car);
|
|
308
|
+
const cacheKey = {
|
|
309
|
+
track,
|
|
310
|
+
geometrySignature: geometry.signature,
|
|
311
|
+
centerSignature: centerStateSignature(options.centerState),
|
|
312
|
+
};
|
|
313
|
+
const cached = car.wheelSurfaceCache;
|
|
314
|
+
if (
|
|
315
|
+
cached?.track === cacheKey.track &&
|
|
316
|
+
cached.geometrySignature === cacheKey.geometrySignature &&
|
|
317
|
+
cached.centerSignature === cacheKey.centerSignature
|
|
318
|
+
) {
|
|
319
|
+
car.wheelStates = cached.result.wheels;
|
|
320
|
+
car.trackLimitState = cached.result.trackLimits;
|
|
321
|
+
car.trackState = cached.trackState;
|
|
322
|
+
return cached.result;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const result = calculateWheelSurfaceState({ car, track, centerState: options.centerState });
|
|
326
|
+
car.wheelStates = result.wheels;
|
|
327
|
+
car.trackLimitState = result.trackLimits;
|
|
328
|
+
car.trackState = {
|
|
329
|
+
...result.representativeState,
|
|
330
|
+
wheelSurface: result.effectiveSurface,
|
|
331
|
+
};
|
|
332
|
+
car.wheelSurfaceCache = {
|
|
333
|
+
...cacheKey,
|
|
334
|
+
result,
|
|
335
|
+
trackState: car.trackState,
|
|
336
|
+
};
|
|
337
|
+
return result;
|
|
338
|
+
}
|