import { describe, expect, it } from 'vitest'; import { ENEMY_TYPES, WAVES } from '../../app/static/js/config.js'; import { createRng } from '../../app/static/js/logic/rng.js'; import { EDGES, eliteCount, generateWave, spawnPointOnEdge, waveBudget, waveFactions, waveMultipliers, } from '../../app/static/js/logic/waves.js'; const cost = (plan) => plan.groups.reduce((sum, g) => sum + g.count * ENEMY_TYPES[g.type].cost, 0); const factionsIn = (plan) => new Set(plan.groups.map((g) => ENEMY_TYPES[g.type].faction)); const tiersIn = (plan) => plan.groups.map((g) => ENEMY_TYPES[g.type].tier); describe('waveBudget', () => { it('beginnt mit dem Startbudget und wächst streng monoton', () => { expect(waveBudget(1)).toBe(WAVES.firstBudget); for (let n = 1; n < 50; n += 1) { expect(waveBudget(n + 1)).toBeGreaterThan(waveBudget(n)); } }); }); describe('waveFactions', () => { it('führt die Fraktionen nacheinander ein', () => { expect(waveFactions(1)).toEqual(['swarm']); expect(waveFactions(2)).toEqual(['swarm']); expect(waveFactions(3)).toEqual(['legion']); expect(waveFactions(4)).toEqual(['legion']); expect(waveFactions(5)).toEqual(['swarm', 'legion']); expect(waveFactions(30)).toEqual(['swarm', 'legion']); }); }); describe('eliteCount', () => { it('bringt Eliten nur in jeder n-ten Welle und begrenzt ihre Zahl', () => { expect(eliteCount(4)).toBe(0); expect(eliteCount(5)).toBe(1); expect(eliteCount(7)).toBe(0); expect(eliteCount(10)).toBe(2); expect(eliteCount(WAVES.eliteEvery * 50)).toBe(WAVES.maxElitesPerWave); }); }); describe('waveMultipliers', () => { it('lässt frühe Wellen unverändert', () => { expect(waveMultipliers(1)).toEqual({ hp: 1, speed: 1 }); expect(waveMultipliers(WAVES.scalingFromWave - 1)).toEqual({ hp: 1, speed: 1 }); }); it('macht spätere Wellen zäher, deckelt aber das Tempo', () => { expect(waveMultipliers(WAVES.scalingFromWave).hp).toBeGreaterThan(1); expect(waveMultipliers(20).hp).toBeGreaterThan(waveMultipliers(10).hp); expect(waveMultipliers(500).speed).toBe(WAVES.speedScaleMax); }); }); describe('generateWave', () => { it('ist mit gleichem Seed reproduzierbar', () => { expect(generateWave(7, createRng(42))).toEqual(generateWave(7, createRng(42))); }); it('liefert mit anderem Seed eine andere Welle', () => { expect(generateWave(7, createRng(1))).not.toEqual(generateWave(7, createRng(2))); }); it('wirft bei ungültiger Wellennummer', () => { expect(() => generateWave(0, createRng(1))).toThrow(RangeError); expect(() => generateWave(1.5, createRng(1))).toThrow(RangeError); }); it('schickt in Welle 1–2 nur Schwärmer und in Welle 3–4 nur die Blechlegion', () => { for (let seed = 0; seed < 20; seed += 1) { expect(factionsIn(generateWave(1, createRng(seed)))).toEqual(new Set(['swarm'])); expect(factionsIn(generateWave(2, createRng(seed)))).toEqual(new Set(['swarm'])); expect(factionsIn(generateWave(3, createRng(seed)))).toEqual(new Set(['legion'])); expect(factionsIn(generateWave(4, createRng(seed)))).toEqual(new Set(['legion'])); } }); it('mischt ab Welle 6 beide Fraktionen', () => { const seen = new Set(); for (let seed = 0; seed < 20; seed += 1) { for (const faction of factionsIn(generateWave(6, createRng(seed)))) seen.add(faction); } expect(seen).toEqual(new Set(['swarm', 'legion'])); }); it('enthält Eliten genau in Elite-Wellen', () => { for (let n = 1; n <= 20; n += 1) { const plan = generateWave(n, createRng(n)); const elites = tiersIn(plan).filter((t) => t === 'elite').length; expect(elites, `Welle ${n}`).toBe(eliteCount(n)); } }); it('wechselt die Elite-Fraktion zwischen Elite-Wellen', () => { const eliteFaction = (n) => generateWave(n, createRng(3)) .groups.map((g) => ENEMY_TYPES[g.type]) .find((t) => t.tier === 'elite').faction; expect(eliteFaction(5)).toBe('swarm'); expect(eliteFaction(10)).toBe('legion'); }); it('schöpft das Budget in Wellen ohne Eliten fast vollständig aus', () => { const cheapest = Math.min(...Object.values(ENEMY_TYPES).map((t) => t.cost)); for (const n of [1, 2, 3, 4, 6, 7, 8, 13]) { for (let seed = 0; seed < 10; seed += 1) { const plan = generateWave(n, createRng(seed)); expect(cost(plan)).toBeLessThanOrEqual(plan.budget); // Rest kleiner als der teuerste Kleingegner einer Fraktion (Legion: 2). expect(plan.budget - cost(plan)).toBeLessThan(cheapest + 2); } } }); it('hält die Gruppengrößen der Gegnertypen ein', () => { for (let n = 1; n <= 25; n += 1) { for (const group of generateWave(n, createRng(n * 7)).groups) { const { groupSize } = ENEMY_TYPES[group.type]; expect(group.count).toBeGreaterThanOrEqual(1); expect(group.count).toBeLessThanOrEqual(groupSize.max); } } }); it('plant Spawns zeitlich aufsteigend ab 0 ms und an gültigen Rändern', () => { const plan = generateWave(12, createRng(99)); expect(plan.groups[0].atMs).toBe(0); for (let i = 1; i < plan.groups.length; i += 1) { expect(plan.groups[i].atMs).toBeGreaterThanOrEqual(plan.groups[i - 1].atMs); } for (const group of plan.groups) expect(EDGES).toContain(group.edge); }); it('zählt die Gegner korrekt und wird mit der Zeit größer', () => { const early = generateWave(2, createRng(5)); const late = generateWave(15, createRng(5)); expect(early.totalEnemies).toBe(early.groups.reduce((s, g) => s + g.count, 0)); expect(late.totalEnemies).toBeGreaterThan(early.totalEnemies); }); it('setzt mehr mittlere Gegner in späten Wellen ein', () => { const mediumShare = (n) => { let medium = 0; let total = 0; for (let seed = 0; seed < 30; seed += 1) { const plan = generateWave(n, createRng(seed)); for (const g of plan.groups) { const t = ENEMY_TYPES[g.type]; if (t.tier === 'medium') medium += g.count * t.cost; total += g.count * t.cost; } } return medium / total; }; expect(mediumShare(12)).toBeGreaterThan(mediumShare(2)); }); }); describe('spawnPointOnEdge', () => { const world = { width: 2000, height: 1000 }; it('setzt Punkte an den richtigen Rand innerhalb der Welt', () => { const rng = createRng(1); expect(spawnPointOnEdge('north', world, 40, rng).y).toBe(40); expect(spawnPointOnEdge('south', world, 40, rng).y).toBe(960); expect(spawnPointOnEdge('west', world, 40, rng).x).toBe(40); expect(spawnPointOnEdge('east', world, 40, rng).x).toBe(1960); for (let i = 0; i < 100; i += 1) { const p = spawnPointOnEdge('north', world, 40, rng); expect(p.x).toBeGreaterThanOrEqual(40); expect(p.x).toBeLessThanOrEqual(1960); } }); it('wirft bei unbekanntem Rand', () => { expect(() => spawnPointOnEdge('oben', world, 40, createRng(1))).toThrow(RangeError); }); });