All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.alloyggp.tournament.internal.SwissFormat1Runner Maven / Gradle / Ivy

There is a newer version: 0.1.0
Show newest version
package net.alloyggp.tournament.internal;

import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.NotThreadSafe;

import org.joda.time.DateTime;

import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultiset;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multiset;
import com.google.common.collect.Multiset.Entry;
import com.google.common.collect.Ordering;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;

import net.alloyggp.tournament.api.TGame;
import net.alloyggp.tournament.api.TMatchResult;
import net.alloyggp.tournament.api.TMatchResult.Outcome;
import net.alloyggp.tournament.api.TMatchSetup;
import net.alloyggp.tournament.api.TNextMatchesResult;
import net.alloyggp.tournament.api.TPlayer;
import net.alloyggp.tournament.api.TPlayerScore;
import net.alloyggp.tournament.api.TRanking;
import net.alloyggp.tournament.api.TScore;
import net.alloyggp.tournament.api.TSeeding;
import net.alloyggp.tournament.internal.spec.MatchSpec;
import net.alloyggp.tournament.internal.spec.RoundSpec;

public class SwissFormat1Runner implements FormatRunner {
    private static final SwissFormat1Runner INSTANCE = new SwissFormat1Runner();

    private SwissFormat1Runner() {
        //Not instantiable
    }

    public static SwissFormat1Runner create() {
        return INSTANCE;
    }

    //TODO: Factor out the common elements between this and SingleEliminationFormatSimulator
    @NotThreadSafe
    private static class SwissFormatSimulator {
        private final String tournamentInternalName;
        private final int stageNum;
        private final TSeeding initialSeeding;
        private final ImmutableList rounds;
        private final ImmutableSet resultsFromEarlierStages;
        private final ImmutableSet resultsInStage;
        private final Set matchesToRun = Sets.newHashSet();
        //TODO: Double-check that all these stats are updated appropriately
        private TGame mostRecentGame = null; //of a fully completed round
        private final Map totalPointsScored = Maps.newHashMap();
        private final Map> pointsScoredByGame = Maps.newHashMap();
        private final Map pointsFromByes = Maps.newHashMap();
        private final Multiset> totalMatchupsSoFar = HashMultiset.create();
        private final Map>> matchupsSoFarByGame = Maps.newHashMap();
        private final Map>> nonFixedSumMatchupsSoFarByNumPlayers = Maps.newHashMap();
//        private final Map>> nonFixedSumRoleAssignmentsSoFarByNumPlayers = Maps.newHashMap();
        private final List standingsHistory = Lists.newArrayList();
        private @Nullable DateTime latestStartTimeSeen = null;

        private SwissFormatSimulator(String tournamentInternalName, int stageNum, TSeeding initialSeeding,
                ImmutableList rounds, ImmutableSet resultsFromEarlierStages,
                ImmutableSet resultsInStage) {
            this.tournamentInternalName = tournamentInternalName;
            this.stageNum = stageNum;
            this.initialSeeding = initialSeeding;
            this.rounds = rounds;
            this.resultsFromEarlierStages = resultsFromEarlierStages;
            this.resultsInStage = resultsInStage;
        }

        public static SwissFormatSimulator createAndRun(String tournamentInternalName, int stageNum, TSeeding initialSeeding,
                ImmutableList rounds, Set allResultsSoFar) {
            Set resultsFromEarlierStages = MatchResults.getResultsPriorToStage(allResultsSoFar, stageNum);
            Set resultsInStage = MatchResults.filterByStage(allResultsSoFar, stageNum);
            SwissFormatSimulator simulator = new SwissFormatSimulator(tournamentInternalName, stageNum, initialSeeding,
                    rounds, ImmutableSet.copyOf(resultsFromEarlierStages), ImmutableSet.copyOf(resultsInStage));
            simulator.run();
            return simulator;
        }

