/** * Headless balance simulation: a greedy bot plays the game on 'normal' * to verify the campaign is winnable (and not trivially easy). * * Run: npm run sim */ import { GameEngine } from '../src/game/engine.ts' import { TOWERS } from '../src/game/config.ts' import type { TowerKind } from '../src/game/types.ts' /** campaign balance stays tuned for 3 levels – evolutions are endless-mode luxury */ const SIM_MAX_LEVEL = 3 const eng = new GameEngine() eng.startGame('normal') // predetermined build spots (tile coords) near path hot spots const spots: [number, number][] = [ [8, 5], // center between the two long vertical runs [3, 0], [7, 0], [11, 0], [4, 4], [12, 6], [8, 8], [8, 10], [12, 10], [16, 3], [18, 4], [15, 0], [4, 7], [8, 2], [12, 3], [7, 10], [13, 10], [1, 4], [18, 6], [16, 6], [5, 0], [9, 0], [13, 0], [8, 6], [12, 7], [4, 2], ] // what to build in which order (afterwards: upgrades only) const plan: TowerKind[] = [ 'arrow', 'frost', 'arrow', 'cannon', 'arrow', 'tesla', 'frost', 'arrow', 'cannon', 'laser', 'tesla', 'laser', 'frost', 'cannon', 'tesla', 'arrow', 'laser', 'frost', 'cannon', 'tesla', 'laser', 'arrow', 'arrow', 'arrow', 'arrow', 'arrow', ] function bot(): void { // spend greedily: cheapest sensible action first, repeat for (;;) { let acted = false // next planned build on the next free spot const built = eng.towers.length if (built < plan.length) { const kind = plan[built] const spot = spots[built % spots.length] const cost = towerCost(kind) if (eng.money >= cost && eng.canPlace(spot[0], spot[1])) { if (!eng.placeTower(kind, spot[0], spot[1])) throw new Error('place failed at ' + spot) acted = true continue } // spot blocked -> skip this spot for that build if (!eng.canPlace(spot[0], spot[1])) { // try any other free spot const alt = spots.find((s) => eng.canPlace(s[0], s[1])) if (alt && eng.money >= cost) { eng.placeTower(kind, alt[0], alt[1]) acted = true continue } } } // cheapest upgrade among towers (max 3 levels) let bestCost = Infinity let bestT: ReturnType = undefined for (const t of eng.towers) { const c = upgradeCostOf(t.kind, t.level) if (c !== null && c < bestCost) { bestCost = c bestT = t } } if (bestT && bestCost <= eng.money) { eng.upgradeTower(bestT) acted = true continue } if (!acted) break } // start next wave early for the bonus, like an aggressive player if (eng.phase === 'intermission') eng.startWave(true) } function towerCost(kind: TowerKind): number { return TOWERS[kind].cost } function upgradeCostOf(kind: TowerKind, level: number): number | null { if (level >= SIM_MAX_LEVEL) return null return TOWERS[kind].upgradeCost[level - 1] } let lastWave = 0 let guard = 0 while (eng.phase !== 'gameover' && eng.phase !== 'victory' && guard < 60 * 60 * 60) { eng.update(1 / 60) bot() if (eng.waveNo !== lastWave) { lastWave = eng.waveNo const kindCounts: Record = {} for (const t of eng.towers) kindCounts[t.kind + t.level] = (kindCounts[t.kind + t.level] ?? 0) + 1 console.log( `Welle ${String(eng.waveNo).padStart(2)} | ❤ ${String(eng.lives).padStart(2)} | 🪙 ${String(Math.floor(eng.money)).padStart(4)} | Türme: ${eng.towers.length} ${JSON.stringify(kindCounts)}`, ) } guard++ } console.log('---') console.log(`Ergebnis: ${eng.phase === 'victory' ? 'SIEG' : 'NIEDERLAGE'} in Welle ${eng.waveNo}, Leben übrig: ${eng.lives}, Punkte: ${eng.score}`) if (eng.phase !== 'victory') { console.log(' → Balancing zu schwer! Gegner-Werte abschwächen.') } else if (eng.lives > 14) { console.log(' → Balancing zu leicht (zu viele Leben übrig).') }