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
+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();