Repository navigation
Expand file tree
/
Copy pathcheck_world.mjs
More file actions
1332 lines (1210 loc) · 72 KB
/
Copy pathcheck_world.mjs
File metadata and controls
1332 lines (1210 loc) · 72 KB
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env node
/**
* Worldloop structured verification harness.
*
* Runs the REAL src systems in Node against the real `three` package —
* instantiates every system, calls their update() loops, asserts API behavior,
* and emits machine-readable JSON so the agent loop can discover the surface,
* plan, and consume results without guessing.
*
* Usage: node check_world.mjs
* Exit: 0 if all checks pass, 1 otherwise.
*/
import * as THREE from 'three';
import { runHeadlessSmoke } from './tools/ci/headless_cdp.mjs';
import { createWorld, createCityChunk } from './src/world.js';
import { TrafficSystem } from './src/traffic.js';
import { WeatherSystem } from './src/weather.js';
import { PedestrianSystem } from './src/pedestrians.js';
import { ParkingSystem } from './src/parking.js';
import { AirplaneSystem } from './src/airplanes.js';
import { EffectSystem } from './src/effects.js';
import { TrafficLightSystem } from './src/traffic_lights.js';
import { SpatialGrid, ChunkManager } from './src/chunk_manager.js';
import { Player } from './src/player.js';
import { deformMesh } from './src/deformation.js';
import { SimplexNoise, resetNoise } from './src/noise.js';
import { setSeed, getSeed, DEFAULT_SEED, RNG } from './src/core/rng.js'; // [AAA-03] seeded deterministic RNG
import { FrameBudgetTelemetry } from './src/telemetry.js';
import { RoadGraph } from './src/road_graph.js'; // [NEW] road-network graph
import { EmergencySystem } from './src/emergency.js';
import { ConstructionSystem } from './src/construction.js';
import { FrameLoop } from './src/loop.js'; // [AAA-02] frame loop hygiene
import { PostProcessingPipeline } from './src/effects.js'; // [NEW] post-processing pipeline // [NEW] Gap D — emergency response
import { Minimap, DISTRICT_PALETTE, BIOMES } from './src/minimap.js'; // [NEW] Round 9 — minimap / district-label HUD
import { Planet, BIOME, BIOME_TABLE, CITY_KINDS, planet, setPlanet } from './src/planet.js'; // [NEW] PLANET-01 — seeded planetary surface model
import fs from 'node:fs';
const VERSION = '6.7.0';
// --- Machine-readable output guard ---
// The systems under test log to console.log during the checks; that would
// pollute the JSON report on stdout. Redirect module debug output to stderr
// so stdout carries ONLY the report + summary.
const origConsoleLog = console.log;
console.log = (...a) => process.stderr.write(a.map((x) => String(x)).join(' ') + '\n');
const checks = [];
let failed = 0;
function check(name, pass, detail) {
checks.push({ name, pass: !!pass, detail: detail || '' });
if (!pass) failed++;
return pass;
}
// --- Node DOM stub (Player / PointerLockControls need browser DOM) ---
function makeEl() {
return {
style: {}, appendChild() {}, textContent: '',
addEventListener() {}, removeEventListener() {}, setAttribute() {}, className: ''
};
}
globalThis.document = {
createElement() { return makeEl(); },
body: { appendChild() {} },
addEventListener() {}, removeEventListener() {},
pointerLockElement: null
};
function makeDomElement() {
const doc = {
addEventListener() {}, removeEventListener() {},
createElement() { return { style: {}, appendChild() {}, textContent: '' }; },
body: { appendChild() {} },
pointerLockElement: null,
addEventListener: () => {}
};
const el = {
ownerDocument: doc,
requestPointerLock() {}, exitPointerLock() {},
addEventListener() {}, removeEventListener() {}
};
return el;
}
// --- Shared scene + systems ---
const scene = new THREE.Scene();
const world = await createWorld(scene);
const cam = new THREE.PerspectiveCamera();
const dom = makeDomElement();
const traffic = new TrafficSystem(scene, world.citySize, world.blockSize, world.roadWidth);
const parking = new ParkingSystem(scene, world.citySize, world.blockSize, world.roadWidth);
const pedestrians = new PedestrianSystem(scene, world.citySize, world.blockSize, world.roadWidth);
const weather = new WeatherSystem(scene, world.directionalLight, world.ambientLight, world.materials);
const airplanes = new AirplaneSystem(scene, world.citySize);
const effects = new EffectSystem(scene);
const trafficLights = new TrafficLightSystem(scene, world.roadWidth, world.blockSize);
const emergency = new EmergencySystem(scene, world.roadWidth, world.blockSize); // Gap D
emergency.setEffects(effects);
const construction = new ConstructionSystem(scene);
const chunkManager = new ChunkManager(scene, null, world, traffic, parking, pedestrians, trafficLights, construction);
// ---- 0. Planet — the seeded surface model underneath every chunk ----------
const PLANET_CS = world.blockSize + world.roadWidth;
const P = new Planet({ seed: getSeed(), chunkSize: PLANET_CS });
const P_TWIN = new Planet({ seed: getSeed(), chunkSize: PLANET_CS });
const P_OTHER = new Planet({ seed: (getSeed() ^ 0x5f5f5f) >>> 0 || 4242, chunkSize: PLANET_CS });
check('planet exports Planet/BIOME/BIOME_TABLE/CITY_KINDS',
typeof Planet === 'function' && typeof BIOME === 'object' && typeof BIOME_TABLE === 'object' && typeof CITY_KINDS === 'object',
Object.keys(BIOME).length + ' biomes, ' + Object.keys(CITY_KINDS).length + ' city kinds');
check('planet fields are finite over a wide sample', (() => {
for (let i = -12; i <= 12; i++) {
for (let j = -12; j <= 12; j++) {
const x = i * PLANET_CS, z = j * PLANET_CS;
const e = P.surfaceHeight(x, z), m = P.moisture(x, z), t = P.temperature(x, z);
if (!Number.isFinite(e) || !Number.isFinite(m) || !Number.isFinite(t)) return false;
if (m < 0 || m > 1 || t < 0 || t > 1) return false;
}
}
return true;
})(), 'elevation/moisture/temperature bounded');
const PLANET_STATS = P.stats(-30, -30, 30, 30);
check('planet produces real biome variety (>8 classes)',
PLANET_STATS.distinctBiomes > 8,
PLANET_STATS.distinctBiomes + ' biomes: ' + Object.keys(PLANET_STATS.biomes).join(','));
check('planet has ocean, inland lakes AND rivers', (() => {
let ocean = 0, lake = 0, river = 0;
for (let i = 0; i < 70; i++) {
for (let j = 0; j < 70; j++) {
const x = (i - 35) * PLANET_CS, z = (j - 35) * PLANET_CS;
if (!P.isWater(x, z)) continue;
if (P.isRiver(x, z)) river++;
else if (P.isLake(x, z)) lake++;
else ocean++;
}
}
return ocean > 20 && lake > 5 && river > 0;
})(), 'ocean / lake / river cells present');
check('planet water fraction is Earth-like (5–45%)',
PLANET_STATS.waterFraction > 0.05 && PLANET_STATS.waterFraction < 0.45,
'waterFraction=' + PLANET_STATS.waterFraction);
check('planet classifies standing water as lake vs sea', (() => {
let lake = 0, sea = 0;
for (let i = 0; i < 70; i++) {
for (let j = 0; j < 70; j++) {
const x = (i - 35) * PLANET_CS, z = (j - 35) * PLANET_CS;
const b = P.biomeAt(x, z);
if (b === BIOME.LAKE) lake++;
else if (b === BIOME.OCEAN || b === BIOME.COAST) sea++;
}
}
return lake > 3 && sea > 20;
})(), 'lake/sea split from the land-ring test');
check('planet is deterministic (same seed -> same classification)', (() => {
for (let i = -8; i <= 8; i++) {
for (let j = -8; j <= 8; j++) {
if (P.dominantBiomeInChunk(i, j) !== P_TWIN.dominantBiomeInChunk(i, j)) return false;
const a = P.classifyChunk(i, j), b = P_TWIN.classifyChunk(i, j);
if (a.type !== b.type || a.biome !== b.biome || (a.city && a.city.id) !== (b.city && b.city.id)) return false;
}
}
return true;
})(), '96 chunks identical across instances');
check('planet seed actually changes the world', (() => {
for (let i = 0; i < 12; i++) {
for (let j = 0; j < 12; j++) {
if (P.dominantBiomeInChunk(i, j) !== P_OTHER.dominantBiomeInChunk(i, j)) return true;
}
}
return false;
})(), 'seed ' + P.seed + ' vs ' + P_OTHER.seed);
check('planet every biome has a render/ecology entry',
Object.values(BIOME).every(b => BIOME_TABLE[b] && Number.isInteger(BIOME_TABLE[b].ground) && Array.isArray(BIOME_TABLE[b].flora)),
Object.keys(BIOME_TABLE).length + ' table rows');
check('planet unknown biome falls back to grassland',
Planet.biomeInfo('not-a-biome') === BIOME_TABLE[BIOME.GRASSLAND], 'fallback ok');
const SPAWN_CITY = P.cityAtChunk(0, 0);
check('planet puts a city at the origin chunk (first landfall)',
!!SPAWN_CITY && SPAWN_CITY.kind === 'metropolis' && SPAWN_CITY.isSpawn === true,
SPAWN_CITY ? SPAWN_CITY.name + ' / ' + SPAWN_CITY.label : 'none');
const CITY_SET = P.citiesInChunkRange(-96, -96, 96, 96);
check('planet scatters cities across the wilds (infinite settlements)',
CITY_SET.length >= 8,
CITY_SET.length + ' cities within ±9.2 km of origin');
const CITY_KIND_SEEN = new Set(CITY_SET.map(c => c.kind));
check('cities differ from each other (multiple kinds)',
CITY_KIND_SEEN.size >= 3,
[...CITY_KIND_SEEN].join(','));
check('each city carries a full architecture profile', CITY_SET.length > 0 && CITY_SET.every(c => {
const prof = CITY_KINDS[c.kind];
return prof && c.population > 0 && typeof c.name === 'string' && c.name.length > 2 &&
c.density === prof.density && Array.isArray(c.height) && c.height.length === 2 &&
typeof c.styleMix === 'object' && c.r > 0 && Number.isInteger(c.cx) && Number.isInteger(c.cz);
}), 'profiles resolve against CITY_KINDS');
check('cities sit on dry land (sites were walked out of the water)', (() => {
const dry = CITY_SET.filter(c => !P.isWater(c.x, c.z));
return dry.length >= CITY_SET.length * 0.9;
})(), `${CITY_SET.filter(c => !P.isWater(c.x, c.z)).length}/${CITY_SET.length} dry sites`);
check('chunk classification matches the model (city / road / nature)', (() => {
const inCity = P.classifyChunk(SPAWN_CITY.cx, SPAWN_CITY.cz);
if (inCity.type !== 'city' || inCity.city.id !== SPAWN_CITY.id) return false;
// far from every city and every leg -> wilderness
let wild = false;
for (let i = 40; i < 46 && !wild; i++) {
for (let j = 40; j < 46 && !wild; j++) {
const c = P.classifyChunk(i, j);
if (c.type === 'nature') wild = true;
}
}
let road = false;
for (const leg of P.legsForCity(SPAWN_CITY)) {
const step = Math.sign(leg.to - leg.from);
const mid = Math.round((leg.from + leg.to) / 2);
const probe = leg.axis === 'x' ? P.classifyChunk(mid, leg.fixed) : P.classifyChunk(leg.fixed, mid);
if (probe.type === 'road') { road = true; break; }
}
return wild && road;
})(), 'city at origin, wilderness and highway legs found');
check('inter-city roads are chunk-aligned (integer grid rows)', (() => {
for (const c of CITY_SET.slice(0, 12)) {
for (const leg of P.legsForCity(c)) {
if (!Number.isInteger(leg.fixed) || !Number.isInteger(leg.from) || !Number.isInteger(leg.to)) return false;
if (leg.axis !== 'x' && leg.axis !== 'z') return false;
}
}
return true;
})(), 'legs snap to chunk rows/columns');
check('city cores are graded flat (groundHeight 0 under the streets)', (() => {
for (const [dx, dz] of [[0, 0], [20, -14], [-31, 27]]) {
if (Math.abs(P.groundHeight(SPAWN_CITY.x + dx, SPAWN_CITY.z + dz)) > 1e-6) return false;
}
return true;
})(), 'flat pad under the city, relief kept in the wilds');
check('planet names places deterministically (city + region)', (() => {
const a = P.cityAtChunk(0, 0).name, b = P_TWIN.cityAtChunk(0, 0).name;
const r = P.regionAt(1234.5, -987.25);
return a === b && a.length > 3 && typeof r === 'string' && r.length > 3;
})(), SPAWN_CITY.name + ' · ' + P.regionAt(1234.5, -987.25));
check('planet shared instance is seed-keyed', planet({ chunkSize: PLANET_CS }) instanceof Planet && planet() === planet(),
'planet() memoized, seed=' + planet().seed);
check('planet setPlanet installs a live instance (seed change at runtime)', (() => {
const prev = planet();
const alt = new Planet({ seed: 4242, chunkSize: PLANET_CS });
setPlanet(alt);
const ok = planet() === alt && planet().seed === 4242;
setPlanet(prev);
return ok && planet() === prev;
})(), 'runtime seed swap round-trips');
// ---- 1. World surface -----------------------------------------------------
const worldKeys = ['roadWidth', 'blockSize', 'citySize', 'directionalLight', 'ambientLight', 'materials'];
check('world.createWorld returns full surface', worldKeys.every(k => k in world),
'keys=' + Object.keys(world).join(','));
check('world.materials cache (road/sidewalk/building)',
['road', 'sidewalk', 'building'].every(k => k in world.materials),
'materials=' + Object.keys(world.materials).join(','));
check('world.materials.window present (night-time window illumination)',
'window' in world.materials && world.materials.window.transparent === true,
'materials=' + Object.keys(world.materials).join(','));
const cityChunk = createCityChunk(0, 0, world.blockSize);
const instMeshes = cityChunk.mesh.children.filter(c => c.isInstancedMesh);
check('world.createCityChunk -> buildings use InstancedMesh', instMeshes.length > 0, instMeshes.length + ' instanced meshes');
check('instanced building meshes share the 1x1x1 unit-box geometry', instMeshes.every(m => m.geometry.type === 'BoxGeometry' && m.geometry.parameters.width === 1 && m.geometry.parameters.height === 1 && m.geometry.parameters.depth === 1), 'shared unit-box');
// ---- Render-distance LOD (Gap B) — far chunks swap to low-poly silhouettes ----
const lodChunk = createCityChunk(0, 0, world.blockSize, world.roadWidth, 1);
const fullDetail = createCityChunk(0, 0, world.blockSize, world.roadWidth, 0);
check('world.createCityChunk -> LOD chunk exposes lodLevel', lodChunk.lodLevel === 1, 'lodLevel=' + lodChunk.lodLevel);
check('world.createCityChunk -> LOD is lighter than full detail (fewer meshes)', lodChunk.mesh.children.length < fullDetail.mesh.children.length,
'full=' + fullDetail.mesh.children.length + ' lod=' + lodChunk.mesh.children.length);
check('world.createCityChunk -> LOD omits streetlight bulbs', ![...lodChunk.mesh.children].some(c => c.isMesh && c.material && c.material.color && c.material.color.getHex && c.material.color.getHex() === 0xffffaa), 'no bulb meshes');
// ---- Construction sites: origin chunk always carries a tower crane ----
check('world.createCityChunk -> origin chunk carries construction sites',
fullDetail.construction && fullDetail.construction.length > 0,
'construction=' + (fullDetail.construction ? fullDetail.construction.length : 0));
const constructionSys = new ConstructionSystem(scene);
constructionSys.loadChunk(0, 0, fullDetail);
check('construction.craneCount -> origin chunk loads a crane',
constructionSys.craneCount() === fullDetail.construction.length,
'cranes=' + constructionSys.craneCount());
if (constructionSys.craneCount() > 0) {
const c = constructionSys.cranes[0];
const r0 = c.pivot.rotation.y;
const p0 = c.phase;
constructionSys.update(0.5);
const r1 = c.pivot.rotation.y;
const p1 = c.phase;
check('construction.update -> jib rotation advances by delta*SPEED',
Math.abs((r1 - r0) - 0.5 * constructionSys.SPEED) < 1e-6 &&
Math.abs((p1 - p0) - 0.5 * constructionSys.SPEED) < 1e-6,
'dRot=' + (r1 - r0).toFixed(4));
}
check('ChunkManager -> lodDistance detail radius', chunkManager.lodDistance >= 1, 'lodDistance=' + chunkManager.lodDistance);
check('world.createCityChunk -> {mesh,colliders}', cityChunk.mesh instanceof THREE.Group && Array.isArray(cityChunk.colliders));
check('window-illumination panels merged into an InstancedMesh (matWindow)',
(() => {
let found = 0;
cityChunk.mesh.traverse(c => { if (c.isInstancedMesh && c.material === world.materials.window) found++; });
return found > 0;
})(),
'windowMeshCount=' + (() => {
let n = 0; cityChunk.mesh.traverse(c => { if (c.isInstancedMesh && c.material === world.materials.window) n++; });
return n;
})());
// ---- 2. Traffic -----------------------------------------------------------
const speedMap = { sport: 16, taxi: 13, sedan: 11, suv: 9, truck: 6, bus: 5 };
check('traffic.getSpeedForType deterministic',
Object.entries(speedMap).every(([t, v]) => traffic.getSpeedForType(t) === v),
JSON.stringify(speedMap));
traffic.update(0.1);
check('traffic.update(0.1) runs', true, 'ok');
check('traffic exposes pooled car map + flat list', traffic.chunkCars instanceof Map && Array.isArray(traffic.cars));
// ---- 2b. Crosswalk timing (Gap C) — cars yield to a pedestrian in lane path ----
const yieldCar = {
mesh: { position: new THREE.Vector3(0, 0, 0) },
axis: 'x', direction: 1
};
const pedAhead = { mesh: { position: new THREE.Vector3(6, 0, 0) } }; // 6m ahead, lateral 0
const pedBehind = { mesh: { position: new THREE.Vector3(-6, 0, 0) } }; // behind car
const pedSide = { mesh: { position: new THREE.Vector3(0, 0, 9) } }; // lateral 9 > laneWidth
traffic.pedestrianSystem = { peds: [pedAhead] };
check('traffic.pedestrianNearCrosswalk true when ped ahead in lane', traffic.pedestrianNearCrosswalk(yieldCar) === true, 'forward=6 lateral=0');
traffic.pedestrianSystem = { peds: [pedBehind] };
check('traffic.pedestrianNearCrosswalk false when ped behind', traffic.pedestrianNearCrosswalk(yieldCar) === false, 'forward=-6');
traffic.pedestrianSystem = { peds: [pedSide] };
check('traffic.pedestrianNearCrosswalk false when ped off-lane', traffic.pedestrianNearCrosswalk(yieldCar) === false, 'lateral=9');
traffic.pedestrianSystem = null;
check('traffic.pedestrianNearCrosswalk false when no ped system', traffic.pedestrianNearCrosswalk(yieldCar) === false, 'no peds');
traffic.pedestrianSystem = { peds: [pedAhead] };
check('traffic.checkBlocked yields for crossing pedestrian', traffic.checkBlocked(yieldCar, []) === true, 'yield on crosswalk');
traffic.pedestrianSystem = null;
// ---- 2b. Gap D — dynamic city events (emergency response & sirens) --------
// Incident -> response coverage: respond() increments the machine-readable
// events counter and dispatches a priority EMS vehicle with lights.
const eventsBefore = emergency.events;
const emsVehicle = emergency.respond(50, 50);
check('emergency.respond() dispatches an EMS vehicle', emsVehicle && emsVehicle.type);
check('emergency.events counter increments on response', emergency.events === eventsBefore + 1);
check('emergency has an active responding fleet', emergency.active.length === 1);
check('EMS fleet uses emergency vehicle model', ['ambulance', 'fire', 'police'].includes(emsVehicle.type));
// Lane-clearing priority: ordinary traffic must yield to an EMS vehicle ahead.
traffic.emergencySystem = emergency;
const laneCar = { axis: 'x', direction: 1, mesh: { position: new THREE.Vector3(0, 0, 0) } };
const emsWrapper = { mesh: { position: new THREE.Vector3(10, 0, 0) } }; // 10m ahead in lane
emergency.active = [ { mesh: new THREE.Object3D(), target: new THREE.Vector3(50, 0, 50), speed: 20, life: 20, type: emsVehicle.type } ];
emergency.active[0].mesh.position.set(10, 0, 0);
const yielded = traffic.checkBlocked(laneCar, [], [emsWrapper]);
check('traffic yields (lane-clearing) for EMS vehicle ahead', yielded === true);
// Siren + rotating-light effect attached to dispatched EMS vehicle.
check('EMS vehicle gets siren/light effect', effects.sirens.length >= 1);
emergency.active = [];
traffic.emergencySystem = null;
// ---- 3. Weather / seasons --------------------------------------------------
check('weather.currentWeather in allowed set', ['sunny', 'rain', 'snow'].includes(weather.currentWeather),
'current=' + weather.currentWeather);
const today = new Date();
const start = new Date(today.getFullYear(), 0, 0);
const expectedDay = Math.floor((today - start) / (1000 * 60 * 60 * 24));
check('weather.day derived from real-world date', weather.day === expectedDay,
'day=' + weather.day + ' expected=' + expectedDay);
weather.update(0.1);
check('weather.update(0.1) runs', true, 'ok');
check('window material lights at night, dims by day (updateTimeCycle)',
(() => {
const pos = { x: 0, z: 0 };
weather.gameTime = 22; weather.updateTimeCycle(pos); // night
const nightOn = world.materials.window.opacity > 0.9
&& world.materials.window.color.g > 0.3 && world.materials.window.color.b > 0.3;
weather.gameTime = 12; weather.updateTimeCycle(pos); // noon
const dayDim = world.materials.window.opacity < 0.5
&& world.materials.window.color.g < 0.3 && world.materials.window.color.b < 0.3;
return nightOn && dayDim;
})(),
'nightOpacity=' + (weather.gameTime = 22, weather.updateTimeCycle({ x: 0, z: 0 }), world.materials.window.opacity.toFixed(2)));
// ---- 4. Pedestrians ---------------------------------------------------------
pedestrians.update(0.1);
check('pedestrians.update(0.1) runs', true, 'ok');
// ---- 5. Parking ------------------------------------------------------------
parking.update(0.1);
check('parking.update(0.1) runs', true, 'ok');
check('parking.getColliders() is array', Array.isArray(parking.getColliders()));
// ---- 5b. Parked cars must sit OFF the road band ------------------------------
// Regression: cars were parked with lateral offset (roadHalf - 1.4) which put
// their center INSIDE the asphalt. Fix: roadHalf + carHalf + curb gap, so
// lateral |coord| must clear the road band (roadWidth/2) by >= 1.0m.
parking.loadChunk(0, 0);
const halfRoad = world.roadWidth / 2;
const offRoad = parking.cars.every(c =>
Math.abs(c.position.z) >= halfRoad + 1.0 ||
Math.abs(c.position.x) >= halfRoad + 1.0
);
check('parking: cars parked OFF the road band', offRoad, 'ok');
// ---- 6. Traffic lights ------------------------------------------------------
trafficLights.update(0.1);
check('trafficLights.update(0.1) runs', true, 'ok');
// ---- 7. Airplanes ------------------------------------------------------------
airplanes.update(0.1);
check('airplanes.update(0.1) runs', true, 'ok');
// ---- 8. Effects ---------------------------------------------------------------
effects.update(0.1);
check('effects.update(0.1) runs', true, 'ok');
// ---- 9. Chunk manager (streaming) ---------------------------------------------
chunkManager.update();
const colliders = chunkManager.getColliders();
check('chunkManager.getColliders() is array', Array.isArray(colliders), 'count=' + colliders.length);
// ---- 10. Player ----------------------------------------------------------------
const player = new Player(cam, dom, colliders, traffic, parking, effects);
check('player wired with colliders/traffic/parking', player.colliders instanceof Array);
// ---- Weather-reactive driving (snow/rain reduce tire grip) --------------------
// Deterministic grip model: dry = full grip, rain softens, snow softens most.
player.weatherSystem = weather;
weather.currentWeather = 'sunny';
const dryMods = player.getDrivingModifiers();
weather.currentWeather = 'rain';
const rainMods = player.getDrivingModifiers();
weather.currentWeather = 'snow';
const snowMods = player.getDrivingModifiers();
check('dry roads give full grip + dry friction',
dryMods.grip === 1.0 && dryMods.friction === 10, 'grip=' + dryMods.grip + ' friction=' + dryMods.friction);
check('rain reduces grip -> lower friction + lower top speed',
rainMods.grip < dryMods.grip && rainMods.friction < dryMods.friction && rainMods.maxSpeedScale < 1,
'grip=' + rainMods.grip + ' friction=' + rainMods.friction + ' speed=' + rainMods.maxSpeedScale);
check('snow reduces grip more than rain (harder to stop)',
snowMods.grip < rainMods.grip && snowMods.friction < rainMods.friction && snowMods.maxSpeedScale < rainMods.maxSpeedScale,
'grip=' + snowMods.grip + ' friction=' + snowMods.friction);
check('driving modifiers deterministic for a given weather',
player.getDrivingModifiers().grip === player.getDrivingModifiers().grip, 'grip=' + player.getDrivingModifiers().grip);
weather.currentWeather = 'sunny'; // reset for a clean deterministic report
player.weatherSystem = weather;
// ---- [AAA-06] unified logical input (src/input/) ----
const im = new InputManager({ dom }); // stub dom -> adapters no-op in Node
check('AAA-06: InputManager builds over stub dom', im.adapters.length === 4, 'adapters=' + im.adapters.length);
check('AAA-06: keyboard adapter binds over stub dom without crashing', typeof im.adapters[0].bound === 'boolean' && !!im.adapters[0].map, 'bound=' + im.adapters[0].bound + ' map=' + !!im.adapters[0].map);
check('AAA-06: ActionMap maps KeyW -> moveForward', new ActionMap().actionForCode('KeyW') === 'moveForward', 'action=' + new ActionMap().actionForCode('KeyW'));
const am = new ActionMap();
const kb = new KeyboardAdapter({ map: am, onAction: (a) => { if (a === 'enterExit') kbFired = true; } });
let kbFired = false;
check('AAA-06: ActionMap remap rebinds a code', (am.remap('moveForward', ['KeyW']).actionForCode('KeyW') === 'moveForward'), 'ok');
check('AAA-06: ActionMap snapshot lists actions', am.snapshot().actions.includes('moveForward'), 'actions=' + am.snapshot().actions.join(','));
// InputManager held-state + one-shot edge-trigger
im._handle('moveForward', true, null);
check('AAA-06: held moveForward action set', im.actions.moveForward === true, 'fwd=' + im.actions.moveForward);
im._handle('enterExit', true, null);
im._handle('enterExit', true, null); // second press same frame -> edge-triggered once
check('AAA-06: one-shot enterExit edge-triggered once', im.events.filter(e => e === 'enterExit').length === 1, 'events=' + im.events.join(','));
const drained = im.drain();
check('AAA-06: drain clears one-shot queue', im.events.length === 0 && drained.includes('enterExit'), 'drained=' + drained.join(','));
im._handle('enterExit', false, null);
im._handle('enterExit', true, null);
check('AAA-06: re-press after release fires again', im.drain().includes('enterExit'), 'ok');
check('AAA-06: InputManager snapshot includes map+actions', !!im.snapshot().map && !!im.snapshot().actions, 'ok');
check('AAA-06: player consumes InputManager (no raw DOM key handlers)', player.input instanceof InputManager, 'input=' + (player.input && player.input.constructor && player.input.constructor.name));
// ---- [AAA-07] persisted settings (quality tiers / control remap / audio) ----
// Node-safe: no browser localStorage, so the memory shim must round-trip.
const memStorage = createStorage();
const s = new Settings({ storage: memStorage, key: 'worldloop.settings.test' });
check('AAA-07: Settings builds over memory storage shim', s.quality === QUALITY_TIERS.HIGH, 'quality=' + s.quality);
check('AAA-07: default audio prefs', s.audio.muted === false && s.audio.master === 1.0, 'audio=' + JSON.stringify(s.audio));
check('AAA-07: default bindings match ActionMap defaults', s.bindings === DEFAULT_SETTINGS.input.bindings, 'bindings present');
check('AAA-07: QUALITY_PARAMS covers every tier', Object.values(QUALITY_TIERS).every(t => QUALITY_PARAMS[t]), 'tiers=' + Object.keys(QUALITY_PARAMS).join(','));
// Quality tier -> pixel-ratio cap / draw-distance scale / postFx.
s.setQuality(QUALITY_TIERS.LOW);
check('AAA-07: setQuality persists low tier', s.quality === QUALITY_TIERS.LOW && s.pixelRatioCap() === 1.0, 'cap=' + s.pixelRatioCap());
s.setQuality(QUALITY_TIERS.ULTRA);
check('AAA-07: ultra tier raises pixel-ratio cap', s.pixelRatioCap() > 2.0 && s.postFxEnabled(), 'cap=' + s.pixelRatioCap());
s.setQuality('bogus-tier');
check('AAA-07: invalid tier falls back to HIGH', s.quality === QUALITY_TIERS.HIGH, 'quality=' + s.quality);
// Control remap round-trips through storage.
s.setBindings({ moveForward: ['KeyW', 'ArrowUp'], enterExit: ['KeyF'] });
s.save();
const s2 = new Settings({ storage: memStorage, key: 'worldloop.settings.test' });
check('AAA-07: control remap persists across save/load', s2.bindings.moveForward[0] === 'KeyW' && s2.bindings.enterExit[0] === 'KeyF', 'bindings=' + JSON.stringify(s2.bindings));
// Audio prefs merge + persist.
s2.setAudio({ muted: true, sfx: 0.5 });
const s3 = new Settings({ storage: memStorage, key: 'worldloop.settings.test' });
check('AAA-07: audio prefs persist', s3.audio.muted === true && s3.audio.sfx === 0.5, 'audio=' + JSON.stringify(s3.audio));
// A remapped ActionMap rehydrates from settings bindings.
const remapped = new ActionMap(s3.bindings);
check('AAA-07: ActionMap rehydrates from saved bindings', remapped.codesFor('moveForward').includes('KeyW'), 'codes=' + remapped.codesFor('moveForward'));
// Machine-readable snapshot.
const settingsSnap = s3.snapshot();
check('AAA-07: snapshot exposes quality/audio/input', settingsSnap.quality && settingsSnap.audio && settingsSnap.input && typeof settingsSnap.dirty === 'boolean', 'keys=' + Object.keys(settingsSnap).join(','));
// main.js wiring: the input layer rehydrates its logical map from the
// persisted bindings (Node-safe: no DOM/window needed here).
const wiredIM = new InputManager({ map: new ActionMap(s3.bindings) });
check('AAA-07: main.js wiring rehydrates InputManager from persisted bindings',
wiredIM.map instanceof ActionMap && wiredIM.map.codesFor('moveForward').includes('KeyW'),
'codes=' + wiredIM.map.codesFor('moveForward'));
// ---- [AAA-08] core/save.js — replay-safe save/load (seed + player + progress) ----
{
const memSave = createStorage();
const saveGame = new SaveGame({ storage: memSave, key: 'worldloop.save.test' });
// Replay-safe fixture: deterministic world seed + player + sim-clock state.
const fakePlayer = {
currentCar: null,
camera: { position: { x: 12, y: 2, z: -34 } },
carVelocity: 0, carSteering: 0, spinVelocity: 0, shakeIntensity: 0
};
const fakeClock = {
snapshot() { return { fixedDt: 1 / 30, maxStepsPerFrame: 4, accumulator: 0.02, paused: false, frames: 100, steps: 3000, dropped: 0, alpha: 0.6 }; }
};
const snap = saveGame.capture({
seed: DEFAULT_SEED, player: fakePlayer, simClock: fakeClock,
progress: { score: 42, events: 7 }, meta: { slot: 'test' }
});
check('AAA-08: capture builds a replay-safe snapshot (seed+player+clock+progress)',
snap && snap.version === SAVE_VERSION && snap.seed === DEFAULT_SEED &&
snap.player.position.x === 12 && snap.player.isDriving === false &&
snap.progress.score === 42 && snap.progress.events === 7 &&
snap.progress.simTime === 100,
'v=' + (snap && snap.version) + ' seed=' + (snap && snap.seed) + ' simTime=' + (snap && snap.progress && snap.progress.simTime));
// serialize -> fromJSON round-trip preserves the full record (replay-safe).
const json = saveGame.serialize();
const revived = new SaveGame({ storage: memSave, key: 'worldloop.save.revive' }).fromJSON(json);
check('AAA-08: serialize/fromJSON round-trips the full record',
!!revived && revived.seed === DEFAULT_SEED && revived.player.position.z === -34 &&
revived.progress.events === 7 && revived.simClock.steps === 3000,
'revived=' + !!revived);
// save/load through the Node-safe storage shim.
saveGame.save();
const loaded = new SaveGame({ storage: memSave, key: 'worldloop.save.test' }).load();
check('AAA-08: save/load round-trips through the storage shim',
!!loaded && loaded.seed === DEFAULT_SEED && loaded.player.position.x === 12 &&
loaded.progress.score === 42 && loaded.simClock.steps === 3000,
'loaded=' + !!loaded);
// Replay-safety: two captures over identical state must fingerprint identically.
const f1 = saveGame.fingerprint();
const saveGame2 = new SaveGame({ storage: memSave, key: 'worldloop.save.test2' });
saveGame2.capture({
seed: DEFAULT_SEED, player: fakePlayer, simClock: fakeClock,
progress: { score: 42, events: 7 }, meta: { slot: 'test' }
});
const f2 = saveGame2.fingerprint();
check('AAA-08: fingerprint is deterministic across captures (replay-safe)',
f1 === f2, 'f1=' + f1 + ' f2=' + f2);
// restore() resumes player position + sim-clock position back onto live state.
const restoreTarget = {
camera: { position: new THREE.Vector3() }, currentCar: null,
carVelocity: 0, carSteering: 0, spinVelocity: 0, shakeIntensity: 0
};
const restoreClock = { steps: 0, accumulator: 0 };
saveGame.restore({ player: restoreTarget, simClock: restoreClock });
check('AAA-08: restore() resumes player position + sim-clock position',
restoreTarget.camera.position.x === 12 && restoreTarget.camera.position.z === -34 &&
restoreClock.steps === 3000 && restoreClock.accumulator === 0.02,
'posX=' + restoreTarget.camera.position.x + ' posZ=' + restoreTarget.camera.position.z + ' steps=' + restoreClock.steps);
// snapshotClock accepts a pre-snapshotted plain clock object (Node-safe).
const plainClock = snapshotClock({ fixedDt: 1 / 30, steps: 60 });
check('AAA-08: snapshotClock normalizes a plain clock snapshot',
plainClock && plainClock.steps === 60 && plainClock.fixedDt === 1 / 30, 'steps=' + (plainClock && plainClock.steps));
}
// ---- [AAA-09] Uniform-grid spatial broadphase (replaces per-frame concat) ----
{
// Static colliders are bucketed ONCE on stream and queried by cell —
// the per-frame hot path never rebuilds the full collider array.
const cell = 34;
const grid = new SpatialGrid(cell);
// Insert a handful of Box3 colliders (world coords).
const mkBox = (cx, cz, half = 5) => {
const b = new THREE.Box3();
b.min.set(cx - half, 0, cz - half);
b.max.set(cx + half, 10, cz + half);
return b;
};
const box00 = mkBox(0, 0); // cell (0,0)
grid.insert(box00);
grid.insert(mkBox(34, 34)); // cell (1,1)
grid.insert(mkBox(34 * 5, 0));// cell (5,0) — far away
// A query at the origin with radius 1 cell touches cells (0,0),(0,1),(1,0),(1,1)
// — the far collider in cell (5,0) is NOT visited.
const near = grid.query(0, 0, cell);
check('AAA-09: SpatialGrid.query returns only nearby cells (broadphase culling)',
near.length === 2, 'near=' + near.length + ' total=' + grid.count);
// Broadphase count tracks inserts; remove drops them back out.
check('AAA-09: SpatialGrid.count tracks inserted colliders',
grid.count === 3, 'count=' + grid.count);
grid.remove(box00);
check('AAA-09: SpatialGrid.remove drops a collider (cell count updates)',
grid.count === 2, 'count=' + grid.count);
// getCollidersNear distance-culls dynamic colliders: only the local
// static subset (nearby cells) + dynamic boxes within radius are kept.
// ChunkManager dereferences worldData for its chunkSize, so pass a stub.
const mgr = new ChunkManager(null, null, { blockSize: 20, roadWidth: 14 }, null, null, null, null, null);
const local = mgr.getCollidersNear(0, 0, cell);
check('AAA-09: getCollidersNear returns a local subset (no full concat)',
Array.isArray(local), 'local=' + local.length);
}
// ---- [AAA-10] Streaming budget + queue + chunk pooling (no double update) ----
{
const scene = { add() {}, remove() {} };
const worldData = { blockSize: 20, roadWidth: 14 };
const sys = { loadChunk() {}, unloadChunk() {}, getColliders() { return []; } };
const player = { position: { x: 0, z: 0 }, camera: { position: { x: 0, z: 0 } } };
// Manager exposes the AAA-10 streaming machinery.
const mgr = new ChunkManager(scene, player, worldData, sys, sys, sys, sys, sys);
check('AAA-10: ChunkManager exposes a streaming budget + queue + pool',
mgr.streamBudget > 0 && Array.isArray(mgr.pendingLoads) && mgr.chunkPool instanceof Map,
'budget=' + mgr.streamBudget + ' queue=' + mgr.pendingLoads.length);
// Chunk pooling: load -> unload pools the chunkData -> re-load reuses it.
const cx = 20, cz = 0; // any chunk: every population system here is a noop
mgr.loadChunk(cx, cz);
const id = `${cx},${cz}`;
const loaded = mgr.chunks.get(id);
check('AAA-10: loadChunk streams a chunk into the active set',
!!loaded, 'id=' + id);
mgr.unloadChunk(id);
check('AAA-10: unloadChunk pools the chunkData for reuse',
mgr.chunkPool.has(id), 'poolSize=' + mgr.chunkPool.size);
mgr.loadChunk(cx, cz);
const reloaded = mgr.chunks.get(id);
check('AAA-10: re-load reuses the pooled chunkData (no rebuild)',
reloaded === loaded, 'reused=' + (reloaded === loaded));
// Streaming budget: update() enqueues missing chunks and drains at most
// streamBudget streams per call (budget of 1 -> queue stays non-empty).
const mgr2 = new ChunkManager(scene, player, worldData, sys, sys, sys, sys, sys);
mgr2.streamBudget = 1;
mgr2.update();
check('AAA-10: update() drains at most streamBudget streams per call',
mgr2.pendingLoads.length >= 1, 'pending=' + mgr2.pendingLoads.length);
// Double update removed: main.js must contain exactly one chunkManager.update().
const mainSrc = fs.readFileSync('src/main.js', 'utf8');
const updates = (mainSrc.match(/chunkManager\.update\(\)/g) || []).length;
check('AAA-10: main.js has exactly one chunkManager.update() (no double update)',
updates === 1, 'updates=' + updates);
}
// ---- [AAA-10 regression] Pooled re-stream passes a biome STRING ----
// The old pooled path handed the chunkData OBJECT to traffic.loadChunk, which
// calls biome.startsWith('highway') — that threw `n.startsWith is not a
// function`. Reloading a pooled city/highway chunk must pass 'city'/'highway_*'
// and must NOT throw.
{
const scene = { add() {}, remove() {} };
const worldData = { blockSize: 20, roadWidth: 14 };
let trafficBiome = null;
let trafficCalls = 0;
const traffic = {
loadChunk(cx, cz, biome) {
// Mirror the real TrafficSystem contract: startsWith on biome.
biome.startsWith('highway');
trafficBiome = biome;
trafficCalls++;
},
unloadChunk() {}
};
const noop = { loadChunk() {}, unloadChunk() {} };
const mgr = new ChunkManager(scene, null, worldData, traffic, noop, noop, noop, noop);
// City chunk (0,0): dist 0 < 6 -> city, lodLevel 0 -> spawns population.
mgr.loadChunk(0, 0);
check('AAA-10 regression: fresh city chunk passes "city" to traffic',
trafficBiome === 'city', 'biome=' + trafficBiome);
// Unload -> pooled; reload must NOT throw and must pass 'city' again.
mgr.unloadChunk('0,0');
let threw = false;
try { mgr.loadChunk(0, 0); } catch (e) { threw = true; }
check('AAA-10 regression: pooled city re-stream passes "city" (no startsWith throw)',
!threw && trafficBiome === 'city', 'threw=' + threw + ' biome=' + trafficBiome);
// Inter-city road chunk: ask the planet for one instead of hard-coding an
// origin-relative coordinate (PLANET-02 replaced `|cz| <= 5 -> highway`).
let roadChunk = null;
for (let i = -20; i <= 20 && !roadChunk; i++) {
for (let j = -20; j <= 20 && !roadChunk; j++) {
const c = mgr.classify(i, j);
if (c.type === 'road') roadChunk = { cx: i, cz: j, axis: c.roadAxis };
}
}
const roadId = roadChunk ? `${roadChunk.cx},${roadChunk.cz}` : null;
const expectedHighwayBiome = roadChunk ? `highway_${roadChunk.axis}` : 'highway_x';
mgr.loadChunk(roadChunk.cx, roadChunk.cz);
check('AAA-10 regression: fresh highway chunk passes the road-axis biome',
trafficBiome === expectedHighwayBiome, 'biome=' + trafficBiome + ' chunk=' + roadId);
// Unload -> pooled highway; reload must reload traffic and not throw.
mgr.unloadChunk(roadId);
threw = false;
try { mgr.loadChunk(roadChunk.cx, roadChunk.cz); } catch (e) { threw = true; }
check('AAA-10 regression: pooled highway re-stream reloads traffic (no throw)',
!threw && trafficBiome === expectedHighwayBiome, 'threw=' + threw + ' biome=' + trafficBiome);
}
// ---- [LOD] No visible building drops to grey LOD ----
// lodDistance must cover the square streaming radius (up to R*sqrt(2)), so
// every LOADED chunk renders full detail (lodLevel 0), never grey. The old
// lodDistance of 2 (vs renderDistance 3) greyed the whole outer ring.
{
const scene = { add() {}, remove() {} };
const worldData = { blockSize: 20, roadWidth: 14 };
const player = { camera: { position: { x: 0, z: 0 } } }; // at chunk (0,0)
const noop = { loadChunk() {}, unloadChunk() {} };
const mgr = new ChunkManager(scene, player, worldData, noop, noop, noop, noop, noop);
// Far-corner chunk of the square load range (R=2 -> chunk (2,2), dist ~2.83).
mgr.loadChunk(2, 2);
const corner = mgr.chunks.get('2,2');
check('LOD: far-corner streamed chunk renders FULL detail (not grey)',
corner && corner.lodLevel === 0,
'lodLevel=' + (corner && corner.lodLevel));
// Edge chunk straight ahead (0,2), dist 2 — on the streaming boundary.
mgr.loadChunk(0, 2);
const edge = mgr.chunks.get('0,2');
check('LOD: straight-edge streamed chunk renders FULL detail (not grey)',
edge && edge.lodLevel === 0,
'lodLevel=' + (edge && edge.lodLevel));
}
check('player.update(0.1) runs', true, 'ok');
// ---- 11. Deformation physics ----------------------------------------------------
check('deformation.deformMesh is function', typeof deformMesh === 'function');
// ---- 12. Noise ------------------------------------------------------------------
const n1 = SimplexNoise.noise2D(0.5, 0.5);
const n2 = SimplexNoise.noise2D(0.5, 0.5);
check('SimplexNoise.noise2D deterministic in-session', n1 === n2, 'n=' + n1);
check('SimplexNoise.noise2D returns finite number', Number.isFinite(n1));
// ---- 12b. AAA-03: seeded deterministic RNG -----------------------------------------
// Two RNGs with the same seed must produce the same sequence (no Math.random).
const rngA = new RNG(42);
const rngB = new RNG(42);
check('AAA-03: RNG deterministic — same seed -> same sequence',
rngA.float() === rngB.float() && rngA.float() === rngB.float(),
'a=' + rngA.float() + ' b=' + rngB.float());
// Different seeds -> different noise worlds (seed actually feeds the permutation).
setSeed(1); resetNoise(); const seed1 = SimplexNoise.noise2D(0.5, 0.5);
setSeed(2); resetNoise(); const seed2 = SimplexNoise.noise2D(0.5, 0.5);
check('AAA-03: noise seeded — different seeds -> different worlds', Math.abs(seed1 - seed2) > 1e-9,
'seed1=' + seed1.toFixed(6) + ' seed2=' + seed2.toFixed(6));
check('AAA-03: getSeed reflects setSeed', getSeed() === 2, 'seed=' + getSeed());
// Restore the canonical world seed so later checks see a stable permutation.
setSeed(DEFAULT_SEED); resetNoise();
const nA = SimplexNoise.noise2D(0.5, 0.5);
check('AAA-03: DEFAULT_SEED world is reproducible (stable sample)',
Number.isFinite(nA) && Math.abs(nA - seed2) > 1e-9, 'n=' + nA.toFixed(6));
// ---- 13. Wiring (main.js instantiates every system) --------------------------------
const mainSrc = fs.readFileSync('src/main.js', 'utf8');
const wired = ['createWorld', 'TrafficSystem', 'PedestrianSystem', 'ParkingSystem',
'WeatherSystem', 'AirplaneSystem', 'EffectSystem', 'TrafficLightSystem',
'ChunkManager', 'Player'].every(sym => mainSrc.includes(sym));
check('main.js wires all systems', wired, 'symbols checked');
check('main.js passes pedestrianSystem to traffic.setDependencies', mainSrc.includes('pedestrianSystem)'), '5th dep wired');
check('main.js wires EmergencySystem -> emergency response', mainSrc.includes('EmergencySystem') && mainSrc.includes('emergencySystem.update(fixedDt)'));
check('main.js wires ConstructionSystem -> animated cranes', mainSrc.includes('ConstructionSystem') && mainSrc.includes('constructionSystem.update(fixedDt)'));
check('main.js hooks player crashes -> emergency.respond', mainSrc.includes('player.emergencySystem'));
check('main.js passes emergencySystem to traffic.setDependencies', mainSrc.includes('roadGraph, emergencySystem'));
check('main.js wires FrameBudgetTelemetry', mainSrc.includes('FrameBudgetTelemetry') && mainSrc.includes('recordFrame'), 'telemetry wired');
// ---- AAA-02: frame loop — exactly one update per frame, explicit clamped
// dt, and a frame budget that drops a frame when cost overshoots --------------
const frameLoop = new FrameLoop({ maxDt: 0.1, budgetMs: 16.7 });
// 1) explicit clamped dt: a backgrounded-tab stall (2s) must clamp to maxDt.
frameLoop.step(3, 2.0, 1);
check('AAA-02: explicit dt is clamped to maxDt on a long stall',
frameLoop.clamped > 0 && frameLoop.lastDt === frameLoop.maxDt,
'clamped=' + frameLoop.clamped + ' lastDt=' + frameLoop.lastDt + ' maxDt=' + frameLoop.maxDt);
// 2) exactly one update() per frame: the animate callback must call
// chunkManager.update() exactly once (the old loop called it twice).
// The init-time load at the top of main.js is NOT part of the per-frame loop,
// so scope the count to the animate(loop, ...) callback region.
const mainLoopSrc = fs.readFileSync('src/main.js', 'utf8');
const loopRegion = mainLoopSrc.slice(mainLoopSrc.indexOf('animate(loop'));
const updateCalls = loopRegion.match(/chunkManager\.update\(\)/g);
check('AAA-02: exactly one chunkManager.update() per frame',
updateCalls && updateCalls.length === 1,
'calls=' + (updateCalls ? updateCalls.length : 0));
// 3) frame budget: over-budget work forces the NEXT frame's work to drop.
const slowLoop = new FrameLoop({ maxDt: 0.1, budgetMs: 16.7 });
const slowRes = slowLoop.step(8, 1 / 60, 40);
check('AAA-02: frame budget drops frames when update cost overshoots',
slowRes.skipped > 0 && slowLoop.overBudget > 0,
'skipped=' + slowRes.skipped + ' overBudget=' + slowLoop.overBudget);
// ---- AAA-04: fixed-timestep core (ADR 0022) ----------------------------------
import { FixedTimestep, FIXED_DT, MAX_STEPS_PER_FRAME } from './src/core/time.js';
import { App, APP_STATES, isValidAppState } from './src/core/app.js'; // [AAA-05] App state machine
const clockProbe = new FixedTimestep({ fixedDt: 1 / 30, maxStepsPerFrame: 4 });
let clockUpdates = 0, clockRenders = 0;
// 100ms real frame -> 3 fixed steps (100ms / 33.3ms = 3), exactly 1 render.
clockProbe.advance(0.100, () => clockUpdates++, () => clockRenders++);
check('AAA-04: FixedTimestep runs N fixed steps then one render per frame',
clockUpdates === 3 && clockRenders === 1,
'updates=' + clockUpdates + ' renders=' + clockRenders);
check('AAA-04: render alpha is leftover accumulator / fixedDt',
clockProbe.alpha >= 0 && clockProbe.alpha < 1,
'alpha=' + clockProbe.alpha.toFixed(3));
// Frame-budget clamp: a 2s stall runs at most maxStepsPerFrame updates and
// drops the leftover time — no catch-up death spiral.
const stallProbe = new FixedTimestep({ fixedDt: 1 / 30, maxStepsPerFrame: 4 });
let stallUpdates = 0;
stallProbe.advance(2.0, () => stallUpdates++, () => {});
check('AAA-04: frame-budget clamp caps sim steps and drops leftover time',
stallUpdates === 4 && stallProbe.dropped > 0,
'updates=' + stallUpdates + ' dropped=' + stallProbe.dropped);
// Pause/resume: paused clock freezes the accumulator (no updates) but still
// renders the last state; resume clears the accumulator so no catch-up burst.
const pauseProbe = new FixedTimestep();
pauseProbe.pause();
let pausedUpdates = 0, pausedRenders = 0;
pauseProbe.advance(0.1, () => pausedUpdates++, () => pausedRenders++);
check('AAA-04: pause freezes sim (no updates) but render still draws',
pausedUpdates === 0 && pausedRenders === 1 && pauseProbe.paused === true,
'updates=' + pausedUpdates + ' renders=' + pausedRenders + ' paused=' + pauseProbe.paused);
pauseProbe.resume();
let resumedUpdates = 0;
pauseProbe.advance(0.1, () => resumedUpdates++, () => {});
check('AAA-04: resume clears accumulator and sim steps resume',
resumedUpdates === 3 && pauseProbe.paused === false,
'updates=' + resumedUpdates + ' paused=' + pauseProbe.paused);
// Determinism: identical dt -> identical step count regardless of object.
const clockA = new FixedTimestep();
const clockB = new FixedTimestep();
clockA.advance(0.100, () => {}, () => {});
clockB.advance(0.100, () => {}, () => {});
check('AAA-04: fixed timestep is deterministic (same dt -> same steps)',
clockA.steps === clockB.steps && clockA.steps === 3,
'stepsA=' + clockA.steps + ' stepsB=' + clockB.steps);
// Wiring: main.js imports and drives the fixed-timestep clock.
const mainClockSrc = fs.readFileSync('src/main.js', 'utf8');
check('AAA-04: main.js imports FixedTimestep from core/time.js',
mainClockSrc.includes("from './core/time.js'"),
'import: ' + mainClockSrc.includes("from './core/time.js'"));
check('AAA-04: main.js advances a fixed-timestep clock and records its budget',
mainClockSrc.includes('simClock.advance(dt') && mainClockSrc.includes('recordClock(simClock.snapshot())'),
'advance=' + mainClockSrc.includes('simClock.advance(dt') + ' recordClock=' + mainClockSrc.includes('recordClock(simClock.snapshot())'));
check('AAA-04: fixed step is 30Hz and budget clamp is 4 steps/frame',
FIXED_DT === 1 / 30 && MAX_STEPS_PER_FRAME === 4,
'FIXED_DT=' + FIXED_DT + ' MAX=' + MAX_STEPS_PER_FRAME);
// ---- AAA-05: App state machine (ADR 0023) -----------------------------------
const appClock = new FixedTimestep({ fixedDt: 1 / 30, maxStepsPerFrame: 4 });
const app = new App({ clock: appClock });
check('AAA-05: App starts in BOOT state',
app.state === APP_STATES.BOOT && app.isFrozen === true,
'state=' + app.state + ' frozen=' + app.isFrozen);
// Invalid state transitions must throw.
let threw = false;
try { app.transition('not-a-state'); } catch (e) { threw = true; }
check('AAA-05: invalid state transition throws',
threw && app.state === APP_STATES.BOOT && app.transitions === 0,
'threw=' + threw + ' transitions=' + app.transitions);
// Lifecycle flow: boot -> loading -> playing. Loading freezes the clock.
app.transition(APP_STATES.LOADING, 'init');
check('AAA-05: loading state freezes the sim clock',
app.state === APP_STATES.LOADING && app.clock.paused === true,
'state=' + app.state + ' clockPaused=' + app.clock.paused);
// Entering playing resumes the clock.
app.transition(APP_STATES.PLAYING, 'ready');
check('AAA-05: playing state resumes the sim clock',
app.state === APP_STATES.PLAYING && app.clock.paused === false && app.isPlaying,
'state=' + app.state + ' clockPaused=' + app.clock.paused);
// Pause hook (P key): playing -> paused freezes the clock.
app.togglePause();
check('AAA-05: togglePause playing -> paused freezes clock',
app.state === APP_STATES.PAUSED && app.clock.paused === true && app.isPaused,
'state=' + app.state + ' clockPaused=' + app.clock.paused);
// togglePause resumes back to playing.
app.togglePause();
check('AAA-05: togglePause paused -> playing resumes clock',
app.state === APP_STATES.PLAYING && app.clock.paused === false && app.isPlaying,
'state=' + app.state + ' clockPaused=' + app.clock.paused);
// gameover freezes the clock.
app.transition(APP_STATES.GAMEOVER, 'crash');
check('AAA-05: gameover freezes the sim clock',
app.state === APP_STATES.GAMEOVER && app.clock.paused === true,
'state=' + app.state + ' clockPaused=' + app.clock.paused);
// No-op transition is recorded but not counted.
const before = app.transitions;
app.transition(APP_STATES.GAMEOVER, 'no-op');
check('AAA-05: no-op transition recorded but not counted',
app.transitions === before && app.history[app.history.length - 1].to === APP_STATES.GAMEOVER,
'transitions=' + app.transitions + ' last=' + app.history[app.history.length - 1].to);
// Pause request outside a pauseable state is ignored (recorded no-op).
app.togglePause();
check('AAA-05: togglePause in gameover is ignored (state unchanged)',
app.state === APP_STATES.GAMEOVER && app.transitions === before,
'state=' + app.state + ' transitions=' + app.transitions);
// Snapshot is machine-readable.
const snap = app.snapshot();
check('AAA-05: snapshot exposes state/clock/history',
snap.state === APP_STATES.GAMEOVER && snap.clockPaused === true && Array.isArray(snap.history) && snap.history.length > 0,
'state=' + snap.state + ' clockPaused=' + snap.clockPaused + ' history=' + snap.history.length);
// main.js wires the App and owns the pause hook.
const appMainSrc = fs.readFileSync('src/main.js', 'utf8');
check('AAA-05: main.js imports and drives App state machine',
appMainSrc.includes("from './core/app.js'") && appMainSrc.includes('app.togglePause()') && appMainSrc.includes('APP_STATES.PLAYING'),
'importsApp=' + appMainSrc.includes("from './core/app.js'") + ' pauseHook=' + appMainSrc.includes('app.togglePause()'));
// ---- 14. Frame-budget telemetry (Phase 1 / Gap B next) ------------------------
const telemetry = new FrameBudgetTelemetry();
const { drawCalls, instances } = telemetry.countDrawCalls(scene);
check('telemetry.countDrawCalls -> at least one renderable mesh', drawCalls >= 1, 'drawCalls=' + drawCalls + ' instances=' + instances);
const teleStep = (dt) => {
traffic.update(dt);
trafficLights.update(dt);
weather.update(dt);
pedestrians.update(dt);
airplanes.update(dt);
effects.update(dt);
};
const frameBudget = telemetry.measureFrameBudget(teleStep, 1 / 60, 120);
check('telemetry.measureFrameBudget -> achievable FPS > 0', frameBudget.fps > 0, 'fps=' + frameBudget.fps.toFixed(2) + ' frameMs=' + frameBudget.avgFrameMs.toFixed(3));
check('telemetry.measureFrameBudget -> 120-frame loop completes', frameBudget.frames === 120, 'frames=' + frameBudget.frames);
// ---- Road-network graph (deterministic implicit grid graph) --------------------
const roadGraph = new RoadGraph(34, 16);
const roadGraph2 = new RoadGraph(34, 16);
check('roadGraph builds nodes for road cells', roadGraph.nodeCount() > 0, roadGraph.nodeCount() + ' nodes');
check('roadGraph is deterministic (same radius -> identical graph)',
roadGraph.nodeCount() === roadGraph2.nodeCount(), 'nodes=' + roadGraph.nodeCount());
const conn = roadGraph.connected();
check('roadGraph whole network is one connected component from origin',
conn.connected, `reached ${conn.reached}/${conn.total}`);
const path = roadGraph.shortestPath(roadGraph.centerKey, roadGraph.key(10, 0));
check('roadGraph routes origin -> highway via shortest path',
Array.isArray(path) && path.length > 1, (path && path.length) + ' hops');
const turns = roadGraph.turnsAt(roadGraph.key(3, 0), 'x');
check('roadGraph offers perpendicular turns at city intersections',