186 lines
6.9 KiB
JavaScript
186 lines
6.9 KiB
JavaScript
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);
|
||
});
|
||
});
|