slop game oder so

This commit is contained in:
2026-09-24 17:57:54 +02:00
commit 4c997eee21
166 changed files with 14976 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
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);
});
});