Auto-Build advisor + resilient simulation loop

- New advisor button (toolbar, purple wand): one click per planning pass
  * Genesis: founds a town from a blank map (main street + zone bands)
  * Keeps power ahead of demand (wind/solar/coal by deficit size)
  * BFS road spurs to reach orphaned zoned land
  * Stamps new districts when demand is high and empty plots run out
  * Places the weakest service coverage; parks/plazas for land value
  * Never spends below an emergency reserve; recomputes power per action
- Simulation no longer dies when rAF throttles: interval watchdog
  compensates only after 3s of total frame silence
- preserveDrawingBuffer now opt-in via ?cap=1 (software-GL perf)
- Smoke test drives via game API, prefers headless_shell binary,
  tolerates stalled screenshots; engine suite at 51 assertions
This commit is contained in:
PolyCity
2026-08-23 03:28:17 +00:00
parent 3266b8d640
commit a54f04ed5f
17 changed files with 482 additions and 37 deletions
+37
View File
@@ -4,6 +4,7 @@
* brownouts, save/load integrity and milestones.
*/
import { City } from '../src/game/city.js';
import { AutoBuilder } from '../src/game/autobuilder.js';
import { STRUCT, ZONE, SIM } from '../src/config.js';
function findSpot(city, w = 1, h = 1, startX = 8, startZ = 34) {
@@ -255,5 +256,41 @@ console.log('— save / load integrity —');
ok(!spawnedWhileOff, 'disabled flag prevents tornado spawns');
}
// ---------------- auto-builder advisor ----------------
{
// blank-map genesis
const c0 = new City(777777, 'Blankville');
const a0 = new AutoBuilder(c0);
const acts0 = a0.run();
const roadCount0 = [...c0.grid.struct].filter(v => v === STRUCT.ROAD).length;
ok(roadCount0 >= 10 && acts0.length > 0, `genesis builds a town from nothing (${acts0[0] || 'nothing'})`);
a0.run(); c0.tick();
const plantOnBlank = [...c0.grid.struct].some(v => v >= STRUCT.COAL && v <= STRUCT.WIND);
ok(plantOnBlank, 'second pass powers the new town');
}
{
const c = new City(424242, 'AutoTown');
for (let x = 10; x < 42; x++) c.placeStruct(STRUCT.ROAD, x, 30);
const tiles = [];
for (let x = 11; x < 32; x++) for (let z = 27; z <= 33; z++) tiles.push([x, z]);
c.placeZone(ZONE.RES, tiles);
c.placeZone(ZONE.COM, [[33, 27], [34, 28], [35, 29], [36, 30], [37, 31], [38, 32]]);
const ab = new AutoBuilder(c);
const moneyBefore = c.money;
const actions1 = ab.run();
ok(actions1.length > 0, `advisor acts on a needy town (${actions1.join('; ')})`);
const hasPlant = [...c.grid.struct].some(v =>
v === STRUCT.COAL || v === STRUCT.SOLAR || v === STRUCT.WIND);
if (hasPlant) c.tick();
ok(c.stats.powerCap > 0, 'advisor secured a working power plant');
const spent = moneyBefore - c.money;
ok(spent > 0 && c.money >= ab.reserve - 1, `advisor respects the emergency reserve (spent $${spent}, left $${Math.round(c.money)})`);
// idempotent-ish: immediately running again with everything satisfied does little/nothing
const actions2 = ab.run();
ok(actions2.length <= 2, `second pass is conservative (${actions2.length} actions)`);
}
console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);