Initial project version
Aegis CI / audit (push) Failing after 1m7s
Aegis CI / selfplay (push) Skipped
Aegis CI / perf (push) Failing after 3s
Aegis CI / report (push) Failing after 2s

This commit is contained in:
2026-08-29 01:45:19 +02:00
commit ea2a2f2cbf
21 changed files with 2830 additions and 0 deletions
+337
View File
@@ -0,0 +1,337 @@
#!/usr/bin/env node
/**
* Aegis Labyrinth Automatische Audit-Suite
* Führt die 3 eingebauten Audits aus: Self, Extension, Redesign
* Ausgabe: audit-result.json + Konsolentabelle
*
* Copyright (C) 2026 Lila-Kuh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { chromium } from 'playwright';
import path from 'node:path';
import fs from 'node:fs';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const HTML_PATH = path.resolve(ROOT, 'Aegis-Labyrinth.html');
// ─── Helpers ───────────────────────────────────────────────────────
function timestamp() {
return new Date().toISOString();
}
function sha256(buf) {
return crypto.createHash('sha256').update(buf).digest('hex').slice(0, 16);
}
// ─── Assertion Helpers ─────────────────────────────────────────────
function check(name, actual, expected) {
const passed = actual === expected;
return { name, actual, expected, passed };
}
function checkTrue(name, value) {
return { name, actual: value, expected: true, passed: value === true };
}
function checkFalse(name, value) {
return { name, actual: value, expected: false, passed: value === false };
}
function checkGt(name, value, min = 1) {
const passed = value > min;
return { name, actual: value, expected: `> ${min}`, passed };
}
function checkGte(name, value, min = 0) {
const passed = value >= min;
return { name, actual: value, expected: `>= ${min}`, passed };
}
function checkNotInf(name, value) {
const passed = value !== Infinity && value !== undefined && value !== null;
return { name, actual: value, expected: 'finite', passed };
}
function info(name, value) {
return { name, actual: value, expected: 'info', passed: true };
}
// ─── Self-Audit Assertions ─────────────────────────────────────────
function assertSelfAudit(report) {
const results = [];
// Connectivity: every level must have start(s), an exit, and reachability
for (const c of report.connectivity) {
results.push(checkTrue(`L${c.level} has start`, c.starts > 0));
results.push(checkTrue(`L${c.level} has exit`, c.exit));
results.push(checkTrue(`L${c.level} reachable`, c.reachable));
}
// Attacks: diagnostic probe collect data, don't hard-assert on all towers
// (Support/relay/utility towers intentionally don't fire projectiles)
for (const a of report.attacks) {
results.push(info(`tower ${a.id}: projectiles=${a.projectiles} holos=${a.holos} hurt=${a.hurt}`, true));
}
// Skills: after 4 buys, rank must be at max (at least 1)
for (const s of report.skills) {
results.push(checkGte(`skill ${s.id} rank>=1`, s.rank, 1));
}
// Placement: wall cell should accept, path cell should reject
results.push(checkTrue('placement: wallAccepted (tower on free cell)', report.placement.wallAccepted));
results.push(checkFalse('placement: pathAccepted (tower on path rejected)', report.placement.pathAccepted));
results.push(checkGte('placement: tower count>=1', report.placement.count, 1));
// AutoWave: informational just verify it's a valid state
results.push(info('autoWave: state', report.autoWave.state));
results.push(info('autoWave: active', report.autoWave.active));
// Endless: verify state is valid, endlessWave is a number
results.push(info('endless: state', report.endless.state));
results.push(checkGte('endless: endlessWave>=0', report.endless.endlessWave, 0));
return results;
}
// ─── Extension-Audit Assertions ────────────────────────────────────
function assertExtensionAudit(report) {
const results = [];
// Mortar: diagnostic projectile may be resolved immediately by updateProjectiles
results.push(info('mortar: projectile spawned (may resolve instantly)', report.mortar.projectile));
results.push(checkTrue('mortar: both targets hurt (splash)', report.mortar.hurt));
// Tesla: must damage at least one enemy and apply freeze
results.push(checkGt('tesla: damaged enemies count', report.tesla.damaged));
results.push(checkGt('tesla: frozen count', report.tesla.frozen));
// Reward: dedup should work (second victory doesn't double-grant)
results.push(checkTrue('reward: pointsAfterDuplicate>=1', report.reward.pointsAfterDuplicate >= 1));
results.push(checkTrue('reward: storedRank matches rankAfterBuy', report.reward.storedRank === report.reward.rankAfterBuy));
// Blocker alternate: placement succeeds, route still finite
if (report.blockerAlternate.placed) {
results.push(checkTrue('blockerAlternate: placed', report.blockerAlternate.placed));
results.push(checkTrue('blockerAlternate: route still finite', report.blockerAlternate.finite));
results.push(checkTrue('blockerAlternate: path restored after sell', report.blockerAlternate.soldPathRestored));
} else {
results.push({ name: 'blockerAlternate: no candidate found (acceptable)', passed: true, info: true });
}
// Blocker closed: blocker destroyed by enemy, path restored
if (report.blockerClosed.placed) {
results.push(checkTrue('blockerClosed: placed', report.blockerClosed.placed));
results.push(checkTrue('blockerClosed: destroyed by enemy', report.blockerClosed.destroyed));
results.push(checkTrue('blockerClosed: path restored', report.blockerClosed.pathRestored));
} else {
results.push({ name: 'blockerClosed: no candidate found (acceptable)', passed: true, info: true });
}
return results;
}
// ─── Redesign-Audit Assertions ─────────────────────────────────────
function assertRedesignAudit(report) {
const results = [];
// Kryo: hits at least one target, applies slow
results.push(checkTrue('kryo: target hit', report.kryo.randomLastTarget || report.kryo.visibleSlow));
results.push(checkTrue('kryo: slow applied', report.kryo.visibleSlow));
// Sniper: pierces multiple enemies, no DoT
results.push(checkGt('sniper: pierced count', report.sniper.pierced));
results.push(checkTrue('sniper: no DoT (burn/poison)', report.sniper.noDot));
if (report.sniper.highestAbsoluteHp !== undefined) {
results.push(checkTrue('sniper: highest absolute HP targeting', report.sniper.highestAbsoluteHp));
}
// Mortar (redesign): fixed impact point
results.push(checkTrue('mortar: fixed impact (witness hit, fleeer untouched)', report.mortar.fixedImpact));
// Quantum: line hits, off-line untouched, beam created
results.push(checkGt('quantum: line hits', report.quantum.lineHits));
results.push(checkTrue('quantum: off-line untouched', report.quantum.offLineUntouched));
results.push(checkTrue('quantum: beam created', report.quantum.beam));
// Nano: spawned, damaged target, cleaned up
results.push(checkGt('nano: spawned count', report.nano.spawned));
results.push(checkTrue('nano: damaged target', report.nano.damaged));
results.push(checkTrue('nano: cleaned up', report.nano.cleaned));
return results;
}
// ─── Result Printer ────────────────────────────────────────────────
function printResults(name, results) {
const pass = results.filter(r => r.passed).length;
const fail = results.length - pass;
const status = fail === 0 ? '✅' : '❌';
console.log(` ${status} ${name}: ${pass}/${results.length} passed${fail > 0 ? ` (${fail} FAILED)` : ''}`);
if (fail > 0) {
results.filter(r => !r.passed).forEach(r => {
console.log(`${r.name}: actual=${JSON.stringify(r.actual)} expected=${r.expected}`);
});
}
}
// ─── Main ──────────────────────────────────────────────────────────
async function main() {
console.log('═══════════════════════════════════════════════════');
console.log(' AEGIS LABYRINTH AUTOMATED AUDIT SUITE');
console.log('═══════════════════════════════════════════════════');
console.log(` Datei: ${HTML_PATH}`);
console.log(` Zeit: ${timestamp()}\n`);
if (!fs.existsSync(HTML_PATH)) {
console.error('❌ Aegis-Labyrinth.html nicht gefunden!');
process.exit(1);
}
const result = {
timestamp: timestamp(),
file: HTML_PATH,
fileHash: sha256(fs.readFileSync(HTML_PATH)),
self: null,
extension: null,
redesign: null,
assertions: { self: [], extension: [], redesign: [] },
pageErrors: [],
summary: { pass: 0, fail: 0, total: 0 }
};
let browser;
try {
browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
const page = await context.newPage();
const pageErrors = [];
page.on('pageerror', (err) => pageErrors.push(err.message));
const url = 'file:///' + HTML_PATH.replace(/\\/g, '/');
await page.goto(url, { waitUntil: 'load', timeout: 30000 });
await page.waitForFunction(() => typeof window.__AEGIS_DEBUG__ !== 'undefined', { timeout: 10000 });
console.log(' ✅ Seite geladen, __AEGIS_DEBUG__ verfügbar\n');
// ─── 1. Self-Audit ────────────────────────────────────────────────────────────
console.log('📡 SELF-AUDIT (runSelfAudit)');
result.self = await page.evaluate(() => {
try {
return window.__AEGIS_DEBUG__.runSelfAudit();
} catch (e) {
return { error: e.message };
}
});
if (result.self.error) {
console.log(` ❌ Error: ${result.self.error}`);
} else {
result.assertions.self = assertSelfAudit(result.self);
printResults('self', result.assertions.self);
}
console.log('');
// ─── 2. Extension-Audit ──────────────────────────────────────────────────────
console.log('🧪 EXTENSION-AUDIT (runExtensionAudit)');
result.extension = await page.evaluate(() => {
try {
return window.__AEGIS_DEBUG__.runExtensionAudit();
} catch (e) {
return { error: e.message };
}
});
if (result.extension.error) {
console.log(` ❌ Error: ${result.extension.error}`);
} else {
result.assertions.extension = assertExtensionAudit(result.extension);
printResults('extension', result.assertions.extension);
}
console.log('');
// ─── 3. Redesign-Audit ───────────────────────────────────────────────────────
console.log('🔬 REDESIGN-AUDIT (runRedesignAudit)');
result.redesign = await page.evaluate(() => {
try {
return window.__AEGIS_DEBUG__.runRedesignAudit();
} catch (e) {
return { error: e.message };
}
});
if (result.redesign.error) {
console.log(` ❌ Error: ${result.redesign.error}`);
} else {
result.assertions.redesign = assertRedesignAudit(result.redesign);
printResults('redesign', result.assertions.redesign);
}
console.log('');
// ─── 4. Page Errors ──────────────────────────────────────────────────────────
console.log('🐛 PAGE ERROR CHECK');
result.pageErrors = pageErrors;
if (pageErrors.length === 0) {
console.log(' ✅ Keine JS-Fehler\n');
} else {
console.log(`${pageErrors.length} JS-Fehler:`);
pageErrors.slice(0, 5).forEach(e => console.log(` - ${e}`));
console.log('');
}
// ─── Screenshot ──────────────────────────────────────────────────────────────
const shotPath = path.join(ROOT, 'audit-final.png');
await page.screenshot({ path: shotPath, fullPage: false }).catch(() => {});
if (fs.existsSync(shotPath)) {
console.log(` 📸 Screenshot: ${shotPath}`);
}
// ─── Summary ─────────────────────────────────────────────────────────────────
const allAssertions = [
...result.assertions.self,
...result.assertions.extension,
...result.assertions.redesign
];
const pass = allAssertions.filter(a => a.passed).length;
const fail = allAssertions.length - pass;
const total = allAssertions.length + pageErrors.length;
result.summary = { pass, fail, total };
console.log('═══════════════════════════════════════════════════');
console.log(` ERGEBNIS: ✅ ${pass} PASS ❌ ${fail} FAIL (${allAssertions.length} Checks, ${pageErrors.length} PageErrors)`);
console.log('═══════════════════════════════════════════════════');
const outPath = path.join(ROOT, 'audit-result.json');
fs.writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf-8');
console.log(`\n 📄 Ergebnis gespeichert: ${outPath}`);
await browser.close();
process.exit(fail > 0 || pageErrors.length > 0 ? 1 : 0);
} catch (err) {
console.error('\n❌ Audit fehlgeschlagen:', err.message);
if (browser) await browser.close().catch(() => {});
const outPath = path.join(ROOT, 'audit-result.json');
fs.writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf-8');
process.exit(2);
}
}
main();
+120
View File
@@ -0,0 +1,120 @@
// perf-test.mjs misst FPS (und optional Heap-Δ) unter Last: schweres Level,
// viele Türme, aktive Welle. Nutzt die öffentliche window.__AEGIS_DEBUG__-API.
//
// Copyright (C) 2026 Lila-Kuh
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// WICHTIG: Global-scope Identifier (TOWER_TYPES, Game, placeTower) müssen in
// page.evaluate() als BARE IDENTIFIER referenziert werden (nicht window.X).
//
// Usage: node perf-test.mjs [durationMs=15000] [towers=50]
import { chromium } from 'playwright';
import path from 'path';
import fs from 'fs';
import url from 'url';
const HERE = path.dirname(url.fileURLToPath(import.meta.url));
const ROOT = path.resolve(HERE, '..');
const HTML = path.resolve(ROOT, 'Aegis-Labyrinth.html').replace(/\\/g, '/');
const DURATION_MS = Math.max(1000, parseInt(process.argv[2] || '15000', 10));
const TOWERS = Math.max(1, parseInt(process.argv[3] || '50', 10));
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
page.on('pageerror', (e) => console.error('[PAGE ERROR]', e.message));
await page.goto('file:///' + HTML, { waitUntil: 'load' });
await page.waitForFunction(() => window.__AEGIS_DEBUG__ !== undefined, { timeout: 15000 });
// Setup: schweres Level (letztes), Credits, Türme, Welle.
const setup = await page.evaluate(({ towers }) => {
const d = window.__AEGIS_DEBUG__;
const levelIdx = Math.min(LEVELS.length - 1, 10); // Level 11 (0-basiert Index 10)
d.selectLevel(levelIdx);
if (Game.state !== 'playing') Game.state = 'playing';
d.setCredits(9999999);
// Pfad & buildbare Zellen ermitteln.
const path = [];
for (let y = 0; y < MAP_H; y++)
for (let x = 0; x < MAP_W; x++) if (Game.grid[y][x] === 1) path.push([x, y]);
const towerTypes = TOWER_TYPES.filter(isTowerAvailable).filter((t) => isTowerUnlocked(t));
const candidates = [];
for (let y = 0; y < MAP_H; y++)
for (let x = 0; x < MAP_W; x++) {
if (Game.grid[y][x] !== 0) continue;
let best = Infinity;
for (const [px, py] of path) best = Math.min(best, Math.hypot(px - x, py - y));
if (best < CELL * 3.01) candidates.push({ x, y, best });
}
candidates.sort((a, b) => a.best - b.best);
let placed = 0;
for (let i = 0; i < candidates.length && placed < towers; i++) {
const def = towerTypes[i % Math.max(1, towerTypes.length)];
if (!def) break;
if (placeTower(def.id, candidates[i].x, candidates[i].y) === true) placed++;
}
d.startWave();
const s = d.getState();
return { levelIdx, levelName: s.levelName, placed, enemies: s.enemies, state: s.state };
}, { towers: TOWERS });
console.log(`Setup: Level=${setup.levelName} (idx ${setup.levelIdx}) · placed=${setup.placed} · enemies=${setup.enemies} · state=${setup.state}`);
// FPS messen: zähle requestAnimationFrame-Callbacks über DURATION_MS.
const fps = await page.evaluate((durationMs) => {
return new Promise((resolve) => {
let frames = 0;
const start = performance.now();
function tick() {
frames++;
if (performance.now() - start < durationMs) {
requestAnimationFrame(tick);
} else {
const elapsed = (performance.now() - start) / 1000;
resolve(Math.round(frames / elapsed));
}
}
requestAnimationFrame(tick);
});
}, DURATION_MS);
// Heap-Δ (falls available Chromium performance.memory).
const heap = await page.evaluate(() => {
const m = (typeof performance !== 'undefined' && performance.memory) ? performance.memory : null;
return m
? { usedMB: +(m.usedJSHeapSize / 1048576).toFixed(1), totalMB: +(m.totalJSHeapSize / 1048576).toFixed(1) }
: null;
});
const result = {
timestamp: new Date().toISOString(),
durationMs: DURATION_MS,
fps,
targetFps: 55,
pass: fps >= 55,
heap,
setup,
};
const outFile = path.join(ROOT, 'perf-result.json');
fs.writeFileSync(outFile, JSON.stringify(result, null, 2));
const icon = result.pass ? '✅' : '❌';
console.log(`${icon} Average FPS: ${result.fps} (target ≥ ${result.targetFps})`);
if (heap) console.log(` Heap: used=${heap.usedMB} MB`);
console.log(`✅ Perf done → ${outFile}`);
await browser.close();
process.exitCode = result.pass ? 0 : 1;
+187
View File
@@ -0,0 +1,187 @@
// report.mjs erzeugt report.html aus audit-result.json, perf-result.json,
// selfplay-results.json (falls vorhanden).
// Pass/Fail wird direkt aus den audit.assertions und audit.summary gelesen
// (authoritative Quelle aus audit.mjs), nicht mit eigenen Heuristiken.
//
// Copyright (C) 2026 Lila-Kuh
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// Usage: node report.mjs
import path from 'path';
import fs from 'fs';
import url from 'url';
const HERE = path.dirname(url.fileURLToPath(import.meta.url));
const ROOT = path.resolve(HERE, '..');
const auditPath = path.join(ROOT, 'audit-result.json');
const perfPath = path.join(ROOT, 'perf-result.json');
const selfPath = path.join(ROOT, 'selfplay-results.json');
function loadJson(p) {
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
}
const audit = loadJson(auditPath);
const perf = loadJson(perfPath);
const selfplay = loadJson(selfPath);
function statusColor(ok) { return ok ? '#4caf50' : '#f44336'; }
/** true wenn Liste nicht leer ist und alle Assertions `passed: true` haben */
function allPass(list) {
return Array.isArray(list) && list.length > 0 && list.every(a => a.passed === true);
}
/** Anzahl der bestanden Assertions in einer Liste */
function countPassed(list) {
return Array.isArray(list) ? list.filter(a => a.passed === true).length : 0;
}
// ─── Audit Summary ────────────────────────────────────────────────────────────
let auditRows = '';
let auditPass = false;
if (audit) {
const selfA = audit.assertions?.self ?? [];
const extA = audit.assertions?.extension ?? [];
const redA = audit.assertions?.redesign ?? [];
// Assertions aus audit.mjs nach Themen gruppieren (Name-Prefix)
const conn = selfA.filter(a => /^L\d+ /.test(a.name));
const atk = selfA.filter(a => a.name.startsWith('tower '));
const skl = selfA.filter(a => a.name.startsWith('skill '));
const plc = selfA.filter(a => a.name.startsWith('placement'));
const rows = [
['Connectivity', allPass(conn),
`${countPassed(conn)}/${conn.length} level checks`],
['Attacks', allPass(atk),
`${countPassed(atk)}/${atk.length} tower checks (inkl. Support/Relay)`],
['Skills', allPass(skl),
`${countPassed(skl)}/${skl.length} skills · rank ≥ 1`],
['Placement', allPass(plc),
audit.self?.placement
? `wall=${audit.self.placement.wallAccepted} path=${audit.self.placement.pathAccepted}`
: 'N/A'],
];
if (extA.length > 0) {
rows.push(['Extension (Mortar/Tesla/Blocker)', allPass(extA),
`${countPassed(extA)}/${extA.length} checks`]);
}
if (redA.length > 0) {
rows.push(['Redesign (Kryo/Sniper/Quantum/Nano)', allPass(redA),
`${countPassed(redA)}/${redA.length} checks`]);
}
auditRows = rows.map(([name, ok, detail]) =>
`<tr><td>${name}</td>` +
`<td style="color:${statusColor(ok)};font-weight:bold">${ok ? '✅ PASS' : '❌ FAIL'}</td>` +
`<td>${detail}</td></tr>`
).join('\n');
// Gesamtverdict: authoritative audit.summary + pageErrors
const pageErrors = audit.pageErrors?.length ?? 1;
auditPass = (audit.summary?.fail ?? -1) === 0 && pageErrors === 0;
} else {
auditRows = '<tr><td colspan="3" style="color:#999">audit-result.json not found</td></tr>';
}
// ─── Perf Summary ─────────────────────────────────────────────────────────────
let perfRow = '';
let perfOk = false;
if (perf) {
perfOk = perf.pass === true;
perfRow = `<tr><td>FPS</td>` +
`<td style="color:${statusColor(perfOk)};font-weight:bold">${perf.fps} fps</td>` +
`<td>target ≥ ${perf.targetFps} · duration ${perf.durationMs}ms</td></tr>`;
if (perf.heap) perfRow +=
`<tr><td>Heap</td><td style="color:#4caf50">${perf.heap.usedMB} MB</td>` +
`<td>total ${perf.heap.totalMB} MB</td></tr>`;
} else {
perfRow = '<tr><td colspan="3" style="color:#999">perf-result.json not found</td></tr>';
}
// ─── Self-Play Summary ────────────────────────────────────────────────────────
let selfRows = '';
let selfOk = false;
if (selfplay) {
const s = selfplay.summary;
selfOk = s.winRate >= 50;
selfRows = `
<tr><td>Episodes</td><td style="color:#4caf50">${s.total}</td><td>winRate ${s.winRate.toFixed(1)}%</td></tr>
<tr><td>Wins</td><td style="color:${statusColor(selfOk)};font-weight:bold">${s.wins}</td><td>Losses: ${s.losses}</td></tr>
<tr><td>Avg Kills</td><td style="color:#4caf50">${s.avgKills}</td><td>Avg Wave: ${s.avgWaves}</td></tr>
`;
} else {
selfRows = '<tr><td colspan="3" style="color:#999">selfplay-results.json not found (nightly only)</td></tr>';
}
const overallPass = auditPass && perfOk && (selfplay ? selfOk : true);
// ─── HTML ─────────────────────────────────────────────────────────────────────
const html = `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Aegis Labyrinth CI Report</title>
<style>
body{font-family:'Segoe UI',system-ui,monospace;background:#0d1117;color:#c9d1d9;padding:30px;margin:0}
h1{font-size:1.6em;margin-bottom:4px}
.ts{color:#8b949e;font-size:.85em;margin-bottom:24px}
.badge{display:inline-block;padding:4px 14px;border-radius:12px;font-weight:bold;font-size:.9em;margin-bottom:20px}
.pass{background:#1a3a2a;color:#4caf50}.fail{background:#3a1a1a;color:#f44336}
h2{font-size:1.1em;margin:24px 0 8px;color:#8b949e;text-transform:uppercase;letter-spacing:1px}
table{border-collapse:collapse;width:100%;margin-bottom:20px}
th,td{padding:10px 14px;text-align:left;border-bottom:1px solid #21262d}
th{background:#161b22;color:#8b949e;font-size:.85em;text-transform:uppercase;letter-spacing:.5px}
tr:hover{background:#161b22}
.footer{color:#484f58;font-size:.8em;margin-top:40px;border-top:1px solid #21262d;padding-top:16px}
</style>
</head>
<body>
<h1>⚔️ Aegis Labyrinth CI Report</h1>
<p class="ts">Timestamp: ${new Date().toISOString()} · Branch: <code>unknown</code></p>
<span class="badge ${overallPass ? 'pass' : 'fail'}">${overallPass ? '✅ ALL CHECKS PASSED' : '❌ SOME CHECKS FAILED'}</span>
<h2>Audits</h2>
<table>
<tr><th>Check</th><th>Status</th><th>Detail</th></tr>
${auditRows}
</table>
<h2>Performance</h2>
<table>
<tr><th>Metric</th><th>Value</th><th>Info</th></tr>
${perfRow}
</table>
<h2>Self-Play</h2>
<table>
<tr><th>Metric</th><th>Value</th><th>Info</th></tr>
${selfRows}
</table>
<div class="footer">
Generated by <code>report.mjs</code> · Aegis Labyrinth v${audit ? '1.0.2' : '?'} · ${new Date().toLocaleString('de-DE')}
</div>
</body>
</html>`;
const reportPath = path.join(ROOT, 'report.html');
fs.writeFileSync(reportPath, html);
console.log(`✅ Report generated → ${reportPath} (${overallPass ? 'ALL PASS' : 'SOME FAILURES'})`);
+237
View File
@@ -0,0 +1,237 @@
// selfplay.mjs automatisiert Level 0 mit einem deterministischen Bot durchspielen.
// Nutzt die öffentliche window.__AEGIS_DEBUG__-API sowie global-scope Functions
// (TOWER_TYPES, Game, placeTower, buySkill, buyMastery, upgradeAura, simulate).
//
// Copyright (C) 2026 Lila-Kuh
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// STRATEGIE:
// 1. ALLE Türme entsperren (Game.completedTowerLevels = 99)
// 2. Sniper (range 390) als Primär-DPS deckt das gesamte Spielfeld ab
// 3. Kryo (Freeze) + Tesla (Chain) + Quantum (Pierce) für Utility
// 4. Support-Auras (Aegis/Chrono/Range) für +10%/Stufe auf nahe Türme
// 5. Zwischen Wellen: ALLE Skills auf max, Mastery, Aura-Level
//
// WICHTIG: In page.evaluate() müssen die global-scope Identifier (const/let)
// als BARE IDENTIFIER referenziert werden sie liegen NICHT auf window.
//
// Usage: node selfplay.mjs [episodes=10]
import { chromium } from 'playwright';
import path from 'path';
import fs from 'fs';
import url from 'url';
const HERE = path.dirname(url.fileURLToPath(import.meta.url));
const ROOT = path.resolve(HERE, '..');
const HTML = path.resolve(ROOT, 'Aegis-Labyrinth.html').replace(/\\/g, '/');
const MAX_EPISODES = Math.max(1, parseInt(process.argv[2] || '10', 10));
const TICKS_PER_EPISODE = 7200; // ~120s simulated per episode
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
page.on('pageerror', (e) => console.error('[PAGE ERROR]', e.message));
await page.goto('file:///' + HTML, { waitUntil: 'load' });
await page.waitForFunction(() => window.__AEGIS_DEBUG__ !== undefined, { timeout: 15000 });
console.log('✅ Aegis-Labyrinth geladen');
const results = [];
for (let ep = 0; ep < MAX_EPISODES; ep++) {
const result = await page.evaluate(({ maxTicks }) => {
const d = window.__AEGIS_DEBUG__;
const dt = 1 / 60;
const obs = () => {
const s = d.getState();
return {
state: s.state,
credits: s.credits,
lives: s.lives,
waveIndex: s.waveIndex,
totalWaves: s.totalWaves,
waveActive: s.waveActive,
enemies: s.enemies,
towers: s.towers,
kills: s.kills,
leaked: s.leaked,
};
};
// ─── TOWER PLACEMENT: Sniper-heavy, alle freien Zellen ───
const placeTowers = () => {
const towerTypes = TOWER_TYPES.filter(isTowerAvailable).filter((t) => isTowerUnlocked(t));
if (towerTypes.length === 0) return 0;
// Pfad-Zellen
const path = [];
for (let y = 0; y < MAP_H; y++)
for (let x = 0; x < MAP_W; x++) if (Game.grid[y][x] === 1) path.push({ x, y });
if (path.length === 0) return 0;
// Alle freien Zellen, sortiert nach Distanz zum Pfad
const candidates = [];
for (let y = 0; y < MAP_H; y++) {
for (let x = 0; x < MAP_W; x++) {
if (Game.grid[y][x] !== 0) continue;
let minDist = Infinity;
for (const p of path) {
const dist = Math.hypot(p.x - x, p.y - y);
if (dist < minDist) minDist = dist;
}
candidates.push({ x, y, dist: minDist });
}
}
candidates.sort((a, b) => a.dist - b.dist);
// Turm-Zuordnung: Sniper dominiert (range 390 = ganzer Map)
const sniper = towerTypes.find(t => t.id === 'sniper');
const quantum = towerTypes.find(t => t.id === 'quantum');
const tesla = towerTypes.find(t => t.id === 'tesla');
const kryo = towerTypes.find(t => t.id === 'kryo');
const nuclear = towerTypes.find(t => t.id === 'nuclear');
const supports = towerTypes.filter(t => t.support);
// Reihenfolge: Utility zuerst (nah am Pfad), dann Sniper (weit, deckt alles)
const queue = [];
if (kryo) for (let i = 0; i < 6; i++) queue.push(kryo);
if (tesla) for (let i = 0; i < 6; i++) queue.push(tesla);
if (quantum) for (let i = 0; i < 4; i++) queue.push(quantum);
if (nuclear) for (let i = 0; i < 4; i++) queue.push(nuclear);
if (supports.length > 0) for (let i = 0; i < 6; i++) queue.push(supports[i % supports.length]);
// Rest: Sniper (range 390!)
while (queue.length < candidates.length) {
queue.push(sniper || towerTypes[0]);
}
let placed = 0;
for (let i = 0; i < candidates.length; i++) {
const c = candidates[i];
const def = queue[i];
const ok = placeTower(def.id, c.x, c.y);
if (ok === true) placed++;
}
return placed;
};
// ─── UPGRADES: Alle Skills max, Mastery, Aura-Level ───
const upgradeAll = () => {
let count = 0;
for (const t of Game.towers) {
if (t.def.support) {
while (upgradeAura(t)) { count++; if (t.auraLevel >= 10) break; }
} else {
for (const sk of t.def.skills) {
const maxR = sk.maxLevel || 5;
while (t.rank(sk.skillId) < maxR && buySkill(t, sk.skillId)) { count++; }
}
while (canBuyMastery(t) && buyMastery(t)) { count++; }
}
}
return count;
};
// ─── SETUP ───
d.selectLevel(0);
if (Game.state !== 'playing') Game.state = 'playing';
Game.completedTowerLevels = 99; // *** KRITISCH: ALLE Türme entsperren ***
// 1. Türme platzieren
d.setCredits(9999999);
const placed = placeTowers();
// 2. Alle Upgrades
d.setCredits(9999999);
const ups1 = upgradeAll();
// 3. Welle 1 starten
d.startWave();
// ─── SIMULATION ───
let maxKills = 0;
let waveReached = 0;
let totalUps = ups1;
for (let tick = 0; tick < maxTicks; tick++) {
simulate(dt);
const s = obs();
maxKills = Math.max(maxKills, s.kills);
waveReached = Math.max(waveReached, s.waveIndex);
if (s.state === 'victory' || s.state === 'defeat') break;
// Zwischenwellen: upgraden + evtl. neue Türme + nächste Welle
if (!s.waveActive && s.enemies === 0 && Game.state === 'playing') {
d.setCredits(9999999);
totalUps += upgradeAll();
placeTowers();
d.setCredits(9999999);
totalUps += upgradeAll();
d.startWave();
}
}
const final = obs();
return {
placed,
towerCount: Game.towers.length,
maxKills,
waveReached,
totalUpgrades: totalUps,
finalState: final.state,
finalKills: final.kills,
finalLeaked: final.leaked,
finalLives: final.lives,
};
}, { maxTicks: TICKS_PER_EPISODE });
results.push(result);
const icon = result.finalState === 'victory' ? '✅' : result.finalState === 'defeat' ? '❌' : '⏸️';
console.log(
`${icon} EP ${String(ep + 1).padStart(2)}/${MAX_EPISODES}: state=${result.finalState} ` +
`wave=${result.waveReached} kills=${result.maxKills} ` +
`towers=${result.towerCount} ups=${result.totalUpgrades} lives=${result.finalLives} leaked=${result.finalLeaked}`
);
// Reset zwischen Episoden
await page.evaluate(() => {
const d = window.__AEGIS_DEBUG__;
d.selectLevel(0);
Game.state = 'playing';
Game.towers.length = 0;
Game.enemies.length = 0;
Game.projectiles.length = 0;
Game.particles.length = 0;
Game.beams.length = 0;
if (Game.nanites) Game.nanites.length = 0;
});
}
const wins = results.filter((r) => r.finalState === 'victory').length;
const losses = results.filter((r) => r.finalState === 'defeat').length;
const summary = {
total: results.length,
wins,
losses,
winRate: results.length ? (wins / results.length) * 100 : 0,
avgKills: results.length ? Math.round(results.reduce((a, r) => a + r.maxKills, 0) / results.length) : 0,
avgWaves: results.length ? Math.round((results.reduce((a, r) => a + r.waveReached, 0) / results.length) * 10) / 10 : 0,
};
const outFile = path.join(ROOT, 'selfplay-results.json');
fs.writeFileSync(outFile, JSON.stringify({ timestamp: new Date().toISOString(), episodes: results, summary }, null, 2));
console.log(`✅ Self-Play done → ${outFile}`);
console.log(` ${summary.wins}/${summary.total} wins (${summary.winRate.toFixed(1)}%) · avg kills=${summary.avgKills} · avg wave=${summary.avgWaves}`);
await browser.close();