feat(TournamentService, TournamentConfigTab): enhance tournament advancement logic and knockout stage handling

- Introduced a new function to compare advancement candidates based on multiple criteria, improving the selection process for tournament participants.
- Updated participant data structure to include additional metrics for better ranking and comparison.
- Enhanced the TournamentConfigTab to automatically configure knockout stage settings when applicable, ensuring a smoother user experience during tournament setup.
This commit is contained in:
Torsten Schulz (local)
2026-03-28 11:09:40 +01:00
parent adefb120c0
commit 7fdbe85d3c
2 changed files with 106 additions and 8 deletions

View File

@@ -217,6 +217,38 @@ function nextPowerOfTwo(n) {
return p;
}
function compareAdvancementCandidates(a, b) {
const posA = Number(a.position || a.place || 999);
const posB = Number(b.position || b.place || 999);
if (posA !== posB) return posA - posB;
const pointsA = Number(a.points || 0);
const pointsB = Number(b.points || 0);
if (pointsB !== pointsA) return pointsB - pointsA;
const setDiffA = Number(a.setDiff || 0);
const setDiffB = Number(b.setDiff || 0);
if (setDiffB !== setDiffA) return setDiffB - setDiffA;
const setsWonA = Number(a.setsWon || 0);
const setsWonB = Number(b.setsWon || 0);
if (setsWonB !== setsWonA) return setsWonB - setsWonA;
const pointsDiffA = Number(a.pointsDiff || 0);
const pointsDiffB = Number(b.pointsDiff || 0);
if (pointsDiffB !== pointsDiffA) return pointsDiffB - pointsDiffA;
const pointsWonA = Number(a.pointsWon || 0);
const pointsWonB = Number(b.pointsWon || 0);
if (pointsWonB !== pointsWonA) return pointsWonB - pointsWonA;
const groupNumberA = Number(a.groupNumber || 999);
const groupNumberB = Number(b.groupNumber || 999);
if (groupNumberA !== groupNumberB) return groupNumberA - groupNumberB;
return Number(a.id || 0) - Number(b.id || 0);
}
const THIRD_PLACE_ROUND = 'Spiel um Platz 3';
class TournamentService {
/**
@@ -494,6 +526,7 @@ class TournamentService {
const perGroupRanked = relevantStage1Groups.map(g => ({
groupId: g.groupId,
groupNumber: g.groupNumber,
classId: g.classId ?? null,
// WICHTIG:
// - Für interne Teilnehmer brauchen wir die ClubMember-ID (Member.id / TournamentMember.clubMemberId),
@@ -502,6 +535,14 @@ class TournamentService {
participants: (g.participants || []).map(p => ({
id: p.isExternal ? p.id : (p.clubMemberId ?? p.member?.id ?? p.id),
isExternal: !!p.isExternal,
position: Number(p.position || 999),
points: Number(p.points || 0),
setDiff: Number(p.setDiff || 0),
setsWon: Number(p.setsWon || 0),
pointsDiff: Number(p.pointsDiff || 0),
pointsWon: Number(p.pointsWon || 0),
groupId: g.groupId,
groupNumber: g.groupNumber,
})),
}));
@@ -722,16 +763,40 @@ class TournamentService {
}
}
const uniqueEntrants = Array.from(seen.values());
const selectedKeys = new Set(uniqueEntrants.map(entry => `${entry.isExternal ? 'E' : 'M'}:${entry.id}`));
const allRankedCandidates = perGroupRanked
.filter(group => (group.classId ?? null) === (classId ?? null))
.flatMap(group => (group.participants || []).map(participant => ({
id: Number(participant.id),
isExternal: !!participant.isExternal,
classId,
position: Number(participant.position || 999),
points: Number(participant.points || 0),
setDiff: Number(participant.setDiff || 0),
setsWon: Number(participant.setsWon || 0),
pointsDiff: Number(participant.pointsDiff || 0),
pointsWon: Number(participant.pointsWon || 0),
groupId: group.groupId,
groupNumber: group.groupNumber,
})));
const desiredBracketSize = nextPowerOfTwo(uniqueEntrants.length);
if (desiredBracketSize > uniqueEntrants.length) {
const bestAdditionalCandidates = allRankedCandidates
.filter(candidate => !selectedKeys.has(`${candidate.isExternal ? 'E' : 'M'}:${candidate.id}`))
.sort(compareAdvancementCandidates)
.slice(0, desiredBracketSize - uniqueEntrants.length);
bestAdditionalCandidates.forEach(candidate => {
selectedKeys.add(`${candidate.isExternal ? 'E' : 'M'}:${candidate.id}`);
uniqueEntrants.push(candidate);
});
}
const thirdPlace = wantsThirdPlace;
if (uniqueEntrants.length >= 2) {
// Sortiere nach Platz: beste Plätze zuerst, dann schlechtere
// Wenn mehrere Teilnehmer den gleichen Platz haben, behalte die ursprüngliche Reihenfolge
uniqueEntrants.sort((a, b) => {
const placeA = a.place || 999;
const placeB = b.place || 999;
return placeA - placeB;
});
uniqueEntrants.sort(compareAdvancementCandidates);
// Paare: Bester gegen Schlechtesten, Zweiter gegen Vorletzten, etc.
// Reverse die zweite Hälfte, um das gewünschte Pairing zu erreichen