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
+153
View File
@@ -0,0 +1,153 @@
# 01 Debug-API & In-Game-Console
> Die **Test-Orakel-Schicht** des Games: `window.__AEGIS_DEBUG__` und die eingebauten Audits.
> Die gesamte Test-Suite in `tests/` hängt an dieser API sie ist die einzige stabile Schnittstelle zwischen den Skripten und der Game-Source.
---
## 1. Grundprinzip
`Aegis-Labyrinth.html` ist eine Single-File-App. Für Automatisierung stellt sie zwei Dinge bereit:
1. **`window.__AEGIS_DEBUG__`** ein explizit auf `window` gehängtes Objekt (stabil in `page.evaluate()` referenzierbar).
2. **Funktionen im Global Scope** (`placeTower`, `buySkill`, `simulate`, `triggerVictory`, `TOWER_TYPES`, `Game`, …) frei verfügbar, aber **`const`/`let`-basiert**, d.h. **nicht** auf `window`.
### Playwright-Fallstricke (WICHTIG)
| Identifier | Auf `window`? | Referenz in `page.evaluate()` |
|---|---|---|
| `__AEGIS_DEBUG__` | ✅ ja | `window.__AEGIS_DEBUG__` |
| `TOWER_TYPES` | ❌ nein (const) | `TOWER_TYPES` (bare) |
| `Game` | ❌ nein | `Game` (bare) |
| `placeTower`, `buySkill`, `simulate` | ❌ nein | bare Identifier |
> `window.TOWER_TYPES` → `undefined`. Immer bare Identifier verwenden.
---
## 2. `window.__AEGIS_DEBUG__` Komplette API
### 2.1 Audit-Funktionen
| Funktion | Prüft (Kurz) |
|---|---|
| `runSelfAudit()` | Konnektivität aller 11 Levels, alle 9 Turm-Attacks, Skill-Käufe (4 Ränge), Platzierungs-Validierung, AutoWave, Endless-Transition |
| `runExtensionAudit()` | Mortar-Splash (2 Ziele), Tesla-AoE (5 Feinde + Freeze), Reward-Dedup, Blocker-Alternate/Closed-Path |
| `runRedesignAudit()` | Sniper-Priorisierung (niedriger HP zuerst), Quantum-Beam-Alignment, Nano-Spawner (Limit/Damage/Cleanup) |
Jede Funktion gibt ein **Report-Objekt** zurück (Schema: [`02_Audits.md`](./02_Audits.md)).
### 2.2 State & Utility
```
window.__AEGIS_DEBUG__
├── runSelfAudit() → report object
├── runExtensionAudit() → report object
├── runRedesignAudit() → report object
├── getState() → { state, credits, lives, waveIndex, enemies, towers, … }
├── startEndlessLevel(n) → startet Endless ab Level n (0-basiert)
├── selectLevel(n) → lädt Level n (0-basiert)
├── startWave() → manuelle Wave-Start
├── setCredits(n) → Credits setzen
├── towerTypes[] → 9 Einträge {id, name, cost, range, damage, fireRate, skills}
├── skilltree
│ ├── version, points, nodes, completed, endless20Rewards
│ ├── getState() → deep clone
│ ├── buy(nodeId)
│ └── reset()
├── levels[] → 11 Einträge {id, name, waves, difficulty, endlessAvailable}
├── effectMetadata
│ ├── towerTypes[] (ohne cost, mit skillIds)
│ ├── enemyTypes[] ({id, hp, speed, radius, color})
│ └── effectLimits ({maxParticles:200, maxProjectiles:100, maxHolos:20, particleLife:0.8, projectileLife:1.6})
├── effectTuning
│ ├── holo ({influenceRadius, pullStrength})
│ └── grav ({basePull, baseSlow})
├── aura
│ ├── getAuraState()
│ ├── getBonuses()
│ └── upgrade(tower)
└── audio
├── state() → {initialized, muted, volume, voices}
├── sounds[] → 20 Namen
├── test(name)
├── setMuted(bool)
└── setVolume(01)
```
---
## 3. `getState()` typisches Schema
```js
{
state: "menu" | "playing" | "victory" | "defeat" | "endless",
credits: number,
lives: number,
waveIndex: number,
totalWaves: number,
kills: number,
leaked: number,
enemies: number | Enemy[], // je nach Version Zähler oder Array
towers: number | Tower[],
// …
}
```
> Die Suite nutzt `getState()` primär als **Beobachtungsschnittstelle** für Self-Play & Perf. Exakte Felder: [`02_Audits.md`](./02_Audits.md) §getState.
---
## 4. In-Game-Debug-Console (manuell)
**Aufruf:** Im Browser mit Taste `` ` `` (Backtick) die Debug-Console öffnen.
| Befehl | Beschreibung |
|---|---|
| `help` | Alle Befehle anzeigen |
| `state` | Kompletten Game-State als JSON in der Konsole |
| `credits [Zahl]` | Credits setzen (z. B. `credits 99999`) |
| `unlock all` | Alle Türme entsperren |
| `level [111] [normal\|endless]` | Level laden (1-basiert) |
| `wave` | Nächste Wave starten |
| `audit [self\|extension\|redesign]` | Das entsprechende Audit ausführen |
| `sound [Name]` | Sound testen (einer der 20 Sounds) |
| `clear` | Konsole leeren |
> Die In-Game-Console ist für **manuelle** Debugging-Zwecke gedacht. Die automatisierte Suite nutzt direkt `window.__AEGIS_DEBUG__` (ohne UI-Overlay).
---
## 5. Neue Mechanismen auditierbar machen (Konvention)
Jeder neue Mechanismus sollte **vor Merge** einen Audit-Eintrag in `window.__AEGIS_DEBUG__` erhalten, damit `tests/audit.mjs` ihn automatisch abdeckt:
```js
// Beispiel: neuer Turm "tesla2"
function runTesla2Audit() {
loadLevel(0);
const t = new Tower(TOWER_BY_ID.tesla2, 5, 5);
const e = new Enemy('tank', t.x + 30, t.y);
Game.towers = [t]; Game.enemies = [e];
towerFire(t, 1/60);
return { hit: e.hp < e.maxHp, beam: Game.beams.length > 0 };
}
window.__AEGIS_DEBUG__.runTesla2Audit = runTesla2Audit;
```
Anschließend die neue Funktion in `tests/audit.mjs` anbinden und hier + in `02_Audits.md` dokumentieren.
---
## 6. Glossar (interner Jargon)
| Begriff | Bedeutung |
|---|---|
| `grid[y][x] === 0` | Bau-Kachel (Wand) Turm/Blocker hier erlaubt |
| `grid[y][x] === 1` | Pfad-Kachel Feinde laufen hier |
| `grid[y][x] === 2` | Start-Kachel (Entry) |
| `grid[y][x] === 3` | Exit-Kachel (Base) |
| `flow.field[idx] === INF` | Zelle nicht erreichbar |
| `NANITE_LIMIT` | Max. gleichzeitig aktive Naniten |
| `autoWave` / `autoWaveActive` | Verzögerung zwischen Wellen |
| `TOWER_BY_ID` | Map: `"barrier"` → Definition, `"tesla"` → Definition, etc. |
+193
View File
@@ -0,0 +1,193 @@
# 02 Audits im Detail (self / extension / redesign)
> Was die drei eingebauten Audits konkret prüfen, welche **Assertions** `tests/audit.mjs` darauf aufsetzt und welche **Exit-Codes** daraus resultieren.
> API-Basis: [`01_Debug_API.md`](./01_Debug_API.md).
---
## 1. Übersicht
| Audit | API-Funktion | Fokus |
|---|---|---|
| **Self** | `runSelfAudit()` | Konnektivität (11 Levels), Attacks, Skills, Platzierung, AutoWave, Endless |
| **Extension** | `runExtensionAudit()` | Mortar-Splash, Tesla-Chain/Freeze, Reward-Dedup, Blocker-Logik |
| **Redesign** | `runRedesignAudit()` | Kryo, Sniper (Pierce), Mortar-FixImpact, Quantum-Beam, Nano-Spawner |
`audit.mjs` führt alle drei in einer Headless-Chromium-Session aus, sammelt `pageerror`-Events und bewertet die Rückgabe-Werte mit eigenen Assertion-Hilfsfunktionen.
---
## 2. `runSelfAudit()` Struktur & Assertions
### 2.1 Report-Struktur (Rückgabe der API)
```jsonc
{
"connectivity": [ { "level": 0, "starts": 2, "exit": true, "reachable": true }, ], // 11 Levels
"attacks": [ { "id": "sniper", "projectiles": 3, "holos": 0, "hurt": 2 }, ],
"skills": [ { "id": "sniper", "rank": 4 }, ],
"placement": { "wallAccepted": true, "pathAccepted": false, "count": 1 },
"autoWave": { "state": "playing", "active": true },
"endless": { "state": "endless", "endlessWave": 1 }
}
```
### 2.2 Assertions in `tests/audit.mjs` (`assertSelfAudit`)
| # | Assertion | Erwartung |
|---|---|---|
| S-conn | `L{n} has start` | `starts > 0` (pro Level) |
| S-conn | `L{n} has exit` | `exit === true` (pro Level) |
| S-conn | `L{n} reachable` | `reachable === true` (pro Level) |
| S-atk | `tower {id}: projectiles=… holos=… hurt=…` | **info** (Support-/Relay-Türme feuern absichtlich nicht → kein Hard-Fail) |
| S-sk | `skill {id} rank>=1` | `rank >= 1` (nach 4× `buySkill`) |
| S-plc | `placement: wallAccepted (tower on free cell)` | `wallAccepted === true` |
| S-plc | `placement: pathAccepted (tower on path rejected)` | `pathAccepted === false` |
| S-plc | `placement: tower count>=1` | `count >= 1` |
| S-aw | `autoWave: state` / `autoWave: active` | **info** (nur State prüfen) |
| S-ee | `endless: state` | **info** |
| S-ee | `endless: endlessWave>=0` | `endlessWave >= 0` |
> Hinweis: „Attacks“ und „AutoWave“ sind bewusst **Diagnose-/Info-Checks** die harten Pfade laufen über Konnektivität, Skills und Platzierung.
---
## 3. `runExtensionAudit()` Struktur & Assertions
### 3.1 Report-Struktur
```jsonc
{
"mortar": { "projectile": true, "hurt": true },
"tesla": { "damaged": 3, "frozen": 2 },
"reward": { "pointsAfterDuplicate": 1, "storedRank": 4, "rankAfterBuy": 4 },
"blockerAlternate":{ "placed": true, "finite": true, "soldPathRestored": true },
"blockerClosed": { "placed": true, "destroyed": true, "pathRestored": true }
}
```
### 3.2 Assertions (`assertExtensionAudit`)
| # | Assertion | Erwartung |
|---|---|---|
| E-mrt | `mortar: projectile spawned (may resolve instantly)` | **info** |
| E-mrt | `mortar: both targets hurt (splash)` | `hurt === true` |
| E-tes | `tesla: damaged enemies count` | `damaged > 1` |
| E-tes | `tesla: frozen count` | `frozen > 1` |
| E-rwd | `reward: pointsAfterDuplicate>=1` | `pointsAfterDuplicate >= 1` |
| E-rwd | `reward: storedRank matches rankAfterBuy` | `storedRank === rankAfterBuy` |
| E-blt | `blockerAlternate: placed` / `route still finite` / `path restored after sell` | `placed`, `finite`, `soldPathRestored` (falls Kandidat gefunden) |
| E-blc | `blockerClosed: placed` / `destroyed by enemy` / `path restored` | `placed`, `destroyed`, `pathRestored` (falls Kandidat gefunden) |
> Fehlt ein geeigneter Blocker-Kandidat, wird ein **info-Pass** („no candidate found (acceptable)") erzeugt kein Hard-Fail.
---
## 4. `runRedesignAudit()` Struktur & Assertions
### 4.1 Report-Struktur
```jsonc
{
"kryo": { "randomLastTarget": true, "visibleSlow": true },
"sniper": { "pierced": 3, "noDot": true, "highestAbsoluteHp": true },
"mortar": { "fixedImpact": true },
"quantum":{ "lineHits": 3, "offLineUntouched": true, "beam": true },
"nano": { "spawned": 5, "damaged": true, "cleaned": true }
}
```
### 4.2 Assertions (`assertRedesignAudit`)
| # | Assertion | Erwartung |
|---|---|---|
| R-kyo | `kryo: target hit` | `randomLastTarget || visibleSlow` |
| R-kyo | `kryo: slow applied` | `visibleSlow === true` |
| R-snp | `sniper: pierced count` | `pierced > 1` |
| R-snp | `sniper: no DoT (burn/poison)` | `noDot === true` |
| R-snp | `sniper: highest absolute HP targeting` | `highestAbsoluteHp === true` (falls vorhanden) |
| R-mrt | `mortar: fixed impact (witness hit, fleeer untouched)` | `fixedImpact === true` |
| R-qtm | `quantum: line hits` | `lineHits > 1` |
| R-qtm | `quantum: off-line untouched` | `offLineUntouched === true` |
| R-qtm | `quantum: beam created` | `beam === true` |
| R-nno | `nano: spawned count` | `spawned > 1` |
| R-nno | `nano: damaged target` | `damaged === true` |
| R-nno | `nano: cleaned up` | `cleaned === true` |
---
## 5. `audit-result.json` Schema (authoritative)
```jsonc
{
"timestamp": "2026-08-28T19:00:00.000Z",
"file": "…/Aegis-Labyrinth.html",
"fileHash": "a1b2c3d4e5f60718", // sha256, erste 16 Hex-Zeichen
"self": { /* Sektion 2.1 */ },
"extension": { /* Sektion 3.1 */ },
"redesign": { /* Sektion 4.1 */ },
"assertions": {
"self": [ { "name", "actual", "expected", "passed" } ],
"extension": [ ],
"redesign": [ ]
},
"pageErrors": [], // gemeldete JS-Fehler (pageerror-Events)
"summary": { "pass": 40, "fail": 0, "total": 42 } // total inkl. PageErrors
}
```
Das Gesamtverdict in `report.mjs` wird direkt aus `summary.fail === 0` **und** `pageErrors.length === 0` abgeleitet.
---
## 6. Exit-Code & Fail-Verhalten
```
exit 0 → summary.fail === 0 UND pageErrors leer
exit 1 → ≥ 1 Assertion failed ODER ≥ 1 PageError
exit 2 → harter Crash (Datei fehlt, Browser-Start, Timeout, …)
```
Zusätzlich werden immer `audit-result.json` **und** `audit-final.png` geschrieben (auch bei Fail), damit die Fehler in `report.html` nachvollziehbar bleiben.
---
## 7. Fehlerklassen, die die Audits typischerweise fangen
| Kategorie | Wo sichtbar | Häufige Ursache |
|---|---|---|
| Pfad unvollständig | `self.connectivity[n].reachable === false` | Flow-Field `rebuild()` nach `grid`-Change vergessen |
| Turm schießt nicht | `self.attacks[id].projectiles === 0` (info) | Cooldown nicht reset / Range zu klein |
| Skill nicht kaufbar | `self.skills[id].rank < 1` | `buySkill`-Cost > Credits / Skill-ID-Typo |
| Platzierung falsch | `self.placement.pathAccepted === true` | `placeTower` prüft `grid` nicht |
| Splash trifft nicht | `extension.mortar.hurt === false` | Splash-Radius / Ziel-Filter |
| Tesla ohne Effekt | `extension.tesla.frozen < 1` | Freeze-Status nicht gesetzt |
| Doppelte Rewards | `extension.reward.storedRank !== rankAfterBuy` | `triggerVictory` ohne Idempotenz |
| Pfad kaputt nach Blocker | `extension.blockerClosed.pathRestored === false` | `sellTower` ruft `Game.flow.rebuild()` nicht auf |
| Sniper kein Pierce | `redesign.sniper.pierced < 2` | Pierce-Logik fehlt |
| Quantum trifft Off-Line | `redesign.quantum.offLineUntouched === false` | Winkel-Test zu locker |
| Naniten lecken | `redesign.nano.cleaned === false` | Cleanup-Flag / `NANITE_LIMIT` |
---
## 8. Bug-Fix-Routine (empfohlen)
```
1. Replizieren
→ npm run audit
→ audit-result.json öffnen → failing Assertion identifizieren
→ Optional: In-Game-Debug-Console (Taste ` `) → `audit self` / `state`
2. Isolation
→ Spiel-State minimal halten:
Game.towers = [onlyTower]
Game.enemies = [onlyEnemy]
Game.projectiles = []
→ Einzelne Funktion aufrufen: towerFire(t, 1/60)
3. Fixen
→ Aegis-Labyrinth.html editieren
4. Verifizieren
→ npm run audit erneut → grün
→ npm run report → report.html prüfen
+276
View File
@@ -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 5080)
- **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 |
| 4554 | ⚠️ 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 | 3060 s |
| **Nightly** | Audit + `selfplay.mjs 50` + `perf-test.mjs` | zeitplangetrieben | 510 min |
| **Release** | Audit + `selfplay.mjs 500` + `perf-test.mjs 30000 80` + Report | Tagging | 1530 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`) |
+188
View File
@@ -0,0 +1,188 @@
# 04 Resultate & Report (Ausgabe-Dateien)
> Schema-Referenz für alle Artefakte der Test-Suite und Aufbau des generierten `report.html`.
> Diese Dateien werden **zur Laufzeit erzeugt** und **nicht versioniert** (→ `.gitignore`).
---
## 1. Übersicht der Artefakte
| Datei | Erzeugt von | Zweck |
|---|---|---|
| `audit-result.json` | `tests/audit.mjs` | Raw-Reports der 3 Audits + Assertions + Page-Errors + Summary |
| `selfplay-results.json` | `tests/selfplay.mjs` | Episoden-Log + Win-Rate + Aggregates |
| `perf-result.json` | `tests/perf-test.mjs` | FPS, Heap, Setup-Metadaten |
| `report.html` | `tests/report.mjs` | Zusammengefasster, menschlich lesbarer CI-Report |
| `audit-final.png` | `tests/audit.mjs` | Finaler Screenshot des Headless-Browsers |
> `report.mjs` liest die drei `*-result.json` und rendert daraus ein einzelnes `report.html`.
> Fehlt eine JSON-Datei, zeigt `report.html` für die entsprechende Sektion „no data".
---
## 2. `audit-result.json`
```json
{
"timestamp": "2026-08-28T19:20:00.000Z",
"self": {
"connectivity": [
{ "level": 0, "name": "…", "reachable": true }
],
"attacks": [
{ "id": "sniper", "projectiles": 12, "hits": 10 }
],
"skills": [
{ "id": "…", "rank": 4 }
],
"placement": { "wall": true, "path": true },
"autoWave": true,
"endlessTransition": true
},
"extension": {
"mortarSplashTargets": 2,
"teslaAoE": { "targets": 5, "frozen": true },
"rewardDedup": true,
"blockerAltPath": true,
"blockerClosedPath": true
},
"redesign": {
"sniperPriority": true,
"quantumBeam": { "aligned": 3, "offLineUnharmed": true },
"nanoSpawner": { "limit": true, "damage": true, "cleanup": true }
},
"pageErrors": [],
"assertions": [
{ "name": "self.connectivity.allReachable", "pass": true },
{ "name": "self.attacks.noZeroProjectiles", "pass": true },
{ "name": "self.skills.allRank4", "pass": true }
],
"summary": {
"total": 12,
"pass": 12,
"fail": 0
}
}
```
> **Exit-Code-Kopplung:** `audit.mjs` beendet sich mit `1`, sobald `summary.fail > 0`
> oder `pageErrors` nicht leer ist. Ein harter Crash im Browser führt zu `2`.
---
## 3. `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
}
}
```
**Felder:**
| Feld | Bedeutung |
|---|---|
| `placed` | Anzahl tatsächlich platzierter Türme in der Episode |
| `towerCount` | Endgültige Turm-Anzahl (inkl. Spawner) |
| `maxKills` | Höchststand an Kills während der Simulation |
| `waveReached` | Letzt erreichte Wave |
| `totalUpgrades` | Anzahl gekaufter Skill-Upgrades |
| `finalState` | `victory` \| `defeat` |
| `winRate` | `wins / total × 100` |
---
## 4. `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"
}
}
```
| Feld | Bedeutung |
|---|---|
| `fps` | Gemittelte Frames pro Sekunde während `durationMs` |
| `targetFps` | Zielwert (Default `55`) `pass` = `fps >= targetFps` |
| `heap.usedMB` | `performance.memory.usedJSHeapSize` / 1048576 |
| `heap.totalMB` | `performance.memory.totalJSHeapSize` / 1048576 |
> Heap-Messung benötigt Chromium (`performance.memory`). In nicht-Chromium-Browsern ist
> `heap` ggf. `null` → `report.mjs` blendet dann die Heap-Spalte aus.
---
## 5. `report.html`
`report.mjs` rendert eine statische, single-file HTML-Seite:
```
┌──────────────────────────────────────────────────────────────────┐
│ Aegis Labyrinth Test & Audit Report │
│ Timestamp: 2026-08-28 19:45 │
├──────────────────────────────────────────────────────────────────┤
│ ✔ AUDIT pass 12/12 self ✔ extension ✔ redesign ✔ │
│ ✔ SELF-PLAY winRate 80% (8/10) avgKills 118 │
│ ✔ PERFORMANCE 58 fps (target 55) heap 48.3 / 96.0 MB │
├──────────────────────────────────────────────────────────────────┤
│ Assertion-Details (expandierbare Liste) │
│ Page-Errors (leer) │
└──────────────────────────────────────────────────────────────────┘
```
**Verhalten:**
- Jede Sektion hat einen Ampel-Status: grün (pass) / gelb (teilweise) / rot (fail).
- Fehlende Quelldatei → Sektion wird als „no data" (grau) gerendert, statt zu crashen.
- Kein externes JS/CSS → `report.html` ist vollständig offline-viewbar.
---
## 6. .gitignore (Empfehlung)
```gitignore
# Test-Artefakte (werden zur Laufzeit erzeugt)
audit-result.json
selfplay-results.json
perf-result.json
report.html
audit-final.png
# Externe Game-Source (manuell in die Root legen)
Aegis-Labyrinth.html
# Node
node_modules/
+95
View File
@@ -0,0 +1,95 @@
# 05 Tasks & Backlog (Changelog)
> Zentrales Log über **abgearbeitete Tasks** (Changelog) und das **offene Backlog**.
> Jeder abgeschlossene Arbeitsschritt an der Test-Suite wird hier als Eintrag dokumentiert
> das beantwortet die Frage: *„Werden Tasks, die abgearbeitet werden, dokumentiert?"* → **Ja, hier.**
**Konvention:**
- Chronologisch, neuestens oben.
- Format: `### [YYYY-MM-DD] Kurztitel` + Stichpunkte (Geändert / Begründung / Impact).
- Offene Punkte wandern in §3 (Backlog), abgeschlossene in §2 (Changelog).
- **Trennung zu `CHANGELOG.md`:** Diese Datei = **Task-Changelog** (was an der Suite abgearbeitet wurde).
Die **Versions-Historie** des Games + der Suite (z. B. `1.0.2`, Versionssprünge) lebt im Root-`CHANGELOG.md`.
Große Changes gehören in **beide**.
---
## 1. Status-Snapshot
| Bereich | Status |
|---|---|
| Test-Suite (Audit / Self-Play / Perf / Report) | ✅ implementiert & lauffähig |
| Dokumentation (0106) | ✅ überarbeitet & konsistent |
| CI-Definition | 🟡 dokumentiert (Workflow noch zu commiten) |
| Refactoring der Game-Datei | 🔵 Advisory-Backlog (siehe `06_Refactoring_Game.md`) |
---
## 2. Changelog (abgearbeitete Tasks)
### [2026-08-28] Doku-Nummerierung vereinheitlicht (0106)
- **Geändert:** Drei veraltete Dateien (`01_Refactoring_Performance.md`, `02_Audit_Bericht.md`, `03_Automatisierung_SelfPlay.md`) mit altem „Game"-Framing entfernt.
- **Neu:** Kohärentes Set `01_Debug_API`, `02_Audits`, `03_Automatisierung_CI`, `04_Resultate_Report`, `05_Tasks_Backlog`, `06_Refactoring_Game`.
- **Begründung:** Repo ist eine **Test-Suite**, kein Spiel → Doku musste umfokussiert und neu nummeriert werden.
- **Impact:** Keine Code-Änderung; reine Dokumentation.
### [2026-08-28] Schemata gegen echten Code verifiziert
- **Geändert:** `selfplay-results.json`, `perf-result.json`, `audit-result.json`-Schemata in `03` und `04` an den tatsächlichen Skript-Output angepasst.
- **Geändert:** CI-Gate prüft jetzt `summary.fail` statt des nicht existierenden Felds `"failed"`.
- **Begründung:** Doku-Beispiele kollidierten mit der realen Ausgabe (falsche Feldnamen).
- **Impact:** CI-Beispiel läuft jetzt konsistent mit `audit-result.json`.
### [2026-08-28] README auf Test-Suite-Framing umgestellt
- **Geändert:** README beschreibt jetzt das **Test-/Audit-Projekt**, nicht das Spiel.
- **Neu:** Hinweis, dass `Aegis-Labyrinth.html` **manuell** in die Root gelegt werden muss.
- **Begründung:** User-Fokus: Suite testet & auditiert die extern eingelegte Game-Datei.
- **Impact:** Onboarding-Clairity für neue Nutzer.
### [2026-08-28] `report.mjs` als Artefakt-Generator dokumentiert
- **Geändert:** `04_Resultate_Report.md` mit `report.html`-Aufbau + „no data"-Verhalten.
- **Impact:** Klare Referenz, was `npm run report` erzeugt.
---
## 3. Offenes Backlog
> Priorität: **P0** = sofort, **P1** = kurzfristig, **P2** = später. Jede erledigte Aufgabe
> wandert nach oben in §2 (Changelog) mit Datum.
### Test-Suite / CI
| # | Prio | Task |
|---|---|---|
| 1 | P0 | `.github/workflows/ci.yml` aus `03_Automatisierung_CI.md` commiten & aktivieren |
| 2 | P0 | `Aegis-Labyrinth.html` + Test-Artefakte in `.gitignore` sicherstellen |
| 3 | P1 | CI-Secret `AEGIS_GAME_BASE64` anlegen (Game-Datei in CI bereitstellen) |
| 4 | P1 | Lighthouse-Job als eigene CI-Stufe ergänzen (Performance-Score ≥ 80) |
| 5 | P2 | Trend-Tracking: Lauf-Results als CSV/JSON-Artifact archivieren (Trend-Chart) |
### Robustheit der Skripte
| # | Prio | Task |
|---|---|---|
| 6 | P1 | Self-Play: deterministischer Seed für `Math.random()` (reproduzierbare Episoden) |
| 7 | P1 | Perf-Test: Heap-Delta (vorher/nachher) statt absoluter Werte → Leak-Heuristik |
| 8 | P2 | Optionaler Q-Learning-/MCTS-Bot (Outline in alter Doku, ggf. neu aufbauen) |
### Dokumentation
| # | Prio | Task |
|---|---|---|
| 9 | P1 | `01_Debug_API.md` um `effectTuning`/`effectLimits`-Detailfeld ergänzen, wenn es sich ändert |
| 10 | P2 | `report.html`-Screenshot/Beispiel als Referenz in `04` aufnehmen |
---
## 4. How-to: Neue Task dokumentieren
1. Task erledigen (Code / Doku / CI).
2. Oben in §2 einen Eintrag anfügen: `### [JJJJ-MM-TT] Titel` + Stichpunkte.
3. Falls noch offen: als Zeile in §3 eintragen, Priorität setzen.
4. Bei Abschluss: aus §3 entfernen und in §2 verschieben.
+123
View File
@@ -0,0 +1,123 @@
# 06 Refactoring & Performance (Advisory für `Aegis-Labyrinth.html`)
> **Advisory-Dokumentation** Empfehlungen, `Aegis-Labyrinth.html` (~707 KB, Single-File-Game)
> sauberer, schneller und wartbarer zu machen, **ohne** das sichtbare Spielverhalten zu ändern.
>
> ⚠️ Diese Datei gehört **nicht** zur Test-Suite selbst sie ist ein **Beratungs-Backlog** für die
> extern eingelegte Game-Datei. Die Suite (`tests/*`) prüft nur, dass Änderungen daran die 3 Audits
> nicht brechen (→ `02_Audits.md`) und die Performance nicht einbricht (→ `04_Resultate_Report.md`).
---
## 1. Ist-Zustand (kurz)
| Aspekt | Status |
|---|---|
| Struktur | Eine HTML-Datei: CSS + ~4.500 Zeilen JS + UI |
| Rendering | Canvas 2D, `requestAnimationFrame`-Loop, Partikel-/Projectile-Arrays |
| State | Globale `Game`-Objekte + freie Funktionen im Global Scope |
| Persistenz | `localStorage` (Skilltree, Audio, Unlocks, Custom Levels) |
| Debug | `window.__AEGIS_DEBUG__` (Audits, State, Audio, Levels) → `01_Debug_API.md` |
| Tests | 3 eingebauten Audits + externe Suite (`tests/`) |
| Bundle | Base64-PNG-Logo (ca. 350 KB) inline bereits optimiert |
---
## 2. Priorisierte Maßnahmen (P0 = mach heute, P2 = Backlog)
### P0 Hohe Wirkung, geringes Risiko
| # | Maßnahme | Wo | Erwarteter Effekt |
|---|---|---|---|
| 1 | **Partikel-/Projectile-Pooling** statt `push`/`filter` pro Frame | `updateProjectiles`, `updateEnemies`, Partikel-Arrays | 3050 % GC-Pressure |
| 2 | **Spatial Hash** (Grid 32 px) für `towerFire`-Zielwahl (heute O(N·M) alle Paare) | `towerFire(t,dt)` | 5080 % CPU bei 50+ Türmen |
| 3 | **Canvas-Layer-Trennung**: `bgCanvas`, `midCanvas`, `fxCanvas` + `ctx.imageSmoothingEnabled=false` | `render()` | Weniger Re-Draws, stabilerer 60 fps |
| 4 | **Offscreen-Canvas für statische Route** (1× pro Level zeichnen, dann `drawImage`) | Route-Drawing | 1020 % Framezeit |
| 5 | **Delta-Clamp**: `dt = Math.min(dt, 1/30)` am Loop-Start | Game-Loop | Keine Explosionen nach Tab-Wechsel |
| 6 | **`willReadFrequently:true`** nur bei `getImageData`-Puffern, `desynchronized:true` bei FX-Canvas | Canvas-Kontexte | Geringere GPU-Roundtrips |
| 7 | **Event-Debounce** auf `mousemove` (Tooltip) & `resize` | HUD/Tooltip | Weniger Layout-Thrashing |
### P1 Mittlere Wirkung
| # | Maßnahme |
|---|---|
| 8 | **Modularisierung** in IIFE/Module: `core/loop`, `core/state`, `units/tower`, `units/enemy`, `systems/pathflow`, `systems/aura`, `systems/skills`, `ui/*`, `audio`, `persistence`, `debug` |
| 9 | **State-Reduktion**: `Game`-Objekt durch `store`-Muster mit `subscribe/get` ersetzen (~60 Zeilen) |
| 10 | **Pure-Function-Tests für Pfadlogik**: `flowField`, `diagnoseRouteState`, `computePath` extrahieren → unit-testbar |
| 11 | **Konstanten-Block** (`CELL`, `MAP_W`, `NANITE_LIMIT`, `PARTICLE_MAX`, …) in `constants` |
| 12 | **Magic-Number-Audit**: `1/60`, `0.05`, `180` etc. in Konstanten umbenennen |
| 13 | **`Symbol()`-Keys** für interne `Game.towers[i]._internal*` statt String-Props |
### P2 Backlog
| # | Maßnahme |
|---|---|
| 14 | Web Worker für Flow-Field-Berechnung bei großen Maps |
| 15 | WebGL2-Option (`<canvas>`-Fallback bleibt) für Partikel > 5.000 |
| 16 | `requestIdleCallback` für UI-Rebuilds (Tower-Panel, Skilltree) |
| 17 | Code-Splitting via `import()` hinter „Menu" |
| 18 | `Intl.NumberFormat` für Zahlen im HUD |
| 19 | i18n-Datei (DE/EN) statt inline-Strings |
| 20 | PWA-Shell (`manifest.json`, `sw.js`) für Offline |
---
## 3. Rendering-Optimierung (Details)
### 3.1 Frame-Pipeline (empfohlen)
```
[1] input → pointer events, key state
[2] update(dt) → enemies → towers → projectiles → nanites → particles
[3] dirty check → bgDirty / midDirty / fxDirty (Boolean pro Layer)
[4] render() → if bgDirty: drawImage(bgCanvas);
if midDirty: draw towers+enemies;
fxCanvas always (additive)
```
### 3.2 Konkrete Canvas-Techniken
```js
const fx = document.createElement('canvas');
const fctx = fx.getContext('2d', { alpha: true, desynchronized: true });
fctx.globalCompositeOperation = 'lighter'; // Partikel additiv
// pro Frame:
fctx.clearRect(0, 0, fx.width, fx.height); // oder 'destination-out' Fade für Trails
```
- **Batching**: Alle Partikel gleicher Farbe in einen `beginPath()` → eine `fill()`.
- **`roundRect`** nativ (Chrome 99+) statt Hand-Rotation.
- **`createPattern`** für Tile-Boden (1× erzeugen).
- **Sprite-Atlas** für die 9 Türme: 1 Atlas 512×512, `drawImage(atlas, sx,sy,sw,sh, x,y,w,h)`.
### 3.3 Zielwerte
| Metrik | Vorher (Messung in DevTools) | Nachher (Ziel) |
|---|---|---|
| Framezeit @ 4K, 80 Türme, 150 Feinde | 1824 ms | ≤ 12 ms |
| GC-Pausen/Minute | ~12 | ≤ 4 |
| Heap-Wachstum (10 Min) | ~40 MB | ≤ 10 MB |
---
## 4. Refactoring-Checkliste (Definition of Done)
- [ ] Alle drei Audits (`self`, `extension`, `redesign`) vor & nach Refactoring grün
- [ ] `window.__AEGIS_DEBUG__.getState()` liefert identisches Schema (nur interne Keys dürfen sich ändern)
- [ ] Kein sichtbarer Frame-Drop bei 60 fps Test-Level
- [ ] `localStorage`-Schema unverändert (oder Migration in `persistence.js`)
- [ ] Editor-Map & Custom-Level import/export byte-identisch
- [ ] Lighthouse-Performance ≥ 95 (mobile emulation)
---
## 5. Vorschlag: Refactoring in 3 Sprints
| Sprint | Inhalt | Aufwand |
|---|---|---|
| S1 (Tag 12) | P0 17 (Pooling, Spatial Hash, Layer-Cache, dt-Clamp) | ~6 h |
| S2 (Tag 34) | P1 811 (Modularisierung + Konstanten + Store) | ~8 h |
| S3 (Tag 5) | P1 1213 + P2 14 (Worker) + Lighthouse-Polish | ~5 h |
**Regressionsschutz:** Vor jedem Sprint einen „Golden Frame" (Screenshot + `getState()`-Dump) mit der
Test-Suite erzeugen (`tests/audit.mjs` + `tests/perf-test.mjs`, siehe `03_Automatisierung_CI.md`).