Initial project version
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
# 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`) |
|
||||
Reference in New Issue
Block a user