Backend: Match-Lobbys, Map-Veto, Echtzeit und CS2-Serveradapter
Express + Socket.IO + SQLite. Veto-Engine mit konfigurierbaren Map-Pools und validierten Abläufen, rollenbasierte Berechtigungen, Server-Lebenszyklus mit Demo-, Docker- und Static-Pool-Adapter (RCON), automatisches Herunterfahren.
This commit is contained in:
commit
f08297dd0d
27 files changed
+6355
No files matched your search
@@ -0,0 +1,93 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { computeVetoState, resolveAction, validateFlow } from '../src/veto.js';
|
||||
import { buildMapPools, loadMapPools } from '../src/mapPools.js';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const pools = loadMapPools(path.join(here, '..', 'config', 'map-pools.json'));
|
||||
|
||||
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.bo3.supported, false);
|
||||
assert.match(custom.mini.formats.bo5.reason, /Kein Veto-Ablauf/);
|
||||
});
|
||||
|
||||
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(pools.competitive, '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 = pools.competitive;
|
||||
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' });
|
||||
});
|
||||
Reference in new issue
Block a user