Match management view

This commit is contained in:
2026-05-06 16:21:50 +02:00
parent c70b9c554e
commit 5a51a8265d
15 changed files with 2554 additions and 7 deletions
@@ -489,9 +489,111 @@ public partial class TournamentsViewModel : ViewModelBase
break;
}
// Auto-promote teams with byes (TBD opponents with no parent match)
await AutoPromoteByeTeams(tournament.Id);
await LoadMatches();
}
private async Task AutoPromoteByeTeams(int tournamentId)
{
// Find matches where a team has a TBD opponent (only 1 team in match) and no parent match
var matchesToPromote = await _context.Matches
.Where(m => m.TournamentId == tournamentId && m.Teams.Count == 1)
.Include(m => m.Teams)
.Include(m => m.WinnerMatch)
.ToListAsync();
foreach (var match in matchesToPromote)
{
// Check if this match has no incoming teams (no parent matches pointing to it)
var hasParent = await _context.Matches
.AnyAsync(m => m.WinnerMatchId == match.Id || m.LoserMatchId == match.Id);
if (!hasParent && match.WinnerMatchId.HasValue)
{
// This is a bye match - promote the team to the next match
var team = match.Teams.First();
var winnerMatch = match.WinnerMatch;
if (winnerMatch != null)
{
// Add team to the winner match (position doesn't matter)
if (!winnerMatch.Teams.Any(t => t.TeamId == team.TeamId))
{
winnerMatch.Teams.Add(new TeamParticipant
{
TeamId = team.TeamId,
Seed = team.Seed,
Score = 0,
Team = team.Team,
MatchId = winnerMatch.Id,
Round = null!
});
}
// Mark the current match's round as finished since it was a bye
if (match.Rounds.Any())
{
match.Rounds.Last().State = RoundState.Finished;
}
// Recursively check if the destination match now has all teams and is also a bye
if (winnerMatch.Teams.Count == 1)
{
var winnerHasParent = await _context.Matches
.AnyAsync(m => m.WinnerMatchId == winnerMatch.Id || m.LoserMatchId == winnerMatch.Id);
if (!winnerHasParent && winnerMatch.WinnerMatchId.HasValue)
{
// Recursively promote
await PromoteRecursive(winnerMatch, tournamentId);
}
}
}
}
}
await _context.SaveChangesAsync();
}
private async Task PromoteRecursive(Match match, int tournamentId)
{
if (match.Teams.Count != 1 || !match.WinnerMatchId.HasValue)
return;
var team = match.Teams.First();
var winnerMatch = await _context.Matches
.FindAsync(match.WinnerMatchId.Value);
if (winnerMatch != null)
{
// Add team to the winner match (position doesn't matter)
if (!winnerMatch.Teams.Any(t => t.TeamId == team.TeamId))
{
winnerMatch.Teams.Add(new TeamParticipant
{
TeamId = team.TeamId,
Seed = team.Seed,
Score = 0,
Team = team.Team,
MatchId = winnerMatch.Id,
Round = null!
});
}
if (match.Rounds.Any())
{
match.Rounds.Last().State = RoundState.Finished;
}
// Check if we need to continue promoting
if (winnerMatch.Teams.Count == 1 && winnerMatch.WinnerMatchId.HasValue)
{
await PromoteRecursive(winnerMatch, tournamentId);
}
}
}
private async Task GenerateSingleElimination(Tournament tournament, List<TeamParticipant> teams)
{
int numTeams = teams.Count;
@@ -504,6 +606,7 @@ public partial class TournamentsViewModel : ViewModelBase
int totalRounds = (int)Math.Log2(nextPowerOf2);
var allMatches = new List<Match>();
// Create all matches first
for (int round = 0; round < totalRounds; round++)
{
int matchesInRound = nextPowerOf2 / (int)Math.Pow(2, round + 1);
@@ -511,7 +614,24 @@ public partial class TournamentsViewModel : ViewModelBase
for (int i = 0; i < matchesInRound; i++)
{
var match = CreateMatch(tournament);
allMatches.Add(match);
}
}
_context.Matches.AddRange(allMatches);
await _context.SaveChangesAsync();
// Now set up bracket navigation
int matchIndex = 0;
for (int round = 0; round < totalRounds; round++)
{
int matchesInRound = nextPowerOf2 / (int)Math.Pow(2, round + 1);
for (int i = 0; i < matchesInRound; i++)
{
var match = allMatches[matchIndex];
// Set up teams for first round
if (round == 0)
{
int highSeed = i + 1;
@@ -523,11 +643,25 @@ public partial class TournamentsViewModel : ViewModelBase
AddTeamToMatch(match, teams[lowSeed - 1], lowSeed);
}
allMatches.Add(match);
// Set winner goes to next round match
if (round < totalRounds - 1)
{
int nextRoundMatches = nextPowerOf2 / (int)Math.Pow(2, round + 2);
int nextMatchIndex = 0;
for (int r = 0; r <= round; r++)
{
nextMatchIndex += nextPowerOf2 / (int)Math.Pow(2, r + 1);
}
int nextMatchInRound = i / 2;
int targetMatchIndex = nextMatchIndex + nextMatchInRound;
match.WinnerMatchId = allMatches[targetMatchIndex].Id;
}
matchIndex++;
}
}
_context.Matches.AddRange(allMatches);
await _context.SaveChangesAsync();
}
@@ -543,6 +677,7 @@ public partial class TournamentsViewModel : ViewModelBase
int wbRounds = (int)Math.Log2(nextPowerOf2);
var allMatches = new List<Match>();
// Create winners bracket matches
var wbMatchesByRound = new List<List<Match>>();
for (int r = 0; r < wbRounds; r++)
{
@@ -556,9 +691,10 @@ public partial class TournamentsViewModel : ViewModelBase
wbMatchesByRound.Add(roundMatches);
}
// Create losers bracket matches
var lbMatchesByRound = new List<List<Match>>();
if (numTeams > 2)
{
var lbMatchesByRound = new List<List<Match>>();
for (int r = 0; r < wbRounds - 1; r++)
{
int matchesInRound = nextPowerOf2 / 4;
@@ -573,11 +709,89 @@ public partial class TournamentsViewModel : ViewModelBase
var lbFinal = CreateMatch(tournament);
allMatches.Add(lbFinal);
lbMatchesByRound.Add(new List<Match> { lbFinal });
var grandFinals = CreateMatch(tournament);
allMatches.Add(grandFinals);
}
// Save all matches first to get IDs
_context.Matches.AddRange(allMatches);
await _context.SaveChangesAsync();
// Set up winners bracket navigation
int wbMatchIndex = 0;
for (int r = 0; r < wbRounds; r++)
{
int matchesInRound = nextPowerOf2 / (int)Math.Pow(2, r + 1);
for (int i = 0; i < matchesInRound; i++)
{
var match = wbMatchesByRound[r][i];
// Winner goes to next WB round
if (r < wbRounds - 1)
{
var nextMatch = wbMatchesByRound[r + 1][i / 2];
match.WinnerMatchId = nextMatch.Id;
}
else
{
// WB final winner goes to grand finals
var grandFinals = allMatches.Last();
match.WinnerMatchId = grandFinals.Id;
}
// Loser drops to losers bracket
if (r > 0 && lbMatchesByRound.Count > 0)
{
int lbRound = r - 1;
if (lbRound < lbMatchesByRound.Count - 1) // Not LB final
{
int lbMatchIndex = i / 2;
if (lbMatchIndex < lbMatchesByRound[lbRound].Count)
{
var lbMatch = lbMatchesByRound[lbRound][lbMatchIndex];
match.LoserMatchId = lbMatch.Id;
}
}
}
else if (r == 0 && lbMatchesByRound.Count > 0)
{
// First round losers go to first LB round
int lbMatchIndex = i / 2;
if (lbMatchIndex < lbMatchesByRound[0].Count)
{
var lbMatch = lbMatchesByRound[0][lbMatchIndex];
match.LoserMatchId = lbMatch.Id;
}
}
}
}
// Set up losers bracket navigation
for (int r = 0; r < lbMatchesByRound.Count; r++)
{
for (int i = 0; i < lbMatchesByRound[r].Count; i++)
{
var match = lbMatchesByRound[r][i];
if (r < lbMatchesByRound.Count - 2)
{
// Winner goes to next LB round
var nextMatch = lbMatchesByRound[r + 1][i / 2];
match.WinnerMatchId = nextMatch.Id;
}
else if (r == lbMatchesByRound.Count - 2)
{
// LB final winner goes to grand finals
var grandFinals = allMatches.Last();
match.WinnerMatchId = grandFinals.Id;
}
// LB losers are eliminated - no LoserMatchId
}
}
// Set up teams for first round
var firstRoundMatches = wbMatchesByRound[0];
for (int i = 0; i < nextPowerOf2 / 2; i++)
{
@@ -591,7 +805,6 @@ public partial class TournamentsViewModel : ViewModelBase
AddTeamToMatch(match, teams[lowSeed - 1], lowSeed);
}
_context.Matches.AddRange(allMatches);
await _context.SaveChangesAsync();
}