Live-Ergebnisse: Stream-Seite, Anzeige, automatischer Mapwechsel
- Stream-Übersicht unter /stream/<Zuschauercode> (1920x1080, Lobby, Veto, Spielstand) - Live-Stand, Rundenverlauf und Serienstand auf Match-Seite und Übersicht - Parser mit echten AMP-Logs geprüft: Game Over mit leerer Map-Gruppe, Match_Start vor der Aufwärmphase, veraltete Seitenzuordnung nach der Halbzeit - Nach Map-Ende automatisch nächste Map bzw. Abschluss der entschiedenen Serie - LOG_DEBUG zur Fehlersuche, README und .env.example ergänzt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5fade769e7
commit
43dc5b837a
20 files changed
+901
-44
No files matched your search
@@ -1,7 +1,12 @@
|
||||
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 - ';
|
||||
|
||||
@@ -12,7 +17,7 @@ function round(winnerSide, ct, t, notice) {
|
||||
}
|
||||
|
||||
test('Logzeilen ergeben Spielstand, Rundenverlauf und Gründe', () => {
|
||||
let state = initialLiveState({ map: 'de_mirage', ctTeam: 'A' });
|
||||
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'),
|
||||
@@ -28,31 +33,62 @@ test('Logzeilen ergeben Spielstand, Rundenverlauf und Gründe', () => {
|
||||
});
|
||||
|
||||
test('Doppelt gesendete Zeilen werden nur einmal gezählt', () => {
|
||||
const state = initialLiveState({ map: 'de_nuke', ctTeam: 'A' });
|
||||
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('Seitenwechsel über "Team playing"-Zeilen', () => {
|
||||
let s = initialLiveState({ map: 'de_nuke', ctTeam: 'A' });
|
||||
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, [
|
||||
`${ts}MatchStatus: Team playing "CT": MaxLAN Blau`,
|
||||
`${ts}MatchStatus: Team playing "TERRORIST": MaxLAN Rot`,
|
||||
// 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),
|
||||
// Halbzeit: Rot ist jetzt CT. In den Seitenwerten steht der Stand der Teams auf der jeweiligen Seite.
|
||||
`${ts}MatchStatus: Team playing "CT": MaxLAN Rot`,
|
||||
`${ts}MatchStatus: Team playing "TERRORIST": MaxLAN Blau`,
|
||||
round('CT', 1, 1),
|
||||
...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 Logzeilen eines AMP-Servers (Wingman, 4 Runden, Halbzeit nach 2)', () => {
|
||||
const lines = fs.readFileSync(path.join(here, 'fixtures', 'amp-wingman-inferno.log'), 'utf8').split(/\r?\n/).filter(Boolean);
|
||||
const warmupEnd = lines.findIndex((l) => l.includes('command "mp_warmup_end"'));
|
||||
const amp = { 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), amp));
|
||||
assert.equal(s.status, 'warmup');
|
||||
s.warmupEnded = true; // Organisator hat "Aufwärmphase beenden" geklickt
|
||||
({ state: s } = applyLogLines(s, lines.slice(warmupEnd), amp));
|
||||
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], amp));
|
||||
check(t);
|
||||
});
|
||||
|
||||
test('Ohne "Team playing": Halbzeitwechsel nach Regelwerk', () => {
|
||||
let s = initialLiveState({ map: 'de_inferno', ctTeam: 'B' });
|
||||
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"`,
|
||||
@@ -65,7 +101,7 @@ test('Ohne "Team playing": Halbzeitwechsel nach Regelwerk', () => {
|
||||
});
|
||||
|
||||
test('Game Over beendet die Map, Aufwärmrunden vor Match_Start zählen nicht', () => {
|
||||
let s = initialLiveState({ map: 'de_dust2', ctTeam: 'A' });
|
||||
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"`,
|
||||
@@ -79,9 +115,20 @@ test('Game Over beendet die Map, Aufwärmrunden vor Match_Start zählen nicht',
|
||||
});
|
||||
|
||||
test('Zeilen einer anderen Map werden ignoriert, Serienstand wird gezählt', () => {
|
||||
const s = initialLiveState({ map: 'de_dust2', ctTeam: 'A' });
|
||||
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');
|
||||
});
|
||||
Reference in new issue
Block a user