131 lines
6.8 KiB
JavaScript
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' });
|
|
});
|