# 03 – Automatisierung, Self-Play, Performance & CI > Wie die Test-Suite aufgerufen wird, was `selfplay.mjs` und `perf-test.mjs` konkret messen und wie die Stufen in eine CI-Pipeline eingebunden werden. --- ## 1. Aufruf der Skripte ### 1.1 npm-Scripts (aus `package.json`) | Script | Aufruf | Parameter (optional) | |---|---|---| | `npm run audit` | `node tests/audit.mjs` | – | | `npm run selfplay` | `node tests/selfplay.mjs [ep]` | `ep` = Episoden (Default 10) | | `npm run perf` | `node tests/perf-test.mjs [ms] [towers]` | `ms` = Dauer (Default 15000), `towers` = Anzahl (Default 50) | | `npm run report` | `node tests/report.mjs` | – | | `npm test` | Audit + Self-Play + Perf | – | | `npm run setup` | `npx playwright install chromium` | – | ### 1.2 Direkt-Aufruf (nützlich für CI / Skripte) ```bash node tests/selfplay.mjs 50 # 50 Episoden node tests/perf-test.mjs 30000 80 # 30 s messen, 80 Türme node tests/audit.mjs # Standard-Audit node tests/report.mjs # Report erzeugen ``` ### 1.3 Exit-Codes (Übersicht) | Skript | exit 0 | exit ≠ 0 | |---|---|---| | `audit.mjs` | alle Assertions grün | ≥ 1 Fehler → `1`, Crash → `2` | | `selfplay.mjs` | (liefert immer 0) | – | | `perf-test.mjs` | FPS ≥ Zielwert (55) | FPS < Zielwert → `1` | | `report.mjs` | (liefert immer 0) | – | --- ## 2. Self-Play (`tests/selfplay.mjs`) ### 2.1 Zweck Deterministischer Bot spielt **Level 0** (Standard) oder ein beliebiges Level durch und misst: - **Win-Rate** über N Episoden - **Durchschnittliche Kills, Leaks, Credits, Waves** - **Stabilität** (keine JS-Page-Errors, keine Freeze-Detection) ### 2.2 Strategie des Bots ``` 1. Level laden (selectLevel, Default Level 0) 2. Credits setzen (setCredits → 999999) 3. Türme platzieren: - Alle Bau-Kacheln (grid==0) identifizieren - In Priorität: Sniper, Tesla, Quantum, Nano, Barrier, Holo, Grav, Mortar, Sniper - Je nach Credits: mehrere Runden × 5 Türme 4. Wave starten (startWave) 5. simulate() in 1/60-Schritten bis victory/defeat 6. Episoden-Log: kills, leaks, credits, waves, duration 7. Aggregation: Win-Rate, mean, std, min, max ``` ### 2.3 Ausgabe (`selfplay-results.json`) ```json { "timestamp": "2026-08-28T19:30:00.000Z", "episodes": [ { "placed": 40, "towerCount": 40, "maxKills": 120, "waveReached": 8, "totalUpgrades": 64, "finalState": "victory", "finalKills": 120, "finalLeaked": 0, "finalLives": 20 } ], "summary": { "total": 10, "wins": 8, "losses": 2, "winRate": 80.0, "avgKills": 118, "avgWaves": 7.4 } } ``` ### 2.4 Playwright-Fallstricke (Self-Play) | Problem | Lösung | |---|---| | `window.TOWER_TYPES` → `undefined` | `TOWER_TYPES` (bare Identifier) | | `window.placeTower` → `undefined` | `placeTower` (bare) | | `window.buySkill` → `undefined` | `buySkill` (bare) | | `window.__AEGIS_DEBUG__` → ✅ | `window.__AEGIS_DEBUG__` | > `selfplay.mjs` nutzt ausschließlich `window.__AEGIS_DEBUG__` für State-Abfragen und bare Identifier für Aktionen. --- ## 3. Performance (`tests/perf-test.mjs`) ### 3.1 Zweck Messen der **Frames per Second (FPS)** und **Heap-Größe** unter definiertem Lastprofil: - **Level:** schweres Level (Default Level 10, 11 Waves) - **Türme:** konfigurierbare Anzahl (Default 50, empfohlen 50–80) - **Dauer:** konfigurierbare Messzeit (Default 15000 ms) ### 3.2 Vorgehen ``` 1. Level laden (selectLevel(10)) 2. Credits setzen (setCredits → 999999) 3. Türme platzieren (N Türme auf Bau-Kacheln, Priorität: schwerste zuerst) 4. Wave starten (startWave) 5. simulate() in 1/60-Schritten 6. FPS messen: - requestAnimationFrame in Page (window.__fps_frames++) - Alle 1000 ms: fps = frames / 1.0 - Frame-Timing: performance.now() Differenz 7. Heap messen: - page.evaluate(() => performance.memory) - usedJSHeapSize / totalJSHeapSize / jsHeapSizeLimit 8. Ergebnis: perf-result.json ``` ### 3.3 FPS-Zielwert & Exit-Code | FPS | Bewertung | Exit-Code | |---|---|---| | ≥ 55 | ✅ Gut | 0 | | 45–54 | ⚠️ Verändert | 1 | | < 45 | ❌ Kritisch | 1 | ### 3.4 Ausgabe (`perf-result.json`) ```json { "timestamp": "2026-08-28T19:45:00.000Z", "durationMs": 15000, "fps": 58, "targetFps": 55, "pass": true, "heap": { "usedMB": 48.3, "totalMB": 96.0 }, "setup": { "levelIdx": 10, "levelName": "…", "placed": 50, "enemies": 40, "state": "playing" } } ``` ### 3.5 Bekannte Performance-Problemzonen (→ `06_Refactoring_Game.md`) | Problem | Wo sichtbar | Empfehlung | |---|---|---| | Particles | `Game.particles` wächst > 200 | Object-Pooling + Max-Limit | | Projectiles | `Game.projectiles` > 100 | Spatial-Partitioning | | Holograms | `Game.holograms` > 20 | Batch-Rendering | | Beam | `Game.beams` > 10 | Line-Rendering statt Circle | | Enemy-Update | O(n²) bei 20+ Feinden | Spatial-Hash | --- ## 4. CI-Workflows (empfohlen) ### 4.1 Stufen | Stufe | Skripte | Wann | Dauer (ca.) | |---|---|---|---| | **Pre-Commit** | `npm run audit` | bei jedem Commit | 30–60 s | | **Nightly** | Audit + `selfplay.mjs 50` + `perf-test.mjs` | zeitplangetrieben | 5–10 min | | **Release** | Audit + `selfplay.mjs 500` + `perf-test.mjs 30000 80` + Report | Tagging | 15–30 min | ### 4.2 GitHub-Actions-Beispiel (`.github/workflows/ci.yml`) ```yaml name: Test-Suite on: push: branches: [main] pull_request: branches: [main] schedule: - cron: '0 2 * * *' # Nightly 2:00 UTC jobs: audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm install - name: Install Playwright Chromium run: npx playwright install chromium # ⚠️ Aegis-Labyrinth.html muss als Secret/Artifact bereitgestellt werden - name: Download Game-Datei run: | echo "${{ secrets.AEGIS_GAME_BASE64 }}" | base64 -d > Aegis-Labyrinth.html - name: Run Audit run: npm run audit continue-on-error: true - name: Run Self-Play (10 Episoden) run: node tests/selfplay.mjs 10 - name: Run Performance Test run: node tests/perf-test.mjs 15000 50 - name: Generate Report run: node tests/report.mjs - name: Upload Artifacts uses: actions/upload-artifact@v4 with: name: test-results path: | audit-result.json selfplay-results.json perf-result.json report.html audit-final.png # Audit muss grün sein für Main-Push - name: Check Audit Exit Code if: github.event_name == 'push' && contains(github.ref, 'refs/heads/main') run: | # audit-result.json nutzt "summary": { "pass", "fail", "total" } if [ ! -f audit-result.json ] || grep -q '"fail": [1-9]' audit-result.json; then echo "❌ Audit failed" exit 1 fi ``` ### 4.3 Game-Datei in CI bereitzustellen Da `Aegis-Labyrinth.html` **nicht im Repo** liegt, muss sie in der CI als **Secret** oder **Artifact** bereitgestellt werden: | Methode | Vorteil | Nachteil | |---|---|---| | **GitHub Secret** (Base64) | Einfach, sicher | Base64-Konvertierung nötig | | **Release Artifact** | Versioniert | Extra Schritt zum Herunterladen | | **Separates Private Repo** | Saubere Trennung | Access-Token nötig | Empfehlung: **GitHub Secret** für einfache CI, **separates Private Repo** für Teams. --- ## 5. Troubleshooting | Symptom | Ursache | Lösung | |---|---|---| | `❌ Aegis-Labyrinth.html nicht gefunden!` | Datei fehlt | Datei in Projekt-Root kopieren | | `browserType.launch: Executable doesn't exist` | Playwright nicht installiert | `npx playwright install chromium` | | `selfplay.mjs` liefert immer 0 | Bot verliert immer | Level prüfen / mehr Credits / Türme | | `perf-test.mjs` FPS < 45 | Zu viele Entities | Level/Türme reduzieren / Refactoring | | `report.html` zeigt "no data" | `*-result.json` fehlt | Audit/Self-Play/Perf erst ausführen | | Playwright `evaluate` → `undefined` | `window.X` statt `X` | Bare Identifier verwenden (siehe `01_Debug_API.md`) |