Files
Laby/tests/selfplay.mjs
T
Lila-Kuh ea2a2f2cbf
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
Initial project version
2026-08-29 01:45:19 +02:00

237 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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();