Files
slop-game-opus5.5/tests/js/waves.test.js
T
2026-09-24 17:57:54 +02:00

186 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});
});