1021 lines
31 KiB
C#
1021 lines
31 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using TournamentOrganizer.Models;
|
|
|
|
namespace TournamentOrganizer.ViewModels;
|
|
|
|
public partial class TournamentsViewModel : ViewModelBase
|
|
{
|
|
private readonly TournamentContext _context;
|
|
|
|
[ObservableProperty]
|
|
private ObservableCollection<EventOption> _availableEvents = [];
|
|
|
|
[ObservableProperty]
|
|
private EventOption? _selectedEvent;
|
|
|
|
[ObservableProperty]
|
|
private ObservableCollection<TournamentOption> _availableTournaments = [];
|
|
|
|
[ObservableProperty]
|
|
private TournamentOption? _selectedTournament;
|
|
|
|
[ObservableProperty]
|
|
private DateTime _tournamentStart;
|
|
|
|
[ObservableProperty]
|
|
private DateTime _tournamentEnd;
|
|
|
|
[ObservableProperty]
|
|
private RuleSetOption? _selectedS1RuleSet;
|
|
|
|
[ObservableProperty]
|
|
private int? _s1Groups;
|
|
|
|
[ObservableProperty]
|
|
private int? _s1GroupAdvances;
|
|
|
|
[ObservableProperty]
|
|
private RuleSetOption? _selectedS2RuleSet;
|
|
|
|
[ObservableProperty]
|
|
private ObservableCollection<TeamOption> _availableTeams = [];
|
|
|
|
[ObservableProperty]
|
|
private ObservableCollection<ParticipatingTeam> _participatingTeams = [];
|
|
|
|
[ObservableProperty]
|
|
private TeamOption? _selectedTeamToAdd;
|
|
|
|
[ObservableProperty]
|
|
private ParticipatingTeam? _selectedParticipatingTeam;
|
|
|
|
[ObservableProperty]
|
|
private ObservableCollection<MatchDisplay> _matches = [];
|
|
|
|
[ObservableProperty]
|
|
private MatchDisplay? _selectedMatch;
|
|
|
|
[ObservableProperty]
|
|
private string _eventFilterSearch = string.Empty;
|
|
|
|
[ObservableProperty]
|
|
private string _tournamentFilterSearch = string.Empty;
|
|
|
|
[ObservableProperty]
|
|
private bool _isEventDropdownOpen;
|
|
|
|
[ObservableProperty]
|
|
private bool _isTournamentDropdownOpen;
|
|
|
|
[ObservableProperty]
|
|
private bool _isEditing;
|
|
|
|
[ObservableProperty]
|
|
private string _statusMessage = string.Empty;
|
|
|
|
private readonly ObservableCollection<EventOption> _filteredEvents = [];
|
|
public ObservableCollection<EventOption> FilteredEvents => _filteredEvents;
|
|
|
|
private readonly ObservableCollection<TournamentOption> _filteredTournaments = [];
|
|
public ObservableCollection<TournamentOption> FilteredTournaments => _filteredTournaments;
|
|
|
|
public ObservableCollection<RuleSetOption> S1RuleSetOptions { get; } = [];
|
|
public ObservableCollection<RuleSetOption> S2RuleSetOptions { get; } = [new RuleSetOption(null, "(None)")];
|
|
|
|
public bool IsTournamentFilterEnabled => SelectedEvent != null;
|
|
|
|
public TournamentsViewModel()
|
|
{
|
|
_context = new TournamentContext();
|
|
foreach (var rs in Enum.GetValues<RuleSet>())
|
|
{
|
|
S1RuleSetOptions.Add(new RuleSetOption(rs, rs.ToDisplayName()));
|
|
S2RuleSetOptions.Add(new RuleSetOption(rs, rs.ToDisplayName()));
|
|
}
|
|
}
|
|
|
|
partial void OnEventFilterSearchChanged(string value) => UpdateFilteredEvents();
|
|
partial void OnSelectedEventChanged(EventOption? value)
|
|
{
|
|
OnPropertyChanged(nameof(IsTournamentFilterEnabled));
|
|
UpdateAvailableTournaments();
|
|
UpdateFilteredTournaments();
|
|
SelectedTournament = null;
|
|
Matches.Clear();
|
|
ParticipatingTeams.Clear();
|
|
IsEditing = false;
|
|
}
|
|
|
|
partial void OnSelectedTournamentChanged(TournamentOption? value)
|
|
{
|
|
if (value == null)
|
|
{
|
|
IsEditing = false;
|
|
Matches.Clear();
|
|
ParticipatingTeams.Clear();
|
|
return;
|
|
}
|
|
|
|
TournamentStart = value.Tournament.Start;
|
|
TournamentEnd = value.Tournament.End;
|
|
SelectedS1RuleSet = S1RuleSetOptions.FirstOrDefault(o => o.Value == value.Tournament.S1RuleSet);
|
|
S1Groups = value.Tournament.S1Groups;
|
|
S1GroupAdvances = value.Tournament.S1GroupAdvances;
|
|
SelectedS2RuleSet = S2RuleSetOptions.FirstOrDefault(o => o.Value == value.Tournament.S2RuleSet);
|
|
IsEditing = true;
|
|
|
|
LoadTournamentDetails();
|
|
_ = LoadMatches();
|
|
StatusMessage = $"Editing tournament: {value.Tournament.Game.Name}";
|
|
}
|
|
|
|
partial void OnSelectedS1RuleSetChanged(RuleSetOption? value)
|
|
{
|
|
_ = RegenerateMatches();
|
|
}
|
|
|
|
partial void OnSelectedS2RuleSetChanged(RuleSetOption? value)
|
|
{
|
|
if (value?.Value == null)
|
|
{
|
|
S1Groups = null;
|
|
S1GroupAdvances = null;
|
|
}
|
|
_ = RegenerateMatches();
|
|
}
|
|
|
|
partial void OnTournamentFilterSearchChanged(string value) => UpdateFilteredTournaments();
|
|
|
|
private void UpdateFilteredEvents()
|
|
{
|
|
_filteredEvents.Clear();
|
|
foreach (var e in AvailableEvents.Where(e => string.IsNullOrWhiteSpace(EventFilterSearch) || e.DisplayName.ToLower().Contains(EventFilterSearch.ToLower())))
|
|
{
|
|
_filteredEvents.Add(e);
|
|
}
|
|
}
|
|
|
|
private void UpdateFilteredTournaments()
|
|
{
|
|
_filteredTournaments.Clear();
|
|
foreach (var t in AvailableTournaments.Where(t => string.IsNullOrWhiteSpace(TournamentFilterSearch) || t.DisplayName.ToLower().Contains(TournamentFilterSearch.ToLower())))
|
|
{
|
|
_filteredTournaments.Add(t);
|
|
}
|
|
}
|
|
|
|
public void SelectEventFilter(EventOption option)
|
|
{
|
|
SelectedEvent = option;
|
|
IsEventDropdownOpen = false;
|
|
EventFilterSearch = string.Empty;
|
|
}
|
|
|
|
public void ClearEventFilter()
|
|
{
|
|
SelectedEvent = null;
|
|
EventFilterSearch = string.Empty;
|
|
}
|
|
|
|
public void SelectTournamentFilter(TournamentOption option)
|
|
{
|
|
SelectedTournament = option;
|
|
IsTournamentDropdownOpen = false;
|
|
TournamentFilterSearch = string.Empty;
|
|
}
|
|
|
|
public void ClearTournamentFilter()
|
|
{
|
|
SelectedTournament = null;
|
|
TournamentFilterSearch = string.Empty;
|
|
}
|
|
|
|
public async Task LoadTournaments()
|
|
{
|
|
var events = await _context.Events
|
|
.Include(e => e.Tournaments)
|
|
.ThenInclude(t => t.Game)
|
|
.ToListAsync();
|
|
|
|
AvailableEvents.Clear();
|
|
_filteredEvents.Clear();
|
|
foreach (var e in events)
|
|
{
|
|
var option = new EventOption(e);
|
|
AvailableEvents.Add(option);
|
|
_filteredEvents.Add(option);
|
|
}
|
|
|
|
UpdateAvailableTournaments();
|
|
}
|
|
|
|
private void UpdateAvailableTournaments()
|
|
{
|
|
AvailableTournaments.Clear();
|
|
if (SelectedEvent == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var t in SelectedEvent.Event.Tournaments)
|
|
{
|
|
AvailableTournaments.Add(new TournamentOption(t));
|
|
}
|
|
}
|
|
|
|
private async Task LoadTournamentDetails()
|
|
{
|
|
if (SelectedTournament == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var tournamentTeams = await _context.TournamentTeams
|
|
.Include(tt => tt.Team)
|
|
.ThenInclude(t => t.Players)
|
|
.Where(tt => tt.TournamentId == SelectedTournament.Tournament.Id)
|
|
.ToListAsync();
|
|
|
|
ParticipatingTeams.Clear();
|
|
foreach (var tt in tournamentTeams)
|
|
{
|
|
ParticipatingTeams.Add(new ParticipatingTeam(tt));
|
|
}
|
|
|
|
SelectedParticipatingTeam = null;
|
|
|
|
var allTeams = await _context.Teams
|
|
.Include(t => t.Players)
|
|
.ToListAsync();
|
|
|
|
var participatingIds = ParticipatingTeams.Select(pt => pt.TeamId).ToHashSet();
|
|
AvailableTeams.Clear();
|
|
foreach (var team in allTeams)
|
|
{
|
|
if (!participatingIds.Contains(team.Id))
|
|
{
|
|
AvailableTeams.Add(new TeamOption(team));
|
|
}
|
|
}
|
|
|
|
SelectedTeamToAdd = null;
|
|
}
|
|
|
|
private async Task LoadMatches()
|
|
{
|
|
if (SelectedTournament == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var matches = await _context.Matches
|
|
.Where(m => m.TournamentId == SelectedTournament.Tournament.Id)
|
|
.Include(m => m.Teams)
|
|
.ThenInclude(tp => tp.Team)
|
|
.Include(m => m.Rounds)
|
|
.ThenInclude(r => r.Players)
|
|
.ThenInclude(pp => pp.Player)
|
|
.ToListAsync();
|
|
|
|
Matches.Clear();
|
|
foreach (var match in matches)
|
|
{
|
|
Matches.Add(new MatchDisplay(match));
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task SaveTournament()
|
|
{
|
|
if (SelectedTournament == null)
|
|
{
|
|
StatusMessage = "Select a tournament to edit";
|
|
return;
|
|
}
|
|
|
|
var tournament = await _context.Tournaments.FirstOrDefaultAsync(t => t.Id == SelectedTournament.Tournament.Id);
|
|
if (tournament == null)
|
|
{
|
|
StatusMessage = "Tournament not found";
|
|
return;
|
|
}
|
|
|
|
tournament.Start = TournamentStart;
|
|
tournament.End = TournamentEnd;
|
|
tournament.S1RuleSet = SelectedS1RuleSet?.Value ?? tournament.S1RuleSet;
|
|
tournament.S1Groups = S1Groups;
|
|
tournament.S1GroupAdvances = S1GroupAdvances;
|
|
tournament.S2RuleSet = SelectedS2RuleSet?.Value;
|
|
|
|
await _context.SaveChangesAsync();
|
|
StatusMessage = "Tournament updated";
|
|
await LoadTournaments();
|
|
|
|
SelectedTournament = AvailableTournaments.FirstOrDefault(t => t.Tournament.Id == tournament.Id);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task AddTeamToTournament()
|
|
{
|
|
if (SelectedTeamToAdd == null)
|
|
{
|
|
StatusMessage = "Select a team to add";
|
|
return;
|
|
}
|
|
|
|
if (SelectedTournament == null)
|
|
{
|
|
StatusMessage = "Select a tournament first";
|
|
return;
|
|
}
|
|
|
|
var tournamentTeams = await _context.TournamentTeams
|
|
.Where(tt => tt.TournamentId == SelectedTournament.Tournament.Id)
|
|
.ToListAsync();
|
|
|
|
if (tournamentTeams.Any(tt => tt.TeamId == SelectedTeamToAdd.Team.Id))
|
|
{
|
|
StatusMessage = "Team is already in this tournament";
|
|
return;
|
|
}
|
|
|
|
var seed = tournamentTeams.Count + 1;
|
|
var tournamentTeam = new TournamentTeam
|
|
{
|
|
TournamentId = SelectedTournament.Tournament.Id,
|
|
TeamId = SelectedTeamToAdd.Team.Id,
|
|
Seed = seed,
|
|
Tournament = SelectedTournament.Tournament,
|
|
Team = SelectedTeamToAdd.Team
|
|
};
|
|
|
|
_context.TournamentTeams.Add(tournamentTeam);
|
|
await _context.SaveChangesAsync();
|
|
StatusMessage = $"Added '{SelectedTeamToAdd.Team.Name}' to tournament";
|
|
|
|
await LoadTournamentDetails();
|
|
await RegenerateMatches();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task RemoveTeamFromTournament()
|
|
{
|
|
if (SelectedParticipatingTeam == null)
|
|
{
|
|
StatusMessage = "Select a team to remove";
|
|
return;
|
|
}
|
|
|
|
if (SelectedTournament == null)
|
|
{
|
|
StatusMessage = "Select a tournament first";
|
|
return;
|
|
}
|
|
|
|
var tournamentTeam = await _context.TournamentTeams
|
|
.FirstOrDefaultAsync(tt => tt.TournamentId == SelectedTournament.Tournament.Id && tt.TeamId == SelectedParticipatingTeam.TeamId);
|
|
|
|
if (tournamentTeam != null)
|
|
{
|
|
_context.TournamentTeams.Remove(tournamentTeam);
|
|
await _context.SaveChangesAsync();
|
|
StatusMessage = $"Removed '{SelectedParticipatingTeam.Name}' from tournament";
|
|
}
|
|
|
|
await LoadTournamentDetails();
|
|
await RegenerateMatches();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task ReseedTeams()
|
|
{
|
|
if (SelectedTournament == null)
|
|
{
|
|
StatusMessage = "Select a tournament first";
|
|
return;
|
|
}
|
|
|
|
var tournamentTeams = await _context.TournamentTeams
|
|
.Where(tt => tt.TournamentId == SelectedTournament.Tournament.Id)
|
|
.ToListAsync();
|
|
|
|
if (tournamentTeams.Count < 2)
|
|
{
|
|
StatusMessage = "Need at least 2 teams to reseed";
|
|
return;
|
|
}
|
|
|
|
var rng = new Random();
|
|
var shuffled = tournamentTeams.OrderBy(_ => rng.Next()).ToList();
|
|
|
|
for (int i = 0; i < shuffled.Count; i++)
|
|
{
|
|
shuffled[i].Seed = i + 1;
|
|
}
|
|
|
|
await _context.SaveChangesAsync();
|
|
StatusMessage = "Teams reseeded";
|
|
await LoadTournamentDetails();
|
|
await RegenerateMatches();
|
|
}
|
|
|
|
private async Task RegenerateMatches()
|
|
{
|
|
if (SelectedTournament == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var tournamentTeams = await _context.TournamentTeams
|
|
.Include(tt => tt.Team)
|
|
.Where(tt => tt.TournamentId == SelectedTournament.Tournament.Id)
|
|
.OrderBy(tt => tt.Seed)
|
|
.ToListAsync();
|
|
|
|
var existingMatches = await _context.Matches
|
|
.Where(m => m.TournamentId == SelectedTournament.Tournament.Id)
|
|
.ToListAsync();
|
|
|
|
if (existingMatches.Count > 0)
|
|
{
|
|
_context.Matches.RemoveRange(existingMatches);
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
|
|
if (tournamentTeams.Count < 2)
|
|
{
|
|
Matches.Clear();
|
|
return;
|
|
}
|
|
|
|
var tournament = await _context.Tournaments.FindAsync(SelectedTournament.Tournament.Id);
|
|
if (tournament == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var participants = tournamentTeams.Select(tt => new TeamParticipant
|
|
{
|
|
TeamId = tt.TeamId,
|
|
Team = tt.Team,
|
|
Seed = tt.Seed,
|
|
Score = 0,
|
|
MatchId = 0,
|
|
Round = null!
|
|
}).ToList();
|
|
|
|
var ruleSet = tournament.S2RuleSet ?? tournament.S1RuleSet;
|
|
|
|
switch (ruleSet)
|
|
{
|
|
case RuleSet.SingleElimination:
|
|
await GenerateSingleElimination(tournament, participants);
|
|
break;
|
|
case RuleSet.DoubleElimination:
|
|
await GenerateDoubleElimination(tournament, participants);
|
|
break;
|
|
case RuleSet.RoundRobin:
|
|
await GenerateRoundRobin(tournament, participants);
|
|
break;
|
|
case RuleSet.Swiss:
|
|
await GenerateSwiss(tournament, participants);
|
|
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;
|
|
int nextPowerOf2 = 1;
|
|
while (nextPowerOf2 < numTeams)
|
|
{
|
|
nextPowerOf2 *= 2;
|
|
}
|
|
|
|
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);
|
|
|
|
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;
|
|
int lowSeed = nextPowerOf2 - i;
|
|
|
|
if (highSeed <= numTeams)
|
|
AddTeamToMatch(match, teams[highSeed - 1], highSeed);
|
|
if (lowSeed <= numTeams)
|
|
AddTeamToMatch(match, teams[lowSeed - 1], lowSeed);
|
|
}
|
|
|
|
// 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++;
|
|
}
|
|
}
|
|
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
|
|
private async Task GenerateDoubleElimination(Tournament tournament, List<TeamParticipant> teams)
|
|
{
|
|
int numTeams = teams.Count;
|
|
int nextPowerOf2 = 1;
|
|
while (nextPowerOf2 < numTeams)
|
|
{
|
|
nextPowerOf2 *= 2;
|
|
}
|
|
|
|
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++)
|
|
{
|
|
int matchesInRound = nextPowerOf2 / (int)Math.Pow(2, r + 1);
|
|
var roundMatches = new List<Match>();
|
|
for (int i = 0; i < matchesInRound; i++)
|
|
{
|
|
roundMatches.Add(CreateMatch(tournament));
|
|
}
|
|
allMatches.AddRange(roundMatches);
|
|
wbMatchesByRound.Add(roundMatches);
|
|
}
|
|
|
|
// Create losers bracket matches
|
|
var lbMatchesByRound = new List<List<Match>>();
|
|
if (numTeams > 2)
|
|
{
|
|
for (int r = 0; r < wbRounds - 1; r++)
|
|
{
|
|
int matchesInRound = nextPowerOf2 / 4;
|
|
var roundMatches = new List<Match>();
|
|
for (int i = 0; i < matchesInRound; i++)
|
|
{
|
|
roundMatches.Add(CreateMatch(tournament));
|
|
}
|
|
allMatches.AddRange(roundMatches);
|
|
lbMatchesByRound.Add(roundMatches);
|
|
}
|
|
|
|
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++)
|
|
{
|
|
int highSeed = i + 1;
|
|
int lowSeed = nextPowerOf2 - i;
|
|
var match = firstRoundMatches[i];
|
|
|
|
if (highSeed <= numTeams)
|
|
AddTeamToMatch(match, teams[highSeed - 1], highSeed);
|
|
if (lowSeed <= numTeams)
|
|
AddTeamToMatch(match, teams[lowSeed - 1], lowSeed);
|
|
}
|
|
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
|
|
private static Match CreateMatch(Tournament tournament)
|
|
{
|
|
var match = new Match
|
|
{
|
|
TournamentId = tournament.Id,
|
|
Tournament = tournament,
|
|
Teams = [],
|
|
Rounds = []
|
|
};
|
|
var r = new Round
|
|
{
|
|
MatchId = match.Id,
|
|
Match = match,
|
|
Players = []
|
|
};
|
|
match.Rounds.Add(r);
|
|
return match;
|
|
}
|
|
|
|
private static void AddTeamToMatch(Match match, TeamParticipant team, int seed)
|
|
{
|
|
match.Teams.Add(new TeamParticipant
|
|
{
|
|
TeamId = team.TeamId,
|
|
Seed = seed,
|
|
Score = 0,
|
|
Team = team.Team,
|
|
MatchId = 0,
|
|
Round = match
|
|
});
|
|
}
|
|
|
|
private async Task GenerateRoundRobin(Tournament tournament, List<TeamParticipant> teams)
|
|
{
|
|
var matches = new List<Match>();
|
|
|
|
for (int i = 0; i < teams.Count; i++)
|
|
{
|
|
for (int j = i + 1; j < teams.Count; j++)
|
|
{
|
|
var match = new Match
|
|
{
|
|
TournamentId = tournament.Id,
|
|
Tournament = tournament,
|
|
Teams =
|
|
[
|
|
new TeamParticipant { TeamId = teams[i].TeamId, Seed = teams[i].Seed, Score = 0, Team = teams[i].Team, MatchId = 0, Round = null! },
|
|
new TeamParticipant { TeamId = teams[j].TeamId, Seed = teams[j].Seed, Score = 0, Team = teams[j].Team, MatchId = 0, Round = null! }
|
|
],
|
|
Rounds = []
|
|
};
|
|
|
|
var round = new Round
|
|
{
|
|
MatchId = match.Id,
|
|
Match = match,
|
|
Players = []
|
|
};
|
|
match.Rounds.Add(round);
|
|
|
|
matches.Add(match);
|
|
}
|
|
}
|
|
|
|
_context.Matches.AddRange(matches);
|
|
await _context.SaveChangesAsync();
|
|
|
|
foreach (var match in matches)
|
|
{
|
|
foreach (var tp in match.Teams)
|
|
{
|
|
tp.Round = match;
|
|
}
|
|
}
|
|
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
|
|
private async Task GenerateSwiss(Tournament tournament, List<TeamParticipant> teams)
|
|
{
|
|
var matches = new List<Match>();
|
|
|
|
int numRounds = (int)Math.Ceiling(Math.Log2(teams.Count));
|
|
var shuffledTeams = teams.OrderBy(_ => Guid.NewGuid()).ToList();
|
|
|
|
for (int round = 0; round < numRounds; round++)
|
|
{
|
|
for (int i = 0; i < shuffledTeams.Count / 2; i++)
|
|
{
|
|
var team1 = shuffledTeams[i * 2];
|
|
var team2 = shuffledTeams[i * 2 + 1];
|
|
|
|
var match = new Match
|
|
{
|
|
TournamentId = tournament.Id,
|
|
Tournament = tournament,
|
|
Teams =
|
|
[
|
|
new TeamParticipant { TeamId = team1.TeamId, Seed = team1.Seed, Score = 0, Team = team1.Team, MatchId = 0, Round = null! },
|
|
new TeamParticipant { TeamId = team2.TeamId, Seed = team2.Seed, Score = 0, Team = team2.Team, MatchId = 0, Round = null! }
|
|
],
|
|
Rounds = []
|
|
};
|
|
|
|
var r = new Round
|
|
{
|
|
MatchId = match.Id,
|
|
Match = match,
|
|
Players = []
|
|
};
|
|
match.Rounds.Add(r);
|
|
|
|
matches.Add(match);
|
|
}
|
|
}
|
|
|
|
_context.Matches.AddRange(matches);
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
public class EventOption
|
|
{
|
|
public Event Event { get; set; } = null!;
|
|
public string DisplayName { get; set; } = string.Empty;
|
|
|
|
public EventOption() { }
|
|
|
|
public EventOption(Event evt)
|
|
{
|
|
Event = evt;
|
|
DisplayName = evt.Name;
|
|
}
|
|
|
|
public override string ToString() => DisplayName;
|
|
}
|
|
|
|
public class TournamentOption
|
|
{
|
|
public Tournament Tournament { get; set; } = null!;
|
|
public string DisplayName { get; set; } = string.Empty;
|
|
|
|
public TournamentOption() { }
|
|
|
|
public TournamentOption(Tournament tournament)
|
|
{
|
|
Tournament = tournament;
|
|
DisplayName = $"{tournament.Game.Name}";
|
|
}
|
|
|
|
public override string ToString() => DisplayName;
|
|
}
|
|
|
|
public class TeamOption
|
|
{
|
|
public Team Team { get; set; } = null!;
|
|
public string DisplayName { get; set; } = string.Empty;
|
|
|
|
public TeamOption() { }
|
|
|
|
public TeamOption(Team team)
|
|
{
|
|
Team = team;
|
|
DisplayName = team.Name;
|
|
}
|
|
|
|
public override string ToString() => DisplayName;
|
|
}
|
|
|
|
public class ParticipatingTeam
|
|
{
|
|
public int TeamId { get; set; }
|
|
public string Name { get; set; } = string.Empty;
|
|
public int Seed { get; set; }
|
|
public int Score { get; set; }
|
|
|
|
public ParticipatingTeam() { }
|
|
|
|
public ParticipatingTeam(TournamentTeam tt)
|
|
{
|
|
TeamId = tt.TeamId;
|
|
Name = tt.Team.Name;
|
|
Seed = tt.Seed;
|
|
Score = 0;
|
|
}
|
|
}
|
|
|
|
public class MatchDisplay
|
|
{
|
|
public int Id { get; set; }
|
|
public string TeamsText { get; set; } = string.Empty;
|
|
public RoundState State { get; set; }
|
|
|
|
public MatchDisplay() { }
|
|
|
|
public MatchDisplay(Match match)
|
|
{
|
|
Id = match.Id;
|
|
if (match.Teams != null && match.Teams.Count >= 2)
|
|
{
|
|
TeamsText = $"{match.Teams[0].Team.Name} vs {match.Teams[1].Team.Name}";
|
|
}
|
|
else if (match.Teams != null && match.Teams.Count == 1)
|
|
{
|
|
TeamsText = $"{match.Teams[0].Team.Name} vs TBD";
|
|
}
|
|
|
|
State = match.Rounds?.LastOrDefault()?.State ?? RoundState.Waiting;
|
|
}
|
|
}
|