        private void run() {
            setInitialTotalsToZero();
            int roundNum = 0;
            SetMultimap matchesByRound = MatchResults.mapByRound(resultsInStage, stageNum);

            @Nullable EndOfRoundState endOfRoundState = TournamentStateCache.getLatestCachedEndOfRoundState(tournamentInternalName, initialSeeding, resultsFromEarlierStages, stageNum, resultsInStage);
            if (endOfRoundState != null) {
                Swiss1EndOfRoundState state = (Swiss1EndOfRoundState) endOfRoundState;
                roundNum = state.roundNum + 1;
                loadCachedState(state);
            }

            for (/* roundNum already set */; roundNum < rounds.size(); roundNum++) {
                RoundSpec round = rounds.get(roundNum);
                Set roundResults = matchesByRound.get(roundNum);
                runRound(round, roundNum, roundResults);
                if (!matchesToRun.isEmpty()) {
                    //We're still finishing up this round, not ready to assign matches in the next one
                    return;
                }
                //If we didn't run the round due to the number of players being too low,
                //skip the standings and caching
                if (!roundResults.isEmpty()) {
                    standingsHistory.add(getStandings());
                    Swiss1EndOfRoundState state = Swiss1EndOfRoundState.create(roundNum,
                            mostRecentGame, totalPointsScored, pointsScoredByGame,
                            pointsFromByes, totalMatchupsSoFar, matchupsSoFarByGame,
                            nonFixedSumMatchupsSoFarByNumPlayers, standingsHistory,
                            latestStartTimeSeen);

                    TournamentStateCache.cacheEndOfRoundState(tournamentInternalName, initialSeeding, resultsFromEarlierStages, stageNum, resultsInStage, state);
                }
            }
        }

        private void loadCachedState(Swiss1EndOfRoundState state) {
            totalPointsScored.putAll(state.totalPointsScored);
            pointsFromByes.putAll(state.pointsFromByes);

            Set possiblePlayerCounts = Sets.newHashSet();
            for (TGame game : RoundSpec.getAllGames(rounds)) {
                Map pointsScoredForGame = pointsScoredByGame.get(game);
                for (TPlayer player : initialSeeding.getPlayersBestFirst()) {
                    pointsScoredForGame.put(player, state.pointsScoredByGame.get(game).get(player));
                }
                for (Entry> entry : state.matchupsSoFarByGame.get(game).entrySet()) {
                    matchupsSoFarByGame.get(game).add(Sets.newHashSet(entry.getElement()), entry.getCount());
                }
                possiblePlayerCounts.add(game.getNumRoles());
            }
            for (int playerCount : possiblePlayerCounts) {
                for (Entry> entry : state.nonFixedSumMatchupsSoFarByNumPlayers.get(playerCount).entrySet()) {
                    nonFixedSumMatchupsSoFarByNumPlayers.get(playerCount).add(Sets.newHashSet(entry.getElement()), entry.getCount());
                }
            }

            mostRecentGame = state.mostRecentGame;
            standingsHistory.addAll(state.standingsHistory);

            for (Entry> entry : state.totalMatchupsSoFar.entrySet()) {
                totalMatchupsSoFar.add(Sets.newHashSet(entry.getElement()), entry.getCount());
            }
            latestStartTimeSeen = state.latestStartTimeSeen;
        }

        private void runRound(RoundSpec round, int roundNum, Set roundResults) {
            handleStartTimeForRound(round);
            //...there should be only one match per round, I think?
            //Or at least they must involve the same game?
            TGame game = getOnlyGame(round);
            //Figure out how to assign players
            List> playerGroups = getPlayerGroups(game);
            double maxScoreAchieved = 0;
            double scoreSum = 0;
            int scoreCount = 0;
            for (int groupNum = 0; groupNum < playerGroups.size(); groupNum++) {
                List players = playerGroups.get(groupNum);
                for (int matchNum = 0; matchNum < round.getMatches().size(); matchNum++) {
                    MatchSpec match = round.getMatches().get(matchNum);
                    Optional attemptNum = getAttemptNumberIfUnfinished(groupNum, matchNum, roundResults);
                    if (attemptNum.isPresent()) {
                        String matchId = MatchIds.create(tournamentInternalName, stageNum,
                                roundNum, groupNum, matchNum, attemptNum.get());

                        matchesToRun.add(match.createMatchSetup(matchId, players));
                        break;
                    } else {
                        TMatchResult result = getSuccessfulAttempt(groupNum, matchNum, roundResults);
                        //Add the results of the match to our point totals
                        List playersInRoleOrder = match.putInOrder(players);
                        for (int role = 0; role < players.size(); role++) {
                            TPlayer player = playersInRoleOrder.get(role);
                            double goalValue = result.getGoals().get(role) * match.getWeight();

                            //TODO: Add to stats here, including stats yet to be introduced
                            //such as player-player meetings
                            addToSumWithKey(player, goalValue, totalPointsScored);
                            addToSumWithKey(player, goalValue, pointsScoredByGame.get(game));

                            maxScoreAchieved = maxScoreAchieved > goalValue ? maxScoreAchieved : goalValue;
                            scoreSum += goalValue;
                            scoreCount++;
                            this.mostRecentGame = game;
                        }
                    }
                }
            }
            if (matchesToRun.isEmpty()) {
                //If we're at the end of a round and all the groups have gone and
                //we have a player left, manage the byes
                Set unassignedPlayers = getUnassignedPlayers(initialSeeding.getPlayersBestFirst(), playerGroups);
                if (!unassignedPlayers.isEmpty()) {
                    //Calculate bye score for the game
                    double byeScore = getByeScoreForRound(game, maxScoreAchieved, scoreSum, scoreCount);
                    Preconditions.checkState(byeScore >= 0 && byeScore <= 100);
                    for (TPlayer player : unassignedPlayers) {
                        addToSumWithKey(player, byeScore, totalPointsScored);
                        addToSumWithKey(player, byeScore, pointsScoredByGame.get(game));
                        addToSumWithKey(player, byeScore, pointsFromByes);
                    }
                }
                //Also...
                updateMatchupStats(game, playerGroups);
            }
        }

