This commit is contained in:
Rainer Leit
2026-09-25 17:02:24 +03:00
parent cc43ed8dc8
commit 9597629951
2149 changed files with 460234 additions and 1770 deletions
+164
View File
@@ -0,0 +1,164 @@
/**
* Autonomous terrain tuning script.
*
* Runs the app headlessly with fixed seeds, collects metrics, saves results.
* Designed to be driven by Claude Code — modify terrain-config.js between runs.
*
* Usage: node tuning/auto-tune.mjs [label]
* Output: tuning/results/<label>.json with metrics from all seeds
*/
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import puppeteer from 'puppeteer';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = path.resolve(__dirname, '..');
const RESULTS_DIR = path.join(__dirname, 'results');
fs.mkdirSync(RESULTS_DIR, { recursive: true });
const label = process.argv[2] || `run-${Date.now()}`;
const MIME = {
'.html': 'text/html', '.js': 'application/javascript', '.mjs': 'application/javascript',
'.css': 'text/css', '.json': 'application/json', '.png': 'image/png',
'.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.wasm': 'application/wasm',
'.txt': 'text/plain', '.xml': 'application/xml',
};
function startServer() {
return new Promise((resolve) => {
const server = http.createServer((req, res) => {
let urlPath = decodeURIComponent(new URL(req.url, 'http://localhost').pathname);
if (urlPath === '/' || urlPath === '') urlPath = '/index.html';
const filePath = path.join(PROJECT_ROOT, urlPath);
if (!filePath.startsWith(PROJECT_ROOT)) { res.writeHead(403); res.end(); return; }
fs.readFile(filePath, (err, data) => {
if (err) { res.writeHead(404); res.end('Not found'); return; }
const ext = path.extname(filePath).toLowerCase();
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
res.end(data);
});
});
server.listen(0, '127.0.0.1', () => {
resolve({ server, port: server.address().port });
});
});
}
// Seeds chosen for diversity: different plate configs, land coverage, etc.
const SEEDS = [42, 100, 200, 300, 400];
async function runSeed(browser, baseUrl, seed) {
const page = await browser.newPage();
await page.setViewport({ width: 1200, height: 900 });
page.on('dialog', (d) => d.dismiss());
page.on('pageerror', () => {}); // suppress
try {
await page.goto(baseUrl, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await new Promise((r) => setTimeout(r, 2000));
// Dismiss overlays
await page.evaluate(() => {
for (const id of ['tutorialOverlay', 'whatsNewOverlay']) {
const el = document.getElementById(id);
if (el) el.style.display = 'none';
}
});
// Set low detail for speed
await page.evaluate(() => {
const el = document.getElementById('sN');
el.value = 400;
el.dispatchEvent(new Event('input', { bubbles: true }));
});
// Patch seed
await page.evaluate((s) => {
const origPost = Worker.prototype.postMessage;
Worker.prototype.postMessage = function(msg, ...rest) {
if (msg && msg.cmd === 'generate' && msg.seed === undefined) msg.seed = Number(s);
return origPost.call(this, msg, ...rest);
};
}, seed);
// Generate
const genDone = page.evaluate((timeout) => {
return new Promise((resolve, reject) => {
const btn = document.getElementById('generate');
const timer = setTimeout(() => reject(new Error('Generation timed out')), timeout);
btn.addEventListener('generate-done', () => { clearTimeout(timer); resolve(); }, { once: true });
});
}, 120_000);
await new Promise((r) => setTimeout(r, 100));
await page.click('#generate');
await genDone;
await new Promise((r) => setTimeout(r, 500));
const metrics = await page.evaluate(() => window.__terrainMetrics);
return { seed, metrics: metrics || { _error: 'no metrics' } };
} finally {
await page.close().catch(() => {});
}
}
async function main() {
const { server, port } = await startServer();
const baseUrl = `http://127.0.0.1:${port}`;
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--enable-webgl',
'--use-gl=angle', '--use-angle=swiftshader-webgl', '--enable-unsafe-swiftshader'],
});
const results = [];
const t0 = performance.now();
try {
for (const seed of SEEDS) {
const r = await runSeed(browser, baseUrl, seed);
results.push(r);
process.stdout.write(` seed ${seed}: ${r.metrics._error ? 'ERROR' : 'OK'} (${(r.metrics._metrics_ms || 0).toFixed(0)}ms metrics)\n`);
}
} finally {
await browser.close();
server.close();
}
const elapsed = ((performance.now() - t0) / 1000).toFixed(1);
// Compute cross-seed averages for key metrics
const validMetrics = results.filter(r => !r.metrics._error).map(r => r.metrics);
const avg = {};
if (validMetrics.length > 0) {
const keys = Object.keys(validMetrics[0]).filter(k => !k.startsWith('_') && typeof validMetrics[0][k] === 'number');
for (const k of keys) {
const vals = validMetrics.map(m => m[k]).filter(v => v != null && !isNaN(v));
avg[k] = vals.length > 0 ? +(vals.reduce((a, b) => a + b, 0) / vals.length).toFixed(4) : null;
}
}
const output = { label, elapsed_s: +elapsed, seeds: SEEDS, results, averages: avg };
const outPath = path.join(RESULTS_DIR, `${label}.json`);
fs.writeFileSync(outPath, JSON.stringify(output, null, 2));
console.log(`\nResults saved: ${outPath} (${elapsed}s total)`);
// Print summary
console.log('\n=== Cross-seed Averages ===');
const highlight = [
'continent_count', 'island_count_total', 'flat_ocean_plate_land_fraction',
'relief_headroom', 'coast_complexity_index', 'hypsometry_trough_depth',
'mountain_boundary_ratio', 'orogenic_elev_correlation', 'erosion_slope_correlation',
'coastal_lowland_fraction', 'land_band_500m_plus_frac',
'shelf_width_passive_km', 'shelf_width_active_km',
];
for (const k of highlight) {
if (avg[k] != null) console.log(` ${k}: ${avg[k]}`);
}
}
main().catch((err) => { console.error(err); process.exit(1); });