Initial project version
This commit is contained in:
@@ -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(0–1)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 [1–11] [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. |
|
||||
@@ -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
|
||||
@@ -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`) |
|
||||
@@ -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/
|
||||
@@ -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 (01–06) | ✅ ü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 (01–06)
|
||||
|
||||
- **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.
|
||||
@@ -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 | −30–50 % GC-Pressure |
|
||||
| 2 | **Spatial Hash** (Grid 32 px) für `towerFire`-Zielwahl (heute O(N·M) alle Paare) | `towerFire(t,dt)` | −50–80 % 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 | −10–20 % 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 | 18–24 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 1–2) | P0 1–7 (Pooling, Spatial Hash, Layer-Cache, dt-Clamp) | ~6 h |
|
||||
| S2 (Tag 3–4) | P1 8–11 (Modularisierung + Konstanten + Store) | ~8 h |
|
||||
| S3 (Tag 5) | P1 12–13 + 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`).
|
||||
Reference in New Issue
Block a user