        private void handleStartTimeForRound(RoundSpec round) {
            if (round.getStartTime().isPresent()) {
                DateTime roundStartTime = round.getStartTime().get();
                if (latestStartTimeSeen == null
                        || latestStartTimeSeen.isBefore(roundStartTime)) {
                    latestStartTimeSeen = roundStartTime;
                }
            }
        }

        private void updateMatchupStats(TGame game, List> playerGroups) {
            for (List players : playerGroups) {
                if (game.isFixedSum()) {
                    if (game.getNumRoles() == 2) {
                        matchupsSoFarByGame.get(game).add(ImmutableSet.of(players.get(0), players.get(1)));
                        totalMatchupsSoFar.add(ImmutableSet.of(players.get(0), players.get(1)));
                    } else {
                        for (int p1 = 0; p1 < players.size(); p1++) {
                            for (int p2 = p1 + 1; p2 < players.size(); p2++) {
                                matchupsSoFarByGame.get(game)
                                    .add(ImmutableSet.of(players.get(p1), players.get(p2)));
                                totalMatchupsSoFar.add(ImmutableSet.of(players.get(p1), players.get(p2)));
                            }
                        }
                    }
                } else {
                    //TODO: Fix this
                    for (int p1 = 0; p1 < players.size(); p1++) {
                        for (int p2 = p1 + 1; p2 < players.size(); p2++) {
                            nonFixedSumMatchupsSoFarByNumPlayers.get(game.getNumRoles())
                                .add(ImmutableSet.of(players.get(p1), players.get(p2)));
                        }
                    }
//                    nonFixedSumRoleAssignmentsSoFarByNumPlayers
                }
            }
        }

        private static double getByeScoreForRound(TGame game, double maxScoreAchieved, double scoreSum, int scoreCount) {
            if (game.isFixedSum()) {
                if (game.getNumRoles() == 2) {
                    return maxScoreAchieved;
                } else {
                    return maxScoreAchieved;
                }
            } else {
                if (scoreCount > 0) {
                    return scoreSum / scoreCount;
                } else {
                    //Not enough players for the round
                    return 0.0;
                }
            }
        }

        private List> getPlayerGroups(TGame game) {
            int numRoles = game.getNumRoles();

            if (numRoles == 1) {
                ImmutableList.Builder> builder = ImmutableList.builder();
                for (TPlayer player : initialSeeding.getPlayersBestFirst()) {
                    builder.add(ImmutableList.of(player));
                }
                return builder.build();
            }
            if (game.isFixedSum()) {
                if (numRoles == 2) {
                    return getTwoPlayerFixedSumPlayerGroups(game);
                } else {
                    return getManyPlayerFixedSumPlayerGroups(game);
                }
            } else {
                //Use quasi-random pairings
                return getNonFixedSumPlayerGroups(game.getNumRoles());
            }
        }

