🔧핵심 알고리즘 구현
1. 메인 매칭 생성 함수
async generateBracket(tournament, players) {
try {
// 1. 입력값 검증
this.validateInputs(tournament, players);
// 2. 선수 정보 완전성 검증
const completePlayers = await this.validatePlayerData(players);
// 3. 선수 수 조정 (4의 배수)
const adjustedPlayers = this.adjustPlayerCount(completePlayers);
// 4. 성별 필터링
const filteredPlayers = this.applyGenderFilter(adjustedPlayers, tournament.sameGenderOnly);
// 5. 매칭 생성
const matches = this.createMatching(filteredPlayers, tournament);
// 6. 라운드 및 코트 배정
const brackets = this.assignRoundsAndCourts(matches, tournament);
// 7. 용병 최적화
const optimizedBrackets = this.optimizeMercenaries(brackets, completePlayers, tournament);
return optimizedBrackets;
} catch (error) {
console.error('대진표 생성 실패:', error);
throw error;
}
}
2. 선수 수 조정 알고리즘
adjustPlayerCount(players) {
const playerCount = players.length;
const remainder = playerCount % 4;
if (remainder === 0) {
console.log('선수 수가 이미 4의 배수입니다.');
return players;
}
const mercenariesNeeded = 4 - remainder;
console.log(`${mercenariesNeeded}명의 용병이 필요합니다.`);
// 성별 분석
const genderCount = this.analyzeGenderDistribution(players);
const mercenaries = this.generateMercenaries(mercenariesNeeded, genderCount);
return [...players, ...mercenaries];
}
analyzeGenderDistribution(players) {
const count = { male: 0, female: 0 };
players.forEach(player => {
if (player.gender === '남성') count.male++;
else if (player.gender === '여성') count.female++;
});
return count;
}
generateMercenaries(count, genderCount) {
const mercenaries = [];
const totalPlayers = genderCount.male + genderCount.female;
for (let i = 0; i < count; i++) {
// 성별 균형을 위한 로직
let gender;
if (genderCount.male < genderCount.female) {
gender = '남성';
genderCount.male++;
} else if (genderCount.female < genderCount.male) {
gender = '여성';
genderCount.female++;
} else {
// 동일한 경우 번갈아가며
gender = i % 2 === 0 ? '남성' : '여성';
}
mercenaries.push({
id: `mercenary_${Date.now()}_${i}`,
name: `용병${i + 1}`,
gender: gender,
skill: 'B',
grade: 'B',
birthYear: '90',
isMercenary: true
});
}
return mercenaries;
}
3. 자체대회 매칭 생성 알고리즘
generateSelfTournamentMatches(players, tournament) {
const { gamesPerPlayer, matchingType } = tournament;
const matches = [];
const playerGameCount = {};
const usedPairings = new Set();
// 초기화
players.forEach(player => {
playerGameCount[player.id] = 0;
});
console.log(`자체대회 매칭 시작: ${players.length}명, 선수당 ${gamesPerPlayer}게임`);
// 1단계: 이상적인 매치 생성
let attempts = 0;
const maxAttempts = players.length * gamesPerPlayer * 3;
while (attempts < maxAttempts) {
attempts++;
// 게임이 필요한 선수들 필터링
const needsGames = players.filter(p => playerGameCount[p.id] < gamesPerPlayer);
if (needsGames.length < 4) break;
// 최적의 4명 선택
const selectedPlayers = this.selectBestMatch(needsGames, matchingType, usedPairings);
if (selectedPlayers.length === 4) {
const match = {
team1: [selectedPlayers[0], selectedPlayers[1]],
team2: [selectedPlayers[2], selectedPlayers[3]],
type: 'self_tournament'
};
matches.push(match);
// 게임 수 업데이트
selectedPlayers.forEach(p => {
playerGameCount[p.id]++;
});
// 사용된 페어링 기록
this.recordUsedPairings(selectedPlayers, usedPairings);
}
}
// 2단계: 강제 매치 생성 (게임 수 부족한 선수들을 위해)
this.generateForceMatches(players, matches, playerGameCount, gamesPerPlayer);
return matches;
}
4. 최적 매치 선택 알고리즘
selectBestMatch(players, matchingType, usedPairings) {
const shuffled = MatchingUtils.shuffleArray(players);
let bestCombination = [];
let minPairingOverlap = Infinity;
// 모든 4명 조합을 검사
for (let i = 0; i < shuffled.length - 3; i++) {
for (let j = i + 1; j < shuffled.length - 2; j++) {
for (let k = j + 1; k < shuffled.length - 1; k++) {
for (let l = k + 1; l < shuffled.length; l++) {
const candidates = [shuffled[i], shuffled[j], shuffled[k], shuffled[l]];
const overlap = this.countPairingOverlap(candidates, usedPairings);
if (overlap < minPairingOverlap) {
minPairingOverlap = overlap;
bestCombination = candidates;
}
// 완벽한 조합을 찾으면 즉시 반환
if (minPairingOverlap === 0) {
return this.balanceMatch(bestCombination, matchingType);
}
}
}
}
}
return this.balanceMatch(bestCombination, matchingType);
}
countPairingOverlap(players, usedPairings) {
let overlap = 0;
for (let i = 0; i < players.length; i++) {
for (let j = i + 1; j < players.length; j++) {
const pairKey = this.getPairKey(players[i].id, players[j].id);
if (usedPairings.has(pairKey)) {
overlap++;
}
}
}
return overlap;
}
getPairKey(id1, id2) {
// 일관된 페어 키 생성 (순서 무관)
return id1 < id2 ? `${id1}-${id2}` : `${id2}-${id1}`;
}
5. 팀 밸런스 조정 알고리즘
// MatchingUtils.js
static balanceTeams(team1, team2) {
const maxIterations = 100;
let bestTeam1 = [...team1];
let bestTeam2 = [...team2];
let bestDifference = this.calculateBalanceDifference(team1, team2);
let currentTeam1 = [...team1];
let currentTeam2 = [...team2];
for (let i = 0; i < maxIterations; i++) {
// 각 팀에서 랜덤하게 선수 선택
const idx1 = Math.floor(Math.random() * currentTeam1.length);
const idx2 = Math.floor(Math.random() * currentTeam2.length);
// 선수 교환 시뮬레이션
const newTeam1 = [...currentTeam1];
const newTeam2 = [...currentTeam2];
[newTeam1[idx1], newTeam2[idx2]] = [newTeam2[idx2], newTeam1[idx1]];
// 새로운 밸런스 계산
const newDifference = this.calculateBalanceDifference(newTeam1, newTeam2);
// 더 좋은 밸런스라면 업데이트
if (newDifference < bestDifference) {
bestTeam1 = newTeam1;
bestTeam2 = newTeam2;
bestDifference = newDifference;
currentTeam1 = newTeam1;
currentTeam2 = newTeam2;
}
// 완벽한 밸런스를 찾으면 조기 종료
if (bestDifference === 0) {
break;
}
}
return {
team1: bestTeam1,
team2: bestTeam2,
difference: bestDifference
};
}
static calculateTeamStrength(team) {
return team.reduce((total, player) => {
if (player.isMercenary) {
return total + 2.5; // 용병은 평균 실력 (B급 = 3점의 중간값)
}
return total + this.getSkillScore(player.skill);
}, 0);
}
static getSkillScore(skill) {
const skillMap = {
'S': 5,
'A': 4,
'B': 3,
'C': 2,
'D': 1,
'초심': 0
};
return skillMap[skill] || 0;
}