163 lines
4 KiB
TypeScript
163 lines
4 KiB
TypeScript
/**
|
|
* 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 type { TowerKind } from '../src/game/types.ts'
|
|
|
|
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<typeof eng.towers.at> = 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 {
|
|
const costs: Record<TowerKind, number> = { arrow: 50, cannon: 110, frost: 80, tesla: 130, laser: 160 }
|
|
return costs[kind]
|
|
}
|
|
|
|
function upgradeCostOf(kind: TowerKind, level: number): number | null {
|
|
const up: Record<TowerKind, [number, number]> = {
|
|
arrow: [60, 110],
|
|
cannon: [100, 180],
|
|
frost: [70, 130],
|
|
tesla: [120, 200],
|
|
laser: [150, 260],
|
|
}
|
|
if (level >= 3) return null
|
|
return up[kind][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<string, number> = {}
|
|
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).')
|
|
}
|