import { test } from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { applyLogLines, initialLiveState, seriesScore, splitLogBody } from '../src/liveScore.js'; const here = path.dirname(fileURLToPath(import.meta.url)); const ctx = { teamNames: { A: 'MaxLAN Blau', B: 'MaxLAN Rot' }, maxRounds: 24, overtimeRounds: 6 }; const ts = '10/25/2026 - 18:00:00.000 - '; function round(winnerSide, ct, t, notice) { const n = notice ?? (winnerSide === 'CT' ? 'CTs_Win' : 'Terrorists_Win'); const side = winnerSide === 'CT' ? 'CT' : 'TERRORIST'; return `${ts}Team "${side}" triggered "SFUI_Notice_${n}" (CT "${ct}") (T "${t}")`; } test('Logzeilen ergeben Spielstand, Rundenverlauf und Gründe', () => { let state = initialLiveState({ map: 'de_mirage', ctTeam: 'A', warmupEnded: true }); const lines = [ `${ts}World triggered "Match_Start" on "de_mirage"`, round('T', 0, 1, 'Target_Bombed'), round('CT', 1, 1, 'Bomb_Defused'), round('CT', 2, 1, 'Target_Saved'), ]; const { state: next, events } = applyLogLines(state, lines, ctx); state = next; assert.equal(state.status, 'live'); assert.deepEqual(state.score, { A: 2, B: 1 }); assert.deepEqual(state.rounds.map((r) => [r.winner, r.reason]), [['B', 'bomb'], ['A', 'defuse'], ['A', 'time']]); assert.equal(events.filter((e) => e.type === 'round').length, 3); }); test('Doppelt gesendete Zeilen werden nur einmal gezählt', () => { const state = initialLiveState({ map: 'de_nuke', ctTeam: 'A', warmupEnded: true }); const line = round('CT', 1, 0); const { state: s } = applyLogLines(state, [line, line, line], ctx); assert.equal(s.rounds.length, 1); assert.deepEqual(s.score, { A: 1, B: 0 }); }); test('Seitenzuordnung nach dem Rundenende hat Vorrang, veraltete Zeilen zu Rundenbeginn zählen nicht', () => { let s = initialLiveState({ map: 'de_nuke', ctTeam: 'B', warmupEnded: true }); const playing = (ct, t) => [`${ts}MatchStatus: Team playing "CT": ${ct}`, `${ts}MatchStatus: Team playing "TERRORIST": ${t}`]; ({ state: s } = applyLogLines(s, [ // Veto sagt Rot auf CT, tatsächlich steht Blau auf CT – das Log gewinnt `${ts}World triggered "Match_Start" on "de_nuke"`, ...playing('MaxLAN Blau', 'MaxLAN Rot'), round('CT', 1, 0), ...playing('MaxLAN Blau', 'MaxLAN Rot'), `${ts}World triggered "Round_End"`, // veraltete Zeile zu Beginn der nächsten Runde (darf nichts ändern) ...playing('MaxLAN Rot', 'MaxLAN Blau'), round('T', 1, 1), ...playing('MaxLAN Blau', 'MaxLAN Rot'), `${ts}World triggered "Round_End"`, ], ctx)); assert.deepEqual(s.score, { A: 1, B: 1 }); assert.deepEqual(s.rounds.map((r) => r.winner), ['A', 'B']); }); test('Echte, anonymisierte Logzeilen eines CS2-Servers (Wingman, 4 Runden, Halbzeit nach 2)', () => { const lines = fs.readFileSync(path.join(here, 'fixtures', 'wingman-anonym.log'), 'utf8').split(/\r?\n/).filter(Boolean); const warmupEnd = lines.findIndex((l) => l.includes('command "mp_warmup_end"')); const ctx = { teamNames: { A: 'MaxLAN Blau', B: 'MaxLAN Rot' }, maxRounds: 4, overtimeRounds: 6 }; const check = (s) => { assert.equal(s.status, 'finished'); assert.equal(s.winner, 'A'); assert.deepEqual(s.score, { A: 3, B: 1 }); assert.deepEqual(s.rounds.map((r) => r.winner), ['B', 'A', 'A', 'A']); assert.ok(s.rounds.every((r) => r.reason === 'elimination')); }; // a) wie vom Server in Paketen geschickt let s = initialLiveState({ map: 'de_inferno', ctTeam: 'A' }); ({ state: s } = applyLogLines(s, lines.slice(0, warmupEnd), ctx)); assert.equal(s.status, 'warmup'); s.warmupEnded = true; // Organisator hat "Aufwärmphase beenden" geklickt ({ state: s } = applyLogLines(s, lines.slice(warmupEnd), ctx)); check(s); // b) jede Zeile einzeln (ungünstigste Paketgrenzen) let t = initialLiveState({ map: 'de_inferno', ctTeam: 'A', warmupEnded: true }); for (const line of lines.slice(warmupEnd)) ({ state: t } = applyLogLines(t, [line], ctx)); check(t); }); test('Ohne "Team playing": Halbzeitwechsel nach Regelwerk', () => { let s = initialLiveState({ map: 'de_inferno', ctTeam: 'B', warmupEnded: true }); const small = { ...ctx, maxRounds: 4 }; ({ state: s } = applyLogLines(s, [ `${ts}World triggered "Match_Start" on "de_inferno"`, round('CT', 1, 0), // B gewinnt als CT round('CT', 2, 0), // B gewinnt als CT -> Halbzeit, B ist jetzt T round('T', 0, 3), // B gewinnt als T: CT-Seite (A) hat 0, T-Seite (B) hat 3 ], small)); assert.deepEqual(s.score, { A: 0, B: 3 }); assert.deepEqual(s.rounds.map((r) => r.winner), ['B', 'B', 'B']); }); test('Game Over beendet die Map, Aufwärmrunden vor Match_Start zählen nicht', () => { let s = initialLiveState({ map: 'de_dust2', ctTeam: 'A', warmupEnded: true }); ({ state: s } = applyLogLines(s, [ round('T', 0, 1), // Aufwärm-/Messerrunde `${ts}World triggered "Match_Start" on "de_dust2"`, round('CT', 1, 0), `${ts}Game Over: competitive mg_active de_dust2 score 1:0 after 2 min`, round('T', 1, 1), // nach Spielende ignoriert ], ctx)); assert.equal(s.status, 'finished'); assert.equal(s.winner, 'A'); assert.deepEqual(s.score, { A: 1, B: 0 }); }); test('Zeilen einer anderen Map werden ignoriert, Serienstand wird gezählt', () => { const s = initialLiveState({ map: 'de_dust2', ctTeam: 'A', warmupEnded: true }); const { state } = applyLogLines(s, [`${ts}Game Over: competitive mg_active de_nuke score 13:2 after 30 min`], ctx); assert.equal(state.status, 'warmup'); assert.deepEqual(seriesScore([{ winner: 'A' }, { winner: 'B' }, { winner: 'A' }, { winner: null }]), { A: 2, B: 1 }); assert.deepEqual(splitLogBody('a\r\n\r\nb\n'), ['a', 'b']); }); test('Match_Start vor dem Ende der Aufwärmphase startet keine Zählung', () => { let s = initialLiveState({ map: 'de_inferno', ctTeam: 'A' }); const start = `${ts}World triggered "Match_Start" on "de_inferno"`; ({ state: s } = applyLogLines(s, [start], ctx)); assert.equal(s.status, 'warmup'); s.warmupEnded = true; const { state, events } = applyLogLines(s, [start], ctx); assert.equal(state.status, 'live'); assert.equal(events[0].type, 'match_start'); });