feat(TournamentService): implement seeded knockout match generation and enhance qualifier handling
- Added a new function to build seeded legacy knockout matches, improving match pairing logic based on group affiliations. - Refactored qualifier selection process to utilize a ranked participants map, ensuring better handling of advancing participants and unused qualifiers. - Updated knockout match handling to incorporate seeded results, enhancing the overall tournament flow and participant management.
This commit is contained in:
@@ -302,6 +302,38 @@ function buildPreferredLegacyKnockoutMatches(qualifiersByGroup, advancingPerGrou
|
||||
return matches.length > 0 ? matches : null;
|
||||
}
|
||||
|
||||
function buildSeededLegacyKnockoutMatches(classQualifiers) {
|
||||
const remaining = [...(classQualifiers || [])].sort(compareAdvancementCandidates);
|
||||
const matches = [];
|
||||
|
||||
while (remaining.length >= 2) {
|
||||
const player1 = remaining.shift();
|
||||
if (!player1 || !player1.id) continue;
|
||||
|
||||
let opponentIndex = remaining.length - 1;
|
||||
while (
|
||||
opponentIndex >= 0 &&
|
||||
String(remaining[opponentIndex]?.groupId ?? '') === String(player1.groupId ?? '')
|
||||
) {
|
||||
opponentIndex -= 1;
|
||||
}
|
||||
|
||||
if (opponentIndex < 0) {
|
||||
opponentIndex = remaining.length - 1;
|
||||
}
|
||||
|
||||
const [player2] = remaining.splice(opponentIndex, 1);
|
||||
if (!player2 || !player2.id || player1.id === player2.id) continue;
|
||||
|
||||
matches.push({ player1, player2 });
|
||||
}
|
||||
|
||||
return {
|
||||
matches,
|
||||
unusedQualifiers: remaining,
|
||||
};
|
||||
}
|
||||
|
||||
const THIRD_PLACE_ROUND = 'Spiel um Platz 3';
|
||||
class TournamentService {
|
||||
/**
|
||||
@@ -2832,7 +2864,7 @@ Ve // 2. Neues Turnier anlegen
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
const qualifiers = [];
|
||||
const rankedParticipantsByClass = new Map();
|
||||
for (const g of groups) {
|
||||
const classId = g.classId;
|
||||
const isDoubles = classId ? (classIsDoublesMap[classId] || false) : false;
|
||||
@@ -2941,23 +2973,58 @@ Ve // 2. Neues Turnier anlegen
|
||||
// Fallback: Nach ID
|
||||
return a.member.id - b.member.id;
|
||||
});
|
||||
// Füge classId und groupId zur Gruppe hinzu
|
||||
// r.member ist entweder TournamentMember oder ExternalTournamentParticipant
|
||||
qualifiers.push(...ranked.slice(0, tournament.advancingPerGroup).map((r, position) => {
|
||||
const classKey = classId == null ? 'null' : String(classId);
|
||||
const groupNumber = Number(g.number || g.groupNumber || g.sortOrder || g.id || 999);
|
||||
const rankedParticipants = ranked.map((r, position) => {
|
||||
const member = r.member;
|
||||
// Stelle sicher, dass id vorhanden ist
|
||||
if (!member || !member.id) {
|
||||
devLog(`[_determineQualifiers] Warning: Member without id found in group ${g.id}`);
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: member.id,
|
||||
classId: g.classId,
|
||||
classId,
|
||||
groupId: g.id,
|
||||
isExternal: r.isExternal || false,
|
||||
position: position + 1 // 1-basierte Position innerhalb der Gruppe (1., 2., 3., etc.)
|
||||
position: position + 1,
|
||||
points: Number(r.points || 0),
|
||||
setDiff: Number((r.setsWon || 0) - (r.setsLost || 0)),
|
||||
setsWon: Number(r.setsWon || 0),
|
||||
pointsDiff: Number(r.pointsDiff || 0),
|
||||
pointsWon: Number(r.pointsWon || 0),
|
||||
groupNumber,
|
||||
};
|
||||
}).filter(q => q !== null));
|
||||
}).filter(q => q !== null);
|
||||
|
||||
if (!rankedParticipantsByClass.has(classKey)) {
|
||||
rankedParticipantsByClass.set(classKey, []);
|
||||
}
|
||||
rankedParticipantsByClass.get(classKey).push(...rankedParticipants);
|
||||
}
|
||||
|
||||
const qualifiers = [];
|
||||
for (const [classKey, rankedParticipants] of rankedParticipantsByClass.entries()) {
|
||||
const explicitQualifiers = rankedParticipants
|
||||
.filter(participant => Number(participant.position || 999) <= Number(tournament.advancingPerGroup || 0))
|
||||
.sort(compareAdvancementCandidates);
|
||||
|
||||
const selectedKeys = new Set(explicitQualifiers.map(participant => `${participant.isExternal ? 'E' : 'M'}:${participant.id}`));
|
||||
const desiredBracketSize = nextPowerOfTwo(explicitQualifiers.length);
|
||||
|
||||
if (desiredBracketSize > explicitQualifiers.length) {
|
||||
const additionalQualifiers = rankedParticipants
|
||||
.filter(participant => !selectedKeys.has(`${participant.isExternal ? 'E' : 'M'}:${participant.id}`))
|
||||
.sort(compareAdvancementCandidates)
|
||||
.slice(0, desiredBracketSize - explicitQualifiers.length);
|
||||
|
||||
additionalQualifiers.forEach(participant => {
|
||||
selectedKeys.add(`${participant.isExternal ? 'E' : 'M'}:${participant.id}`);
|
||||
explicitQualifiers.push(participant);
|
||||
});
|
||||
}
|
||||
|
||||
explicitQualifiers.sort(compareAdvancementCandidates);
|
||||
qualifiers.push(...explicitQualifiers);
|
||||
}
|
||||
return qualifiers;
|
||||
}
|
||||
@@ -3065,7 +3132,16 @@ Ve // 2. Neues Turnier anlegen
|
||||
});
|
||||
|
||||
const advancingPerGroup = t.advancingPerGroup;
|
||||
let matches = buildPreferredLegacyKnockoutMatches(qualifiersByGroup, advancingPerGroup);
|
||||
let matches = null;
|
||||
let unusedQualifiers = [];
|
||||
|
||||
const seededResult = buildSeededLegacyKnockoutMatches(classQualifiers);
|
||||
if (seededResult.matches.length > 0) {
|
||||
matches = seededResult.matches;
|
||||
unusedQualifiers = seededResult.unusedQualifiers;
|
||||
} else {
|
||||
matches = buildPreferredLegacyKnockoutMatches(qualifiersByGroup, advancingPerGroup);
|
||||
}
|
||||
|
||||
if (!matches) {
|
||||
const groups = Object.keys(qualifiersByGroup).sort(compareQualifierGroups);
|
||||
@@ -3124,14 +3200,14 @@ Ve // 2. Neues Turnier anlegen
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
unusedQualifiers = classQualifiers.filter(q => q && q.id && !usedQualifiers.has(q.id));
|
||||
}
|
||||
|
||||
// Falls Qualifiers übrig bleiben (ungerade Teilnehmerzahl / keine gültige Paarung möglich):
|
||||
// Freilos vergeben. Wir erzeugen KEIN Match mit doppelten Spielern.
|
||||
// Der Spieler mit Freilos wird in späteren Runden berücksichtigt, sobald dort (durch Ergebnisse)
|
||||
// echte Gegner feststehen. (Passt zur Vorgabe: keine Placeholder-Matches ohne bekannte Spieler.)
|
||||
const usedQualifierIds = new Set(matches.flatMap(match => [match.player1?.id, match.player2?.id]).filter(id => Number.isFinite(id) && id > 0));
|
||||
const unusedQualifiers = classQualifiers.filter(q => q && q.id && !usedQualifierIds.has(q.id));
|
||||
if (unusedQualifiers.length > 0) {
|
||||
devLog(`[startKnockout] Assigning ${unusedQualifiers.length} bye(s) for class ${classKey}:`, unusedQualifiers.map(q => q.id));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user