        //TODO: Consider prioritizing certain players for byes? e.g. players
        //doing worse in the tournament, those with fewer byes already?
        private List> getNonFixedSumPlayerGroups(int numRoles) {

            //Two-player: there's an elegant round-robin algorithm we
            //could use here, "rotate" all but one player in two rows

            //Do something naive for now
            final Multiset> matchupsSoFar = nonFixedSumMatchupsSoFarByNumPlayers.get(numRoles);
            Preconditions.checkNotNull(matchupsSoFar);
//            Map> roleAssignmentsSoFar = nonFixedSumRoleAssignmentsSoFarByNumPlayers.get(numRoles);
            List playersToAssign = Lists.newArrayList(initialSeeding.getPlayersBestFirst());
            List> results = Lists.newArrayList();
            while (playersToAssign.size() >= numRoles) {
                TPlayer player = playersToAssign.get(0);

                //Grab the first available players with the fewest previous matchups against us and each other
                final List playersInGroup = Lists.newArrayList(player);
                playersToAssign.remove(player);
                while (playersInGroup.size() < numRoles) {
                    Ordering playerOrder = Ordering.from(new Comparator() {
                                @Override
                                public int compare(TPlayer p1, TPlayer p2) {
                                    int sum1 = getSum(p1);
                                    int sum2 = getSum(p2);
                                    return Integer.compare(sum1, sum2);
                                }

                                //Sum of matchups against players already in group
                                private int getSum(TPlayer p) {
                                    int sum = 0;
                                    for (TPlayer playerInGroup : playersInGroup) {
                                        sum += matchupsSoFar.count(ImmutableSet.of(p, playerInGroup));
                                    }
                                    return sum;
                                }
                            })
                            .compound(new Comparator() {
                                @Override
                                public int compare(TPlayer o1, TPlayer o2) {
                                    int seed1 = initialSeeding.getPlayersBestFirst().indexOf(o1);
                                    int seed2 = initialSeeding.getPlayersBestFirst().indexOf(o2);
                                    return Integer.compare(seed1, seed2);
                                }
                            });
                    TPlayer playerToAdd = playerOrder.min(playersToAssign);
                    playersInGroup.add(playerToAdd);
                    playersToAssign.remove(playerToAdd);
                }
                //TODO: Shuffle the roles intelligently, somehow
                //Should role shuffling be per-game? Across the tournament?

                results.add(playersInGroup);
            }
            return results;
        }

        private List> getManyPlayerFixedSumPlayerGroups(TGame game) {
            List> groups = Lists.newArrayList();

            Set assignedSoFar = Sets.newHashSet();
            List overallPlayerRankings = getPlayerRankingsForGame(game);
            int numPlayers = initialSeeding.getPlayersBestFirst().size();
            while (numPlayers - assignedSoFar.size() >= game.getNumRoles()) {
                List curGroup = Lists.newArrayList();
                //First, get the best player left according to the rankings so far
                TPlayer firstPlayer = getFirstUnassignedPlayer(overallPlayerRankings, assignedSoFar);
                curGroup.add(firstPlayer);
                assignedSoFar.add(firstPlayer);
                while (curGroup.size() < game.getNumRoles()) {
                    //Now we look for the best opponent for those players
                    List opponentRankings = getOpponentRankingsForPlayers(curGroup, game);
                    TPlayer opponent = getFirstUnassignedPlayer(opponentRankings, assignedSoFar);
                    curGroup.add(opponent);
                    assignedSoFar.add(opponent);
                }
                groups.add(ImmutableList.copyOf(curGroup));
            }

            return groups;
        }

        //TODO: Avoid awarding another bye to a player; when only one player in
        //assignedSoFar has NOT had a bye, (and the total number of players is
        //odd,) remove that player and reserve them for a bye
        private List> getTwoPlayerFixedSumPlayerGroups(TGame game) {
            List> groups = Lists.newArrayList();

            Set assignedSoFar = Sets.newHashSet();
            List overallPlayerRankings = getPlayerRankingsForGame(game);
            int numPlayers = initialSeeding.getPlayersBestFirst().size();
            while (numPlayers - assignedSoFar.size() >= game.getNumRoles()) {
                //First, get the best player left according to the rankings so far
                TPlayer firstPlayer = getFirstUnassignedPlayer(overallPlayerRankings, assignedSoFar);
                assignedSoFar.add(firstPlayer);
                //Now we look for the best opponent for that player
                List opponentRankings = getOpponentRankingsForPlayer(firstPlayer, game);
                TPlayer opponent = getFirstUnassignedPlayer(opponentRankings, assignedSoFar);
                assignedSoFar.add(opponent);
                //Best seed goes first
                groups.add(ImmutableList.of(firstPlayer, opponent));
            }

            return groups;
        }

        private List getOpponentRankingsForPlayer(TPlayer firstPlayer, TGame game) {
            return getOpponentRankingsForPlayers(ImmutableList.of(firstPlayer), game);
        }

