makefuture / scripts / validate-nordschleife-data.mjs
388 words
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const DATA_DIR = path.join(ROOT, 'games/nordschleife/data');

function readJson(fileName) {
    return JSON.parse(fs.readFileSync(path.join(DATA_DIR, fileName), 'utf8'));
}

function fail(message) {
    console.error(`Nordschleife validation failed: ${message}`);
    process.exit(1);
}

function distance2d(a, b) {
    return Math.hypot(b.x - a.x, b.z - a.z);
}

function validateTrack(track) {
    if (track.version !== 2) fail('track.json must be version 2');
    if (track.lengthM < 20500 || track.lengthM > 21100) fail(`unexpected length ${track.lengthM}`);
    if (!Array.isArray(track.samples) || track.samples.length < 3500) fail('not enough track samples');
    if (!track.validation?.ok) fail('track validation metadata is not ok');

    let maxSpacing = 0;
    let maxTurnDeg = 0;
    for (let i = 0; i < track.samples.length; i++) {
        const a = track.samples[(i - 1 + track.samples.length) % track.samples.length];
        const b = track.samples[i];
        const c = track.samples[(i + 1) % track.samples.length];
        for (const key of ['x', 'y', 'z', 't', 'distanceM']) {
            if (!Number.isFinite(b[key])) fail(`sample ${i} has invalid ${key}`);
        }
        maxSpacing = Math.max(maxSpacing, distance2d(b, c));
        const v1 = { x: b.x - a.x, z: b.z - a.z };
        const v2 = { x: c.x - b.x, z: c.z - b.z };
        const n1 = Math.hypot(v1.x, v1.z);
        const n2 = Math.hypot(v2.x, v2.z);
        if (n1 > 0 && n2 > 0) {
            const dot = Math.max(-1, Math.min(1, (v1.x * v2.x + v1.z * v2.z) / (n1 * n2)));
            maxTurnDeg = Math.max(maxTurnDeg, Math.acos(dot) * 180 / Math.PI);
        }
    }
    if (maxSpacing > 6) fail(`sample spacing too large: ${maxSpacing.toFixed(2)}m`);
    if (maxTurnDeg > 25) fail(`turn spike too sharp: ${maxTurnDeg.toFixed(2)}deg`);
}

function validateSectors(sectors) {
    if (!Array.isArray(sectors.sectors) || sectors.sectors.length < 30) fail('not enough sectors');
    let prev = 0;
    for (const sector of sectors.sectors) {
        if (sector.t0 < prev - 1e-6 || sector.t1 <= sector.t0) fail(`bad sector range for ${sector.name}`);
        prev = sector.t1;
    }
    if (Math.abs(sectors.sectors[0].t0) > 1e-6) fail('first sector must start at 0');
    if (Math.abs(sectors.sectors[sectors.sectors.length - 1].t1 - 1) > 1e-6) fail('last sector must end at 1');
}

function validateCheckpoints(checkpoints) {
    if (!Array.isArray(checkpoints.checkpoints) || checkpoints.checkpoints.length !== 20) fail('expected 20 checkpoints');
    for (let i = 0; i < checkpoints.checkpoints.length; i++) {
        const cp = checkpoints.checkpoints[i];
        if (cp.index !== i) fail(`checkpoint ${i} has wrong index`);
        if (!Number.isFinite(cp.x) || !Number.isFinite(cp.y) || !Number.isFinite(cp.z)) fail(`checkpoint ${i} has invalid coordinates`);
        if (i > 0 && cp.t <= checkpoints.checkpoints[i - 1].t) fail(`checkpoint ${i} is out of order`);
    }
}

validateTrack(readJson('track.json'));
validateSectors(readJson('sectors.json'));
validateCheckpoints(readJson('checkpoints.json'));
console.log('Nordschleife data validation passed');

static mirror of HEAD · about · clone: git clone https://git.makefuture.ro/makefuture.git