// 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 . // // 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();