        private List getOpponentRankingsForPlayers(
                final List curGroup, final TGame game) {
            List allOpponents = Lists.newArrayList(initialSeeding.getPlayersBestFirst());
            allOpponents.removeAll(curGroup);

            Collections.sort(allOpponents, Ordering.from(
            new Comparator() {
                @Override
                public int compare(TPlayer o1, TPlayer o2) {
                    double score1 = compute(o1);
                    double score2 = compute(o2);
                    return Double.compare(score1, score2);
                }
                //Higher points scored by game better
                //but discount 100/n points per matchup already played in this game
                private double compute(TPlayer opponent) {
                    double pointsScored = pointsScoredByGame.get(game).get(opponent);
                    for (TPlayer player : curGroup) {
                        pointsScored -= (100.0 / curGroup.size())
                                * matchupsSoFarByGame.get(game).count(ImmutableSet.of(player, opponent));
                    }
                    return pointsScored;
                }
            }
            ).compound(
                    new Comparator() {
                        @Override
                        public int compare(TPlayer o1, TPlayer o2) {
                            double score1 = compute(o1);
                            double score2 = compute(o2);
                            return Double.compare(score1, score2);
                        }

                        private double compute(TPlayer opponent) {
                            //Higher total points scored better
                            //but discount 100/n points per matchup already played in any Swiss rounds
                            double pointsScored = totalPointsScored.get(opponent);
                            for (TPlayer player : curGroup) {
                                pointsScored -= (100.0 / curGroup.size())
                                        * totalMatchupsSoFar.count(ImmutableSet.of(player, opponent));
                            }
                            return pointsScored;
                        }
                    }
            ).reverse()
                    .compound(new Comparator() {
                                @Override
                                public int compare(TPlayer p1, TPlayer p2) {
                                    int seed1 = initialSeeding.getPlayersBestFirst().indexOf(p1);
                                    int seed2 = initialSeeding.getPlayersBestFirst().indexOf(p2);
                                    return Integer.compare(seed1, seed2);
                                }
                    }));
            return allOpponents;
        }

        private List getPlayerRankingsForGame(final TGame game) {
            List players = Lists.newArrayList(initialSeeding.getPlayersBestFirst());
            //Sort according to an appropriate comparator
            //We do want the best players at the beginning of the list...
            //We may accomplish that through an appropriate reversal
            //First
            Collections.sort(players,
                    Ordering.from(new Comparator() {
                        @Override
                        public int compare(TPlayer o1, TPlayer o2) {
                            Double score1 = pointsScoredByGame.get(game).get(o1);
                            Double score2 = pointsScoredByGame.get(game).get(o2);
                            return Double.compare(score1, score2);
                        }
                    }).compound(new Comparator() {
                        @Override
                        public int compare(TPlayer o1, TPlayer o2) {
                            Double score1 = totalPointsScored.get(o1);
                            Double score2 = totalPointsScored.get(o2);
                            return Double.compare(score1, score2);
                        }
                    }).reverse()
                    .compound(new Comparator() {
                        @Override
                        public int compare(TPlayer o1, TPlayer o2) {
                            int score1 = initialSeeding.getPlayersBestFirst().indexOf(o1);
                            int score2 = initialSeeding.getPlayersBestFirst().indexOf(o2);
                            return Integer.compare(score1, score2);
                        }
                    }));
            return players;
        }

        private TPlayer getFirstUnassignedPlayer(List players, Set assignedSoFar) {
            for (TPlayer player : players) {
                if (!assignedSoFar.contains(player)) {
                    return player;
                }
            }
            throw new IllegalArgumentException("No unassigned players left");
        }

        private  void addToSumWithKey(K key, double addend, Map map) {
            map.put(key, addend + map.get(key));
        }

        private Optional getAttemptNumberIfUnfinished(int groupNum, int matchNum,
                Set roundResults) {
            int attemptsSoFar = 0;
            for (TMatchResult result : roundResults) {
                String matchId = result.getMatchId();
                if (groupNum == MatchIds.parsePlayerMatchingNumber(matchId)
                        && matchNum == MatchIds.parseMatchNumber(matchId)) {
                    if (result.getOutcome() == Outcome.ABORTED) {
                        attemptsSoFar++;
                    } else {
                        return Optional.absent();
                    }
                }
            }
            return Optional.of(attemptsSoFar);
        }

        private TMatchResult getSuccessfulAttempt(int groupNum, int matchNum, Set roundResults) {
            for (TMatchResult result : roundResults) {
                if (result.getOutcome() == Outcome.COMPLETED) {
                    String matchId = result.getMatchId();
                    if (groupNum == MatchIds.parsePlayerMatchingNumber(matchId)
                            && matchNum == MatchIds.parseMatchNumber(matchId)) {
                        return result;
                    }
                }
            }
            throw new IllegalArgumentException("No successful attempts found");
        }

