/** * Map system verification: * - every map builds a valid, non-empty path & fly path * - path cells never collide with blocked (obstacle) cells * - maps differ from each other (distinct layouts) * - lockstep determinism holds on every map (two engines, same actions) * - obstacle clearing frees build tiles on every map * - a full 20-wave simulation is beatable/playable on every map */ import { GameEngine } from '../src/game/engine.ts' import { MAPS, MAP_ORDER } from '../src/game/config.ts' import type { MapId } from '../src/game/types.ts' let failures = 0 function check(label: string, ok: boolean, detail = ''): void { if (!ok) { failures++ console.error(` ✗ ${label}${detail ? ` — ${detail}` : ''}`) } else { console.log(` ✓ ${label}`) } } for (const mapId of MAP_ORDER) { const map = MAPS[mapId] console.log(`\n=== ${map.name} (${mapId}) ===`) const eng = new GameEngine() eng.setMap(mapId) eng.startGame('normal', mapId) // 1. path built & sane check('Wegpunkte vorhanden', eng.pathPx.length >= 2) check('Flugweg vorhanden', eng.flyPathPx.length >= 2) check('Pfadzellen markiert', eng.pathCells.size > 0) const wp = map.waypoints const interior = wp.slice(1, -1) check( 'Innere Wegpunkte im Spielfeld', interior.every(([x, y]) => x >= 0 && x < 20 && y >= 0 && y < 11), JSON.stringify(wp), ) check( 'Start/Ende am Spielfeldrand', wp.every(([x, y]) => x === -1 || x === 20 || y >= 0), ) // 2. no collision between path and obstacles const overlap = [...eng.pathCells].filter((k) => eng.blockedCells.has(k)) check('Keine Hindernisse auf dem Weg', overlap.length === 0, overlap.join(',')) // 3. obstacles exist and are removable const obstacleCells = [...eng.blockedCells] check('Hindernisse vorhanden', obstacleCells.length >= 8, `${obstacleCells.length} Zellen`) const [otx, oty] = obstacleCells[0].split(',').map(Number) eng.money = 999999 eng.selectedObstacle = { tx: otx, ty: oty } eng.removeSelectedObstacle() check('Hindernis entfernbar', !eng.blockedCells.has(`${otx},${oty}`) && eng.canPlace(otx, oty)) // 4. can place a tower next to the path const candidates: [number, number][] = [] for (let tx = 0; tx < 20 && candidates.length < 1; tx++) { for (let ty = 0; ty < 11; ty++) { if (eng.canPlace(tx, ty)) { candidates.push([tx, ty]) break } } } check('Freier Bauplatz gefunden', candidates.length > 0) } // 5. maps are distinct from each other console.log('\n=== Karten unterscheiden sich ===') const signatures = MAP_ORDER.map((m) => { const e = new GameEngine() e.setMap(m) return [m, [...e.pathCells].sort().join('|')] as const }) for (let i = 0; i < signatures.length; i++) { for (let j = i + 1; j < signatures.length; j++) { check( `${signatures[i][0]} ≠ ${signatures[j][0]}`, signatures[i][1] !== signatures[j][1], ) } } // 6. lockstep determinism per map (same action stream on two engines) console.log('\n=== Lockstep-Determinismus je Karte ===') for (const mapId of MAP_ORDER) { const e1 = new GameEngine() const e2 = new GameEngine() for (const e of [e1, e2]) { e.startGame('normal', mapId) e.mpGameActive = true e.mpMode = 'coop' e.coopHpMul = 1.7 } const script: [number, Parameters[0]][] = [ [5, { type: 'build', kind: 'arrow', tx: 8, ty: 5 }], [10, { type: 'wave' }], [200, { type: 'build', kind: 'frost', tx: 4, ty: 0 }], [700, { type: 'wave' }], [900, { type: 'obstacle', tx: 19, ty: 0 }], [1500, { type: 'speed', s: 2 }], ] let idx = 0 for (let tick = 1; tick <= 2400; tick++) { while (idx < script.length && script[idx][0] <= tick) { const a = script[idx][1] e1.applyAction(a, idx % 2) e2.applyAction(a, idx % 2) idx++ } e1.update(1 / 30) e2.update(1 / 30) } const state = (e: GameEngine): string => JSON.stringify({ money: Math.floor(e.money * 1000), lives: e.lives, wave: e.waveNo, phase: e.phase, enemies: e.enemies.map((x) => [x.id, Math.round(x.hp * 1000), x.wp]), towers: e.towers.map((t) => [t.id, t.kind, t.level]), blocked: e.blockedCells.size, pathLen: e.pathPx.length, }) check(`${mapId}: identischer Zustand`, state(e1) === state(e2)) } // 7. full campaign simulation on every map (playable check) console.log('\n=== 20-Wellen-Simulation je Karte ===') for (const mapId of MAP_ORDER) { const e = new GameEngine() e.startGame('easy', mapId) // generous money + a tower on every free tile to verify the map is beatable e.money = 999999 for (let tx = 0; tx < 20; tx++) { for (let ty = 0; ty < 11; ty++) { if (e.canPlace(tx, ty)) e.placeTower('laser', tx, ty) } } let finished = false for (let t = 0; t < 60000 && !finished; t++) { e.update(1 / 30) if (e.phase === 'intermission') { e.startWave(true) } finished = e.phase === 'victory' || e.phase === 'gameover' } check(`${mapId}: Kampagne abgeschlossen`, e.phase === 'victory', `Phase: ${e.phase}, Welle ${e.waveNo}`) } if (failures > 0) { console.error(`\n${failures} Prüfung(en) fehlgeschlagen.`) process.exit(1) } console.log('\nAlle Karten-Prüfungen bestanden. ✅')