Tournament management

This commit is contained in:
2026-05-06 13:28:10 +02:00
parent 67d27ab21c
commit c70b9c554e
20 changed files with 1667 additions and 36 deletions
@@ -50,6 +50,15 @@ public partial class MainViewModel : ViewModelBase
Title = "Events Management";
await eventsVm.LoadEvents();
}
[RelayCommand]
private async Task NavigateToTournaments()
{
var tournamentsVm = new TournamentsViewModel();
CurrentView = tournamentsVm;
Title = "Tournament Management";
await tournamentsVm.LoadTournaments();
}
}
public partial class HomeViewModel : ViewModelBase
@@ -137,14 +137,14 @@ public partial class TeamsViewModel : ViewModelBase
}
UpdateFilteredEvents();
var games = await _context.Games.ToListAsync();
var tournaments = await _context.Tournaments
.Include(t => t.Game)
.Include(t => t.Event)
.ToListAsync();
AvailableTournaments.Clear();
foreach (var g in games)
foreach (var t in tournaments)
{
foreach (var e in events)
{
AvailableTournaments.Add($"{g.Name} @ {e.Name}");
}
AvailableTournaments.Add(GetTournamentDisplayName(t));
}
UpdateFilteredTournaments();
@@ -0,0 +1,807 @@
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;
}
await LoadMatches();
}
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>();
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);
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);
}
allMatches.Add(match);
}
}
_context.Matches.AddRange(allMatches);
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>();
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);
}
if (numTeams > 2)
{
var lbMatchesByRound = new List<List<Match>>();
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);
var grandFinals = CreateMatch(tournament);
allMatches.Add(grandFinals);
}
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);
}
_context.Matches.AddRange(allMatches);
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;
}
}