        private void setInitialTotalsToZero() {
            for (TPlayer player : initialSeeding.getPlayersBestFirst()) {
                totalPointsScored.put(player, 0.0);
                pointsFromByes.put(player, 0.0);
            }
            Set possiblePlayerCounts = Sets.newHashSet();
            for (Game game : RoundSpec.getAllGames(rounds)) {
                HashMap pointsScoredForGame = Maps.newHashMap();
                pointsScoredByGame.put(game, pointsScoredForGame);
                for (TPlayer player : initialSeeding.getPlayersBestFirst()) {
                    pointsScoredForGame.put(player, 0.0);
                }
                matchupsSoFarByGame.put(game, HashMultiset.>create());
                possiblePlayerCounts.add(game.getNumRoles());
            }
            for (int playerCount : possiblePlayerCounts) {
                nonFixedSumMatchupsSoFarByNumPlayers.put(playerCount, HashMultiset.>create());
//                Map> nonFixedSumRoleAssignmentsSoFar = Maps.newHashMap();
//                for (Player player : initialSeeding.getPlayersBestFirst()) {
//                    nonFixedSumRoleAssignmentsSoFar.put(player, HashMultiset.create());
//                }
//                nonFixedSumRoleAssignmentsSoFarByNumPlayers.put(playerCount, nonFixedSumRoleAssignmentsSoFar);
            }

        }

        public TNextMatchesResult getMatchesToRun() {
            return StandardNextMatchesResult.create(ImmutableSet.copyOf(matchesToRun),
                    latestStartTimeSeen);
        }

        private TRanking getStandings() {
            Set scores = Sets.newHashSet();
            ImmutableList playersBestFirst = initialSeeding.getPlayersBestFirst();
            for (int i = 0; i < playersBestFirst.size(); i++) {
                TPlayer player = playersBestFirst.get(i);
                double mostRecentGamePoints = 0;
                String mostRecentGameName = null;
                if (mostRecentGame != null) {
                    mostRecentGamePoints = pointsScoredByGame.get(mostRecentGame).get(player);
                    mostRecentGameName = mostRecentGame.getId();
                }
                TScore score = new SwissScore(totalPointsScored.get(player),
                        mostRecentGameName, mostRecentGamePoints,
                        pointsFromByes.get(player));
                scores.add(TPlayerScore.create(player, score, i));
            }
            return StandardRanking.create(scores);
        }

        public List getStandingsHistory() {
            return ImmutableList.copyOf(standingsHistory);
        }

    }

    private static class SwissScore implements TScore {
        //T1K stands for "times 1000".
        private final long pointsSoFarT1K;
        //Just for display purposes; this makes it more obvious why matchups are selected
        private final @Nullable String mostRecentGameName;
        private final long pointsInMostRecentGameT1K;
        private final long pointsFromByesT1K;

        public SwissScore(double pointsSoFar, @Nullable String mostRecentGameName,
                double pointsInMostRecentGame, double pointsFromByes) {
            this.pointsSoFarT1K = roundToThreePlacesT1K(pointsSoFar);
            this.mostRecentGameName = mostRecentGameName;
            this.pointsInMostRecentGameT1K = roundToThreePlacesT1K(pointsInMostRecentGame);
            this.pointsFromByesT1K = roundToThreePlacesT1K(pointsFromByes);
        }

        private static long roundToThreePlacesT1K(double value) {
            return Math.round(value * 1000.0);
        }

        @Override
        public int compareTo(TScore other) {
            if (!(other instanceof SwissScore)) {
                throw new IllegalArgumentException();
            }
            return Long.compare(pointsSoFarT1K, ((SwissScore)other).pointsSoFarT1K);
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + (int) (pointsSoFarT1K ^ (pointsSoFarT1K >>> 32));
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            SwissScore other = (SwissScore) obj;
            if (pointsSoFarT1K != other.pointsSoFarT1K) {
                return false;
            }
            return true;
        }

        @Override
        public String toString() {
            return getDescription();
        }

        @Override
        public String getDescription() {
            String string;
            if (mostRecentGameName == null) {
                string = addDecimalPoint(pointsSoFarT1K) + " total";
            } else {
                string = addDecimalPoint(pointsSoFarT1K) + " total, " + addDecimalPoint(pointsInMostRecentGameT1K) + " in " + mostRecentGameName;
            }
            if (pointsFromByesT1K > 0L) {
                string += ", " + addDecimalPoint(pointsFromByesT1K) + " from byes";
            }
            return string;
        }

        private String addDecimalPoint(long value) {
            return String.format("%d.%03d", value / 1000, value % 1000);
        }
    }

    @Override
    public TNextMatchesResult getMatchesToRun(String tournamentInternalName, TSeeding initialSeeding, int stageNum,
            List rounds, Set allResultsSoFar) {
        return SwissFormatSimulator.createAndRun(tournamentInternalName, stageNum, initialSeeding,
                ImmutableList.copyOf(rounds), allResultsSoFar).getMatchesToRun();
    }

