@inventure71/paddockjs 4.1.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/INSTALL_AND_UPDATE.md +9 -2
- package/README.md +4 -2
- package/docs/architecture.md +2 -0
- package/docs/component_inventory.md +1 -1
- package/docs/getting-started.md +3 -0
- package/docs/index.md +4 -2
- package/docs/loading.md +141 -0
- package/docs/system_specs.md +4 -1
- package/docs/testing-reliability-guide.md +62 -0
- package/package.json +5 -7
- package/src/data/index.d.ts +64 -1
- package/src/environment/index.d.ts +75 -12
- package/src/index.d.ts +72 -6
- package/src/simulation/collisionGeometry.js +7 -9
- package/src/simulation/driver/driverController.js +2 -2
- package/src/simulation/driver/edgeRecovery.js +2 -2
- package/src/simulation/driver/racingControls.js +35 -25
- package/src/simulation/driver/racingLinePlan.js +6 -5
- package/src/simulation/driver/rejoinControls.js +3 -3
- package/src/simulation/driver/trafficScan.js +48 -22
- package/src/simulation/participants/participantInteractions.js +24 -15
- package/src/simulation/race/raceStep.js +19 -10
- package/src/simulation/race/safetyCar.js +2 -1
- package/src/simulation/rules/pitLaneSpeedingSteward.js +1 -1
- package/src/simulation/rules/rulesReview.js +19 -9
- package/src/simulation/rules/trackLimitsSteward.js +2 -2
- package/src/simulation/timing/sectorPerformance.js +11 -3
- package/src/simulation/track/spatialQueries.js +80 -11
- package/src/simulation/track/trackModel.js +10 -1
- package/src/simulation/vehicle/contactResolution.js +4 -4
- package/src/simulation/vehicle/vehicleGeometry.js +87 -49
- package/src/simulation/vehicle/vehiclePhysics.js +7 -2
- package/src/simulation/vehicle/vehicleSnapshots.js +5 -2
- package/src/simulation/vehicle/vehicleState.js +3 -2
- package/src/ui/raceCanvasTemplate.js +3 -2
- package/examples/train-basic-policy.mjs +0 -276
- package/examples/trainingData.mjs +0 -133
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { createPaddockEnvironment, createProgressReward } from '@inventure71/paddockjs/environment';
|
|
4
|
-
import { TRAINING_DEMO_DRIVERS, TRAINING_DEMO_ENTRIES } from './trainingData.mjs';
|
|
5
|
-
|
|
6
|
-
const DEFAULT_OPTIONS = Object.freeze({
|
|
7
|
-
generations: 4,
|
|
8
|
-
candidates: 5,
|
|
9
|
-
episodes: 1,
|
|
10
|
-
steps: 240,
|
|
11
|
-
frameSkip: 4,
|
|
12
|
-
seed: 71,
|
|
13
|
-
traffic: false,
|
|
14
|
-
evalOnly: false,
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
const INITIAL_POLICY = Object.freeze({
|
|
18
|
-
steeringHeading: -1.2,
|
|
19
|
-
steeringOffset: -0.35,
|
|
20
|
-
steeringRayBalance: 0.45,
|
|
21
|
-
throttleBias: 0.72,
|
|
22
|
-
throttleFrontClearance: 0.28,
|
|
23
|
-
throttleSpeedDamping: 0.18,
|
|
24
|
-
brakeBias: 0,
|
|
25
|
-
brakeFrontDanger: 0.65,
|
|
26
|
-
brakeOffTrack: 0.2,
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
const POLICY_KEYS = Object.keys(INITIAL_POLICY);
|
|
30
|
-
|
|
31
|
-
main();
|
|
32
|
-
|
|
33
|
-
function main() {
|
|
34
|
-
const options = parseCliArgs(process.argv.slice(2));
|
|
35
|
-
if (options.help) {
|
|
36
|
-
printHelp();
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const driverId = TRAINING_DEMO_DRIVERS[0].id;
|
|
41
|
-
const rng = createSeededRandom(options.seed);
|
|
42
|
-
const reward = createProgressReward();
|
|
43
|
-
const evaluate = (policy, seedOffset = 0) => evaluatePolicy({
|
|
44
|
-
policy,
|
|
45
|
-
driverId,
|
|
46
|
-
reward,
|
|
47
|
-
options,
|
|
48
|
-
seedOffset,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
if (options.evalOnly) {
|
|
52
|
-
const baseline = evaluate(INITIAL_POLICY);
|
|
53
|
-
printSummary({ options, driverId, bestPolicy: INITIAL_POLICY, best: baseline });
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
let bestPolicy = { ...INITIAL_POLICY };
|
|
58
|
-
let best = evaluate(bestPolicy);
|
|
59
|
-
|
|
60
|
-
console.log(formatGenerationLine(0, best));
|
|
61
|
-
|
|
62
|
-
for (let generation = 1; generation <= options.generations; generation += 1) {
|
|
63
|
-
const mutationScale = 0.45 / Math.sqrt(generation);
|
|
64
|
-
const candidates = [{ policy: bestPolicy, scoreSeedOffset: generation * 1000 }];
|
|
65
|
-
|
|
66
|
-
for (let index = 1; index < options.candidates; index += 1) {
|
|
67
|
-
candidates.push({
|
|
68
|
-
policy: mutatePolicy(bestPolicy, rng, mutationScale),
|
|
69
|
-
scoreSeedOffset: generation * 1000 + index * 100,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
for (const candidate of candidates) {
|
|
74
|
-
const score = evaluate(candidate.policy, candidate.scoreSeedOffset);
|
|
75
|
-
if (score.averageReward > best.averageReward) {
|
|
76
|
-
best = score;
|
|
77
|
-
bestPolicy = candidate.policy;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
console.log(formatGenerationLine(generation, best));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
printSummary({ options, driverId, bestPolicy, best });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function evaluatePolicy({ policy, driverId, reward, options, seedOffset = 0 }) {
|
|
88
|
-
let totalReward = 0;
|
|
89
|
-
let totalDistanceMeters = 0;
|
|
90
|
-
let totalCollisions = 0;
|
|
91
|
-
let totalOffTrackSteps = 0;
|
|
92
|
-
let completedEpisodes = 0;
|
|
93
|
-
|
|
94
|
-
for (let episode = 0; episode < options.episodes; episode += 1) {
|
|
95
|
-
const env = createTrainingEnvironment({ driverId, reward, options, episode, seedOffset });
|
|
96
|
-
let result = env.reset();
|
|
97
|
-
let episodeReward = 0;
|
|
98
|
-
|
|
99
|
-
for (let step = 0; step < options.steps && !result.done; step += 1) {
|
|
100
|
-
const observation = result.observation[driverId];
|
|
101
|
-
const action = chooseAction(policy, observation);
|
|
102
|
-
result = env.step({ [driverId]: action });
|
|
103
|
-
episodeReward += result.reward?.[driverId] ?? 0;
|
|
104
|
-
totalCollisions += result.events.filter((event) => event.type === 'collision').length;
|
|
105
|
-
if (result.observation[driverId]?.object?.self?.onTrack === false) totalOffTrackSteps += 1;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const car = result.state.snapshot.cars.find((entry) => entry.id === driverId);
|
|
109
|
-
totalReward += episodeReward;
|
|
110
|
-
totalDistanceMeters += Number(car?.distanceMeters ?? 0);
|
|
111
|
-
completedEpisodes += 1;
|
|
112
|
-
env.destroy();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
averageReward: totalReward / Math.max(1, completedEpisodes),
|
|
117
|
-
averageDistanceMeters: totalDistanceMeters / Math.max(1, completedEpisodes),
|
|
118
|
-
collisions: totalCollisions,
|
|
119
|
-
offTrackSteps: totalOffTrackSteps,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function createTrainingEnvironment({ driverId, reward, options, episode, seedOffset }) {
|
|
124
|
-
const drivers = options.traffic ? TRAINING_DEMO_DRIVERS.slice(0, 8) : TRAINING_DEMO_DRIVERS.slice(0, 1);
|
|
125
|
-
return createPaddockEnvironment({
|
|
126
|
-
drivers,
|
|
127
|
-
entries: TRAINING_DEMO_ENTRIES,
|
|
128
|
-
controlledDrivers: [driverId],
|
|
129
|
-
seed: options.seed + seedOffset + episode,
|
|
130
|
-
trackSeed: options.seed + 2026 + seedOffset + episode,
|
|
131
|
-
totalLaps: 3,
|
|
132
|
-
frameSkip: options.frameSkip,
|
|
133
|
-
scenario: {
|
|
134
|
-
participants: options.traffic ? 'all' : 'controlled-only',
|
|
135
|
-
nonControlled: 'ai',
|
|
136
|
-
},
|
|
137
|
-
episode: {
|
|
138
|
-
maxSteps: options.steps,
|
|
139
|
-
endOnRaceFinish: true,
|
|
140
|
-
},
|
|
141
|
-
rules: {
|
|
142
|
-
standingStart: false,
|
|
143
|
-
},
|
|
144
|
-
reward,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function chooseAction(policy, observation) {
|
|
149
|
-
const self = observation.object.self;
|
|
150
|
-
const frontRay = getRayDistance(observation, 0);
|
|
151
|
-
const leftRay = getRayDistance(observation, -60);
|
|
152
|
-
const rightRay = getRayDistance(observation, 60);
|
|
153
|
-
const frontDanger = clamp01((35 - frontRay) / 35);
|
|
154
|
-
const rayBalanceMeters = rightRay - leftRay;
|
|
155
|
-
|
|
156
|
-
const steering = clamp(
|
|
157
|
-
policy.steeringHeading * self.trackHeadingErrorRadians +
|
|
158
|
-
policy.steeringOffset * (self.trackOffsetMeters / 12) +
|
|
159
|
-
policy.steeringRayBalance * (rayBalanceMeters / 120),
|
|
160
|
-
-1,
|
|
161
|
-
1,
|
|
162
|
-
);
|
|
163
|
-
const throttle = clamp01(
|
|
164
|
-
policy.throttleBias +
|
|
165
|
-
policy.throttleFrontClearance * (frontRay / 120) -
|
|
166
|
-
policy.throttleSpeedDamping * (self.speedKph / 320) -
|
|
167
|
-
(self.onTrack ? 0 : 0.35),
|
|
168
|
-
);
|
|
169
|
-
const brake = clamp01(
|
|
170
|
-
policy.brakeBias +
|
|
171
|
-
policy.brakeFrontDanger * frontDanger +
|
|
172
|
-
(self.onTrack ? 0 : policy.brakeOffTrack),
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
return { steering, throttle, brake };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function getRayDistance(observation, angleDegrees) {
|
|
179
|
-
const ray = observation.object.rays.find((entry) => entry.angleDegrees === angleDegrees);
|
|
180
|
-
return Number(ray?.track?.distanceMeters ?? ray?.lengthMeters ?? 120);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function mutatePolicy(policy, rng, scale) {
|
|
184
|
-
return Object.fromEntries(POLICY_KEYS.map((key) => [
|
|
185
|
-
key,
|
|
186
|
-
policy[key] + randomNormal(rng) * scale,
|
|
187
|
-
]));
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function parseCliArgs(argv) {
|
|
191
|
-
const options = { ...DEFAULT_OPTIONS };
|
|
192
|
-
|
|
193
|
-
for (const arg of argv) {
|
|
194
|
-
const [name, rawValue] = arg.split('=');
|
|
195
|
-
if (name === '--help' || name === '-h') options.help = true;
|
|
196
|
-
else if (name === '--eval-only') options.evalOnly = true;
|
|
197
|
-
else if (name === '--traffic') options.traffic = true;
|
|
198
|
-
else if (name === '--no-traffic') options.traffic = false;
|
|
199
|
-
else if (name === '--generations') options.generations = parsePositiveInt(rawValue, name);
|
|
200
|
-
else if (name === '--candidates') options.candidates = parsePositiveInt(rawValue, name);
|
|
201
|
-
else if (name === '--episodes') options.episodes = parsePositiveInt(rawValue, name);
|
|
202
|
-
else if (name === '--steps') options.steps = parsePositiveInt(rawValue, name);
|
|
203
|
-
else if (name === '--frame-skip') options.frameSkip = parsePositiveInt(rawValue, name);
|
|
204
|
-
else if (name === '--seed') options.seed = parsePositiveInt(rawValue, name);
|
|
205
|
-
else throw new Error(`Unknown option: ${name}`);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return options;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function parsePositiveInt(value, optionName) {
|
|
212
|
-
const parsed = Number.parseInt(value, 10);
|
|
213
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
214
|
-
throw new Error(`${optionName} requires a positive integer value.`);
|
|
215
|
-
}
|
|
216
|
-
return parsed;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function createSeededRandom(seed) {
|
|
220
|
-
let state = seed >>> 0;
|
|
221
|
-
return function random() {
|
|
222
|
-
state += 0x6D2B79F5;
|
|
223
|
-
let value = state;
|
|
224
|
-
value = Math.imul(value ^ (value >>> 15), value | 1);
|
|
225
|
-
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
|
|
226
|
-
return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function randomNormal(rng) {
|
|
231
|
-
const a = Math.max(Number.EPSILON, rng());
|
|
232
|
-
const b = Math.max(Number.EPSILON, rng());
|
|
233
|
-
return Math.sqrt(-2 * Math.log(a)) * Math.cos(2 * Math.PI * b);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function clamp(value, min, max) {
|
|
237
|
-
return Math.min(max, Math.max(min, value));
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function clamp01(value) {
|
|
241
|
-
return clamp(value, 0, 1);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function formatGenerationLine(generation, score) {
|
|
245
|
-
return [
|
|
246
|
-
`generation=${generation}`,
|
|
247
|
-
`reward=${score.averageReward.toFixed(2)}`,
|
|
248
|
-
`distanceMeters=${score.averageDistanceMeters.toFixed(1)}`,
|
|
249
|
-
`collisions=${score.collisions}`,
|
|
250
|
-
`offTrackSteps=${score.offTrackSteps}`,
|
|
251
|
-
].join(' ');
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function printSummary({ options, driverId, bestPolicy, best }) {
|
|
255
|
-
console.log(JSON.stringify({
|
|
256
|
-
driverId,
|
|
257
|
-
options,
|
|
258
|
-
best,
|
|
259
|
-
bestPolicy,
|
|
260
|
-
}, null, 2));
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function printHelp() {
|
|
264
|
-
console.log(`Usage: node examples/train-basic-policy.mjs [options]
|
|
265
|
-
|
|
266
|
-
Options:
|
|
267
|
-
--generations=N Random-search generations. Default: ${DEFAULT_OPTIONS.generations}
|
|
268
|
-
--candidates=N Candidate policies per generation. Default: ${DEFAULT_OPTIONS.candidates}
|
|
269
|
-
--episodes=N Episodes per policy evaluation. Default: ${DEFAULT_OPTIONS.episodes}
|
|
270
|
-
--steps=N Maximum manual environment steps per episode. Default: ${DEFAULT_OPTIONS.steps}
|
|
271
|
-
--frame-skip=N Simulator frames advanced by each step(action). Default: ${DEFAULT_OPTIONS.frameSkip}
|
|
272
|
-
--seed=N Deterministic random seed. Default: ${DEFAULT_OPTIONS.seed}
|
|
273
|
-
--traffic Keep built-in AI traffic on track during training.
|
|
274
|
-
--eval-only Evaluate the starter policy without training.
|
|
275
|
-
`);
|
|
276
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
export const TRAINING_DEMO_DRIVERS = [
|
|
2
|
-
{
|
|
3
|
-
id: 'budget',
|
|
4
|
-
code: 'BUD',
|
|
5
|
-
icon: 'BB',
|
|
6
|
-
name: 'Budget Buddy',
|
|
7
|
-
color: '#ff2d55',
|
|
8
|
-
tire: 'M',
|
|
9
|
-
raceData: ['AI finance coach', 'Python + LLM', 'Budget guardrails'],
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
id: 'noir',
|
|
13
|
-
code: 'NOI',
|
|
14
|
-
icon: 'NN',
|
|
15
|
-
name: 'Neural Noir',
|
|
16
|
-
color: '#ffb000',
|
|
17
|
-
tire: 'M',
|
|
18
|
-
raceData: ['Procedural mystery', 'Gemini storytelling', 'Unreal loop'],
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
id: 'vinyl',
|
|
22
|
-
code: 'HOL',
|
|
23
|
-
icon: 'HV',
|
|
24
|
-
name: 'HoloVinyl',
|
|
25
|
-
color: '#00c2a8',
|
|
26
|
-
tire: 'H',
|
|
27
|
-
raceData: ['Touchless DJ control', 'Computer vision', 'Hand tracking'],
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
id: 'drsorriso',
|
|
31
|
-
code: 'DRS',
|
|
32
|
-
icon: 'DR',
|
|
33
|
-
name: 'DrSorrisoDonations',
|
|
34
|
-
color: '#e10600',
|
|
35
|
-
tire: 'S',
|
|
36
|
-
raceData: ['Donation intake', 'Database workflow', 'Automation tool'],
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
id: 'victoria',
|
|
40
|
-
code: 'VIC',
|
|
41
|
-
icon: 'VI',
|
|
42
|
-
name: 'VictorIA',
|
|
43
|
-
color: '#00a3ff',
|
|
44
|
-
tire: 'M',
|
|
45
|
-
raceData: ['Game AI sandbox', 'Robotics ideas', 'Vision systems'],
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
id: 'reminderz',
|
|
49
|
-
code: 'REM',
|
|
50
|
-
icon: 'RZ',
|
|
51
|
-
name: 'ReminderZ',
|
|
52
|
-
color: '#a3e635',
|
|
53
|
-
tire: 'H',
|
|
54
|
-
raceData: ['Knowledge workflow', 'Personal graph', 'Capture tool'],
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
id: 'clipclop',
|
|
58
|
-
code: 'CLP',
|
|
59
|
-
icon: 'CC',
|
|
60
|
-
name: 'ClipClop',
|
|
61
|
-
color: '#f97316',
|
|
62
|
-
tire: 'S',
|
|
63
|
-
raceData: ['Mac + Android sync', 'Local clipboard', 'Productivity app'],
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
id: 'core',
|
|
67
|
-
code: 'COR',
|
|
68
|
-
icon: 'AI',
|
|
69
|
-
name: 'Core Conflict',
|
|
70
|
-
color: '#38bdf8',
|
|
71
|
-
tire: 'M',
|
|
72
|
-
raceData: ['Runtime AI agents', 'Multiplayer state', 'Game systems'],
|
|
73
|
-
},
|
|
74
|
-
];
|
|
75
|
-
|
|
76
|
-
export const TRAINING_DEMO_ENTRIES = [
|
|
77
|
-
{
|
|
78
|
-
driverId: 'budget',
|
|
79
|
-
driverNumber: 71,
|
|
80
|
-
timingName: 'Budget',
|
|
81
|
-
driver: { pace: 52, racecraft: 74, aggression: 38, riskTolerance: 47, patience: 81, consistency: 86 },
|
|
82
|
-
vehicle: { id: 'budget-bb01', name: 'BB-01 Ledger', power: 48, braking: 72, aero: 55, dragEfficiency: 66, mechanicalGrip: 63, weightControl: 58, tireCare: 82 },
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
driverId: 'noir',
|
|
86
|
-
driverNumber: 13,
|
|
87
|
-
timingName: 'Noir',
|
|
88
|
-
driver: { pace: 68, racecraft: 83, aggression: 61, riskTolerance: 72, patience: 57, consistency: 64 },
|
|
89
|
-
vehicle: { id: 'noir-nn13', name: 'NN-13 Shadow', power: 64, braking: 60, aero: 73, dragEfficiency: 57, mechanicalGrip: 69, weightControl: 54, tireCare: 51 },
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
driverId: 'vinyl',
|
|
93
|
-
driverNumber: 33,
|
|
94
|
-
timingName: 'HoloVinyl',
|
|
95
|
-
driver: { pace: 71, racecraft: 66, aggression: 52, riskTolerance: 59, patience: 62, consistency: 58 },
|
|
96
|
-
vehicle: { id: 'vinyl-hv33', name: 'HV-33 Groove', power: 70, braking: 49, aero: 58, dragEfficiency: 74, mechanicalGrip: 52, weightControl: 68, tireCare: 46 },
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
driverId: 'drsorriso',
|
|
100
|
-
driverNumber: 55,
|
|
101
|
-
timingName: 'DrSorriso',
|
|
102
|
-
driver: { pace: 46, racecraft: 69, aggression: 44, riskTolerance: 43, patience: 76, consistency: 73 },
|
|
103
|
-
vehicle: { id: 'sorriso-ds55', name: 'DS-55 Intake', power: 54, braking: 67, aero: 62, dragEfficiency: 49, mechanicalGrip: 71, weightControl: 45, tireCare: 69 },
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
driverId: 'victoria',
|
|
107
|
-
driverNumber: 11,
|
|
108
|
-
timingName: 'VictorIA',
|
|
109
|
-
driver: { pace: 78, racecraft: 79, aggression: 58, riskTolerance: 65, patience: 55, consistency: 68 },
|
|
110
|
-
vehicle: { id: 'victoria-vi11', name: 'VI-11 Vector', power: 76, braking: 58, aero: 65, dragEfficiency: 63, mechanicalGrip: 61, weightControl: 60, tireCare: 48 },
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
driverId: 'reminderz',
|
|
114
|
-
driverNumber: 7,
|
|
115
|
-
timingName: 'ReminderZ',
|
|
116
|
-
driver: { pace: 41, racecraft: 61, aggression: 34, riskTolerance: 39, patience: 85, consistency: 80 },
|
|
117
|
-
vehicle: { id: 'reminderz-rz07', name: 'RZ-07 Recall', power: 42, braking: 76, aero: 50, dragEfficiency: 72, mechanicalGrip: 58, weightControl: 63, tireCare: 88 },
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
driverId: 'clipclop',
|
|
121
|
-
driverNumber: 14,
|
|
122
|
-
timingName: 'ClipClop',
|
|
123
|
-
driver: { pace: 73, racecraft: 88, aggression: 76, riskTolerance: 78, patience: 48, consistency: 61 },
|
|
124
|
-
vehicle: { id: 'clipclop-cc14', name: 'CC-14 Sync', power: 68, braking: 63, aero: 59, dragEfficiency: 61, mechanicalGrip: 78, weightControl: 56, tireCare: 44 },
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
driverId: 'core',
|
|
128
|
-
driverNumber: 91,
|
|
129
|
-
timingName: 'Core',
|
|
130
|
-
driver: { pace: 82, racecraft: 85, aggression: 67, riskTolerance: 74, patience: 53, consistency: 72 },
|
|
131
|
-
vehicle: { id: 'core-ai91', name: 'AI-91 Core', power: 80, braking: 61, aero: 72, dragEfficiency: 59, mechanicalGrip: 75, weightControl: 51, tireCare: 50 },
|
|
132
|
-
},
|
|
133
|
-
];
|