Files
Cs2-Mapveto/server/test/veto.test.js
T

131 lines
6.8 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { computeVetoState, generateFlow, resolveAction, validateFlow } from '../src/veto.js';
import { buildMapPools, loadMapPools, setActiveMaps } from '../src/mapPools.js';
const here = path.dirname(fileURLToPath(import.meta.url));
const pools = loadMapPools(path.join(here, '..', 'config', 'map-pools.json'));
// Die klassischen 7 Competitive-Maps: dafür gilt der fest hinterlegte Ablauf
const SEVEN = ['de_ancient', 'de_anubis', 'de_dust2', 'de_inferno', 'de_mirage', 'de_nuke', 'de_train'];
const competitive7 = () => setActiveMaps(structuredClone(pools.competitive), SEVEN);
test('alle ausgelieferten Abläufe sind gültig, Wingman-BO5 ist nicht unterstützt', () => {
assert.equal(pools.competitive.formats.bo1.supported, true);
assert.equal(pools.competitive.formats.bo3.supported, true);
assert.equal(pools.competitive.formats.bo5.supported, true);
assert.equal(pools.wingman.formats.bo1.supported, true);
assert.equal(pools.wingman.formats.bo3.supported, true);
assert.equal(pools.wingman.formats.bo5.supported, false);
});
test('validateFlow erkennt unpassende Abläufe', () => {
assert.deepEqual(validateFlow(['ban:X', 'ban:Y', 'decider'], 3, 'bo1'), []);
assert.ok(validateFlow(['ban:X', 'decider'], 3, 'bo1').length > 0, 'zu wenige Bans');
assert.ok(validateFlow(['ban:X', 'ban:Y', 'decider'], 3, 'bo3').length > 0, 'fehlende Picks');
assert.ok(validateFlow(['side:X', 'ban:Y', 'ban:X', 'decider'], 3, 'bo1').length > 0, 'Seitenwahl ohne Pick');
assert.ok(validateFlow(['ban:X', 'decider', 'ban:Y'], 3, 'bo1').length > 0, 'Ban nach Entscheidungsmap');
assert.ok(validateFlow(['foo:X'], 1, 'bo1').length > 0, 'unbekannter Schritt');
});
test('ungültiger Ablauf in der Konfiguration deaktiviert nur dieses Format', () => {
const custom = buildMapPools({
pools: {
mini: {
label: 'Mini', teamSize: 2, gameType: 0, gameMode: 2,
maps: [{ id: 'de_a', name: 'A' }, { id: 'de_b', name: 'B' }, { id: 'de_c', name: 'C' }],
flows: { bo1: ['ban:X', 'ban:Y', 'decider'], bo3: ['pick:X', 'decider'] },
},
},
});
assert.equal(custom.mini.formats.bo1.supported, true);
assert.equal(custom.mini.formats.bo1.generated, false, 'passender fester Ablauf wird genommen');
// Ungültiger fester Ablauf: die Anwendung erzeugt einen passenden
assert.equal(custom.mini.formats.bo3.supported, true);
assert.equal(custom.mini.formats.bo3.generated, true);
assert.match(custom.mini.formats.bo5.reason, /mindestens 5 Maps/);
});
test('Erzeugte Abläufe sind für jede Map-Anzahl gültig', () => {
for (let n = 2; n <= 12; n += 1) {
for (const format of ['bo1', 'bo3', 'bo5']) {
const flow = generateFlow(n, format);
const need = { bo1: 2, bo3: 3, bo5: 5 }[format];
if (n < need) {
assert.equal(flow, null, `${format} mit ${n} Maps`);
continue;
}
assert.deepEqual(validateFlow(flow, n, format), [], `${format} mit ${n} Maps: ${flow.join(' ')}`);
}
}
// Bans und Picks wechseln sich ab, die Seite der Entscheidungsmap wählt das Team, das nicht zuletzt dran war
assert.deepEqual(generateFlow(5, 'bo3'), ['ban:X', 'ban:Y', 'pick:X', 'side:Y', 'pick:Y', 'side:X', 'decider', 'side:X']);
assert.deepEqual(generateFlow(3, 'bo1'), ['ban:X', 'ban:Y', 'decider', 'side:X']);
});
test('Aktive Maps wählen: Formate passen sich an, Reihenfolge bleibt wie im Katalog', () => {
const pool = structuredClone(pools.competitive);
setActiveMaps(pool, ['de_vertigo', 'de_cache', 'de_mirage', 'de_unbekannt']);
assert.deepEqual(pool.maps.map((m) => m.id), ['de_cache', 'de_mirage', 'de_vertigo']);
assert.equal(pool.formats.bo1.supported, true);
assert.equal(pool.formats.bo3.supported, true);
assert.equal(pool.formats.bo5.supported, false);
// 10 Maps (alle): kein fester Ablauf passt, es wird erzeugt
assert.equal(pools.competitive.maps.length, 10);
assert.equal(pools.competitive.formats.bo3.generated, true);
// 7 klassische Maps: fester Ablauf aus der Konfiguration
assert.equal(competitive7().formats.bo3.generated, false);
});
function play(pool, format, firstTeam, choices) {
const ctx = { maps: pool.maps, flow: pool.formats[format].flow, firstTeam, actions: [] };
for (const choice of choices) {
const state = computeVetoState(ctx);
const added = resolveAction(ctx, { team: state.current.team, stepIndex: state.current.index, ...choice });
ctx.actions = [...ctx.actions, ...added];
}
return computeVetoState(ctx);
}
test('BO3 Competitive: kompletter Ablauf mit Seitenwahl und Entscheidungsmap', () => {
const state = play(competitive7(), 'bo3', 'B', [
{ map: 'de_train' }, // B bannt
{ map: 'de_nuke' }, // A bannt
{ map: 'de_mirage' }, // B pickt
{ side: 'CT' }, // A wählt Seite auf Mirage
{ map: 'de_inferno' }, // A pickt
{ side: 'T' }, // B wählt Seite auf Inferno
{ map: 'de_dust2' }, // B bannt
{ map: 'de_anubis' }, // A bannt -> Ancient bleibt
{ side: 'CT' }, // B (X) wählt Seite auf der Entscheidungsmap
]);
assert.equal(state.done, true);
assert.deepEqual(state.series.map((s) => s.map), ['de_mirage', 'de_inferno', 'de_ancient']);
assert.equal(state.series[0].pickedBy, 'B');
assert.deepEqual(state.series[0].startSides, { A: 'CT', B: 'T' });
assert.deepEqual(state.series[1].startSides, { B: 'T', A: 'CT' });
assert.equal(state.series[2].pickedBy, null);
assert.deepEqual(state.series[2].startSides, { B: 'CT', A: 'T' });
assert.equal(state.maps.find((m) => m.id === 'de_ancient').status, 'decider');
});
test('BO1 Wingman: Entscheidungsmap wird automatisch übernommen', () => {
const state = play(pools.wingman, 'bo1', 'A', [{ map: 'de_inferno' }, { map: 'de_nuke' }, { map: 'de_overpass' }]);
assert.equal(state.done, false);
assert.equal(state.current.type, 'side');
assert.equal(state.current.team, 'B');
assert.equal(state.current.map, 'de_vertigo');
});
test('falsches Team, veraltete Schritte und vergebene Maps werden abgelehnt', () => {
const pool = competitive7();
const ctx = { maps: pool.maps, flow: pool.formats.bo1.flow, firstTeam: 'A', actions: [] };
assert.throws(() => resolveAction(ctx, { team: 'B', stepIndex: 0, map: 'de_nuke' }), { code: 'not_your_turn' });
assert.throws(() => resolveAction(ctx, { team: 'A', stepIndex: 1, map: 'de_nuke' }), { code: 'stale_step' });
assert.throws(() => resolveAction(ctx, { team: 'A', stepIndex: 0, map: 'de_cache' }), { code: 'invalid_map' });
ctx.actions = resolveAction(ctx, { team: 'A', stepIndex: 0, map: 'de_nuke' });
assert.throws(() => resolveAction(ctx, { team: 'B', stepIndex: 1, map: 'de_nuke' }), { code: 'map_unavailable' });
assert.throws(() => resolveAction(ctx, { team: 'A', stepIndex: 0, map: 'de_train' }), { code: 'stale_step' });
});