    public static Set getUnassignedPlayers(Collection allPlayers, List> playerGroups) {
        Set results = Sets.newHashSet(allPlayers);
        for (List group : playerGroups) {
            for (TPlayer player : group) {
                results.remove(player);
            }
        }
        return results;
    }

    @Override
    public List getStandingsHistory(String tournamentInternalName, TSeeding initialSeeding, int stageNum,
            List rounds, Set allResultsSoFar) {
        return SwissFormatSimulator.createAndRun(tournamentInternalName, stageNum, initialSeeding,
                ImmutableList.copyOf(rounds), allResultsSoFar).getStandingsHistory();
    }

    private static TGame getOnlyGame(RoundSpec round) {
        if (round.getMatches().isEmpty()) {
            throw new IllegalArgumentException("Swiss rounds must have at least one match");
        }
        TGame game = round.getMatches().get(0).getGame();
        for (MatchSpec match : round.getMatches()) {
            if (!game.equals(match.getGame())) {
                throw new IllegalArgumentException("Swiss rounds in Swiss variant 1 must use "
                        + "the same game in each match, and frequently have one match per round");
            }
        }
        return game;
    }

    @Override
    public void validateRounds(ImmutableList rounds) {
        //TODO: Implement
        for (RoundSpec round : rounds) {
            //Validates all matches in the round are the same game
            getOnlyGame(round);
        }
    }

    @Immutable
    private static class Swiss1EndOfRoundState implements EndOfRoundState {
        private final int roundNum;
        private final TGame mostRecentGame;
        private final ImmutableMap totalPointsScored;
        private final ImmutableMap> pointsScoredByGame;
        private final ImmutableMap pointsFromByes;
        private final ImmutableMultiset> totalMatchupsSoFar;
        private final ImmutableMap>> matchupsSoFarByGame;
        private final ImmutableMap>> nonFixedSumMatchupsSoFarByNumPlayers;
        private final ImmutableList standingsHistory;
        private final @Nullable DateTime latestStartTimeSeen;

        private Swiss1EndOfRoundState(int roundNum, TGame mostRecentGame, ImmutableMap totalPointsScored,
                ImmutableMap> pointsScoredByGame,
                ImmutableMap pointsFromByes, ImmutableMultiset> totalMatchupsSoFar,
                ImmutableMap>> matchupsSoFarByGame,
                ImmutableMap>> nonFixedSumMatchupsSoFarByNumPlayers,
                ImmutableList standingsHistory, @Nullable DateTime latestStartTimeSeen) {
            this.roundNum = roundNum;
            this.mostRecentGame = mostRecentGame;
            this.totalPointsScored = totalPointsScored;
            this.pointsScoredByGame = pointsScoredByGame;
            this.pointsFromByes = pointsFromByes;
            this.totalMatchupsSoFar = totalMatchupsSoFar;
            this.matchupsSoFarByGame = matchupsSoFarByGame;
            this.nonFixedSumMatchupsSoFarByNumPlayers = nonFixedSumMatchupsSoFarByNumPlayers;
            this.standingsHistory = standingsHistory;
            this.latestStartTimeSeen = latestStartTimeSeen;
        }

        public static Swiss1EndOfRoundState create(int roundNum,
                TGame mostRecentGame,
                Map totalPointsScored,
                Map> pointsScoredByGame,
                Map pointsFromByes,
                Multiset> totalMatchupsSoFar,
                Map>> matchupsSoFarByGame,
                Map>> nonFixedSumMatchupsSoFarByNumPlayers,
                List standingsHistory,
                @Nullable DateTime latestStartTimeSeen) {
            return new Swiss1EndOfRoundState(roundNum,
                    mostRecentGame,
                    ImmutableMap.copyOf(totalPointsScored),
                    toImmutableMapValuedMap(pointsScoredByGame),
                    ImmutableMap.copyOf(pointsFromByes),
                    toImmutableSetEntriedMultiset(totalMatchupsSoFar),
                    toImmutableMultisetOfSetsValuedMap(matchupsSoFarByGame),
                    toImmutableMultisetOfSetsValuedMap(nonFixedSumMatchupsSoFarByNumPlayers),
                    ImmutableList.copyOf(standingsHistory),
                    latestStartTimeSeen);
        }

        private static  ImmutableMap>> toImmutableMultisetOfSetsValuedMap(
                Map>> map) {
            return ImmutableMap.copyOf(Maps.transformValues(map, new Function>, ImmutableMultiset>>() {
                @Override
                public ImmutableMultiset> apply(@Nonnull Multiset> input) {
                    return toImmutableSetEntriedMultiset(input);
                }
            }));
        }

