public int GetTotalRounds(Tournament tournament) { if(tournament == null) throw new ArgumentNullException("tournament"); return RoundManager.GetMaxRoundsForTournament(tournament); }
public int? GetCurrentRoundNumber(Tournament tournament) { if(tournament == null) throw new ArgumentNullException("tournament"); var roundStatus = Enumerable .Range(1, GetTotalRounds(tournament)) .Select(roundNumber => new { Number = roundNumber, State = RoundManager.GetRoundState(tournament, roundNumber) }); if(!roundStatus.Any()) return null; var currentRound = roundStatus .SkipWhile(o => o.State >= RoundState.Final) .FirstOrDefault(); if(currentRound == null || currentRound.State == RoundState.Invalid) return null; return currentRound.Number; }
public StandingsResponse Create(Tournament tournament, int? roundNumber) { if(tournament == null) throw new ArgumentNullException("tournament"); var playerStandings = tournament .Players .Select(player => new { name = player.Name, matchPoints = StatsProvider.GetMatchPoints(tournament, player, roundNumber), matchWinPercentage = StatsProvider.GetMatchWinPercentage(tournament, player, roundNumber), opponentsMatchWinPercentage = StatsProvider.GetOpponentsMatchWinPercentage(tournament, player, roundNumber), gamePoints = StatsProvider.GetGamePoints(tournament, player, roundNumber), gameWinPercentage = StatsProvider.GetGameWinPercentage(tournament, player, roundNumber), opponentsGameWinPercentage = StatsProvider.GetOpponentsGameWinPercentage(tournament, player, roundNumber), }) .OrderByDescending(o => o.matchPoints) .ThenByDescending(o => o.opponentsMatchWinPercentage) .ThenByDescending(o => o.gameWinPercentage) .ThenByDescending(o => o.opponentsGameWinPercentage) .Select((o, rank) => new PlayerStandingResponse( rank: rank + 1, playerName: o.name, matchPoints: o.matchPoints, matchWinPercentage: o.matchWinPercentage, opponentsMatchWinPercentage: o.opponentsMatchWinPercentage, gamePoints: o.gamePoints, gameWinPercentage: o.gameWinPercentage, opponentsGameWinPercentage: o.opponentsGameWinPercentage )) .ToArray(); return new StandingsResponse(playerStandings); }
public decimal GetOpponentsGameWinPercentage(Tournament tournament, Player player, int? roundNumber = null) { var opponents = tournament.GetPlayerOpponents(player, roundNumber); return opponents .Select(opponent => GetGameWinPercentage(tournament, opponent, roundNumber)) .DefaultIfEmpty(0) .Average(); }
public int GetGamePoints(Tournament tournament, Player player, int? roundNumber = null) { return tournament .GetPlayerGames(player, roundNumber) .Select(game => new PlayerStats( wins: game.Winner == player ? 1 : 0, losses: game.Winner != player && game.Winner != null ? 1 : 0, draws: game.Winner == null ? 1 : 0 )) .Aggregate(0, (sum, stats) => sum + (stats.Wins * GameWin + stats.Draws * GameDraw + stats.Losses * GameLoss)); }
public decimal GetMatchWinPercentage(Tournament tournament, Player player, int? roundNumber = null) { var achieved = GetMatchPoints(tournament, player, roundNumber); var maximum = tournament .GetPlayerMatches(player, roundNumber) .Count() * MatchWin; if(maximum == 0) return 0.33m; var rawPercentage = achieved / (decimal)maximum; // Match win percentage is capped at 0.33 on the low end return Math.Max(0.33m, rawPercentage); }
public int GetMatchPoints(Tournament tournament, Player player, int? roundNumber = null) { return tournament .GetPlayerMatches(player, roundNumber) .Select(match => new PlayerStats( wins: match.Games.Where(game => game.Winner == player).Count(), losses: match.Games.Where(game => game.Winner != player && game.Winner != null).Count(), draws: match.Games.Where(game => game.Winner == player).Count() )) .Select(stats => stats.Wins > stats.Losses ? MatchWin : stats.Wins == stats.Losses ? MatchDraw : MatchLoss) .Sum(); }
public TournamentResponse Create(Tournament tournament) { if(tournament == null) throw new ArgumentNullException("tournament"); var tournamentState = TournamentManager.GetTournamentState(tournament); return new TournamentResponse( key: tournament.Key, name: tournament.Name, started: tournamentState >= TournamentState.Started, finished: tournamentState >= TournamentState.Finished, totalRounds: TournamentManager.GetTotalRounds(tournament), activeRoundNumber: tournament.ActiveRoundNumber ); }
public TournamentState GetTournamentState(Tournament tournament) { if(tournament == null) throw new ArgumentNullException("tournament"); var firstRoundState = RoundManager.GetRoundState(tournament, 1); if(firstRoundState < RoundState.Committed) return TournamentState.None; var lastRound = RoundManager.GetMaxRoundsForTournament(tournament); var lastRoundState = RoundManager.GetRoundState(tournament, lastRound); if(lastRoundState < RoundState.Completed) return TournamentState.Started; return TournamentState.Finished; }
public RoundResponse Create(Tournament tournament, Round round) { if(round == null) throw new ArgumentNullException("round"); var roundState = RoundManager.GetRoundState(tournament, round.Number); return new RoundResponse( number: round.Number, started: roundState >= RoundState.Committed, completed: roundState >= RoundState.Completed, final: roundState >= RoundState.Final, matches: round .Matches .OrderBy(match => match.Number) .Select(match => MatchResponseProvider.Create(match)) .ToArray() ); }
public ICollection<Match> CreateMatches(Tournament tournament, int roundNumber) { var seed = tournament.Seed ^ roundNumber; var rng = new Random(seed); var tournamentRounds = tournament .Rounds .SelectMany(round => round.Matches) .ToArray(); // Group players by match points. // Randomize within each group. var playerPool = tournament .Players .Where(player => !player.Dropped) .Select(player => new { Player = player, Randomizer = rng.Next(), MatchPoints = StatsProvider.GetMatchPoints(tournament, player), HadBye = tournamentRounds .Where(match => match.Players.Contains(player)) .Where(match => match.Players.Count == 1) .Any(), Opponents = tournamentRounds .Where(match => match.Players.Contains(player)) .SelectMany(match => match.Players.Except(new[] { player })) .ToArray() }) .OrderBy(o => o.MatchPoints) .ThenBy(o => o.Randomizer) .ToArray() .AsEnumerable(); var matches = Enumerable.Empty<Match>(); // If there is an odd number of players, from the bottom up, take the first player // that hasn't had a bye and remove them from the pool. if(playerPool.Count() % 2 == 1) { var byePlayer = playerPool .Where(o => !o.HadBye) .Take(1); matches = matches .Concat(byePlayer .Select(o => new Match { Number = 1 + (playerPool.Count() / 2), // A bye gives two game wins Games = new[] { new Game { Winner = o.Player, }, new Game { Winner = o.Player, }, }, Players = new[] { o.Player, }, }) .ToArray() ); playerPool = playerPool.Except(byePlayer); } // Start at the top and take the next player in order. If they've never had a match, // place both in a pairing and remove them from the pool. If they have had a match, // take the next lowest player. Repeat until an unplayed opponent is found. var matchNumber = 1; while(playerPool.Any()) { var left = playerPool .First(); var right = playerPool .Skip(1) .SkipWhile(o => left .Opponents .Contains(o.Player) ) .FirstOrDefault() ?? playerPool .Skip(1) .First(); matches = matches .Concat(new[] { new Match { Number = matchNumber, Games = new List<Game>(), Players = new[] { left.Player, right.Player, } .OrderBy(player => player.Name) .ToArray(), } } ); playerPool = playerPool .Except(new[] { left, right }); matchNumber++; } return matches.ToArray(); }
// Rounds have five states: projected, committed, and completed, finalized, and invalid // - A round is finalized when it's been completed and a result entered for the next round // - A round is completed when all matches have the minimum number of results // - A round is committed when the first result is submitted. A result can't be submitted until the previous round is completed. // - A round is projected when the previous round is completed by no results have been submitted. // - A round is invalid if it's more than one greater than the last completed round number // When a round becomes committed, it's written to the database and can't be changed. All matches must have a result submitted to move forward. public RoundState GetRoundState(Tournament tournament, int roundNumber) { if(roundNumber < 0) return RoundState.Invalid; if(roundNumber == 0) return RoundState.Completed; if(tournament.Players.Count < 2) return RoundState.Invalid; var previousRoundState = GetRoundState(tournament, roundNumber - 1); if(previousRoundState < RoundState.Completed) return RoundState.Invalid; var requestedRound = tournament .Rounds .Where(round => round.Number == roundNumber) .FirstOrDefault(); if(requestedRound == null) return RoundState.Projected; // A round is completed when all matches have at least two wins or are called because of time. // Since we don't track called games, we just check for at least one completed game in each match. var requestedRoundIsCompleted = requestedRound .Matches .All(match => match .Games .Count() >= 1 ); var nextRoundHasResults = tournament .Rounds .Where(round => round.Number == roundNumber + 1) .Where(round => round.Matches.Any()) .Any(); if(requestedRoundIsCompleted) if(nextRoundHasResults) return RoundState.Final; else return RoundState.Completed; return RoundState.Committed; }
public Round GetRound(Tournament tournament, int roundNumber) { if(tournament == null) throw new ArgumentNullException("tournament"); if(roundNumber > GetMaxRoundsForTournament(tournament)) return null; var roundState = GetRoundState(tournament, roundNumber); if(roundState == RoundState.Invalid) return null; if(roundState == RoundState.Projected) return new Round { Number = roundNumber, Matches = CreateMatches(tournament, roundNumber), }; else return tournament .Rounds .Where(r => r.Number == roundNumber) .FirstOrDefault(); }
public int GetMaxRoundsForTournament(Tournament tournament) { var playerCount = tournament.Players.Count(); var roundCount = Math.Ceiling(Math.Log(playerCount, 2)); // No less than one round return (int)Math.Max(1, roundCount); }
public ActiveRoundResponse Create(Tournament tournament, int? currentRoundNumber) { return new ActiveRoundResponse(tournament.Key, currentRoundNumber); }