        private static  ImmutableMultiset> toImmutableSetEntriedMultiset(
                Multiset> multiset) {
            ImmutableMultiset.Builder> builder = ImmutableMultiset.builder();
            for (Entry> entry : multiset.entrySet()) {
                builder.addCopies(ImmutableSet.copyOf(entry.getElement()), entry.getCount());
            }
            return builder.build();
        }

        private static  ImmutableMap> toImmutableMapValuedMap(
                Map> map) {
            return ImmutableMap.copyOf(Maps.transformValues(map, new Function, ImmutableMap>() {
                @Override
                public ImmutableMap apply(@Nonnull Map input) {
                    return ImmutableMap.copyOf(input);
                }
            }));
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((latestStartTimeSeen == null) ? 0 : latestStartTimeSeen.hashCode());
            result = prime * result + ((matchupsSoFarByGame == null) ? 0 : matchupsSoFarByGame.hashCode());
            result = prime * result + ((mostRecentGame == null) ? 0 : mostRecentGame.hashCode());
            result = prime * result + ((nonFixedSumMatchupsSoFarByNumPlayers == null) ? 0
                    : nonFixedSumMatchupsSoFarByNumPlayers.hashCode());
            result = prime * result + ((pointsFromByes == null) ? 0 : pointsFromByes.hashCode());
            result = prime * result + ((pointsScoredByGame == null) ? 0 : pointsScoredByGame.hashCode());
            result = prime * result + roundNum;
            result = prime * result + ((standingsHistory == null) ? 0 : standingsHistory.hashCode());
            result = prime * result + ((totalMatchupsSoFar == null) ? 0 : totalMatchupsSoFar.hashCode());
            result = prime * result + ((totalPointsScored == null) ? 0 : totalPointsScored.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            Swiss1EndOfRoundState other = (Swiss1EndOfRoundState) obj;
            if (latestStartTimeSeen == null) {
                if (other.latestStartTimeSeen != null) {
                    return false;
                }
            } else if (!latestStartTimeSeen.equals(other.latestStartTimeSeen)) {
                return false;
            }
            if (matchupsSoFarByGame == null) {
                if (other.matchupsSoFarByGame != null) {
                    return false;
                }
            } else if (!matchupsSoFarByGame.equals(other.matchupsSoFarByGame)) {
                return false;
            }
            if (mostRecentGame == null) {
                if (other.mostRecentGame != null) {
                    return false;
                }
            } else if (!mostRecentGame.equals(other.mostRecentGame)) {
                return false;
            }
            if (nonFixedSumMatchupsSoFarByNumPlayers == null) {
                if (other.nonFixedSumMatchupsSoFarByNumPlayers != null) {
                    return false;
                }
            } else if (!nonFixedSumMatchupsSoFarByNumPlayers.equals(other.nonFixedSumMatchupsSoFarByNumPlayers)) {
                return false;
            }
            if (pointsFromByes == null) {
                if (other.pointsFromByes != null) {
                    return false;
                }
            } else if (!pointsFromByes.equals(other.pointsFromByes)) {
                return false;
            }
            if (pointsScoredByGame == null) {
                if (other.pointsScoredByGame != null) {
                    return false;
                }
            } else if (!pointsScoredByGame.equals(other.pointsScoredByGame)) {
                return false;
            }
            if (roundNum != other.roundNum) {
                return false;
            }
            if (standingsHistory == null) {
                if (other.standingsHistory != null) {
                    return false;
                }
            } else if (!standingsHistory.equals(other.standingsHistory)) {
                return false;
            }
            if (totalMatchupsSoFar == null) {
                if (other.totalMatchupsSoFar != null) {
                    return false;
                }
            } else if (!totalMatchupsSoFar.equals(other.totalMatchupsSoFar)) {
                return false;
            }
            if (totalPointsScored == null) {
                if (other.totalPointsScored != null) {
                    return false;
                }
            } else if (!totalPointsScored.equals(other.totalPointsScored)) {
                return false;
            }
            return true;
        }

        @Override
        public String toString() {
            return "Swiss1EndOfRoundState [roundNum=" + roundNum + ", mostRecentGame=" + mostRecentGame
                    + ", totalPointsScored=" + totalPointsScored + ", pointsScoredByGame=" + pointsScoredByGame
                    + ", pointsFromByes=" + pointsFromByes + ", totalMatchupsSoFar=" + totalMatchupsSoFar
                    + ", matchupsSoFarByGame=" + matchupsSoFarByGame + ", nonFixedSumMatchupsSoFarByNumPlayers="
                    + nonFixedSumMatchupsSoFarByNumPlayers + ", standingsHistory=" + standingsHistory
                    + ", latestStartTimeSeen=" + latestStartTimeSeen + "]";
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy