/*
* Copyright 2011 BetaSteward_at_googlemail.com. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are those of the
* authors and should not be interpreted as representing official policies, either expressed
* or implied, of BetaSteward_at_googlemail.com.
*/
package mage.game.tournament.pairing;
import mage.game.tournament.Round;
import mage.game.tournament.TournamentPairing;
import mage.game.tournament.TournamentPlayer;
import java.util.*;
/**
*
* @author Quercitron
*/
// SwissPairingMinimalWeightMatching creates round pairings for swiss tournament.
// It assigns weight to each possible pair and searches perfect matching with minimal weight
// for more details see https://www.leaguevine.com/blog/18/swiss-tournament-scheduling-leaguevines-new-algorithm/
// This implementation don't use fast minimum weight maximum matching algorithm,
// it uses brute-force search, so it works reasonably fast only up to 16 players.
public class SwissPairingMinimalWeightMatching {
private final int playersCount;
List<PlayerInfo> swissPlayers;
// number of vertexes in graph
private final int n;
// weight of pairings
private final int[][] w;
public SwissPairingMinimalWeightMatching(List<TournamentPlayer> players, List<Round> rounds, boolean isLastRound) {
playersCount = players.size();
swissPlayers = new ArrayList<>();
for (TournamentPlayer tournamentPlayer : players) {
PlayerInfo swissPlayer = new PlayerInfo();
swissPlayer.tournamentPlayer = tournamentPlayer;
swissPlayer.points = tournamentPlayer.getPoints();
swissPlayers.add(swissPlayer);
}
// shuffle players first to add some randomness
Collections.shuffle(swissPlayers);
Map<TournamentPlayer, Integer> map = new HashMap<>();
for (int i = 0; i < playersCount; i++) {
swissPlayers.get(i).id = i;
map.put(swissPlayers.get(i).tournamentPlayer, i);
}
// calculate Tie Breaker points -- Sum of Opponents' Scores (SOS)
// see http://senseis.xmp.net/?SOS
for (Round round : rounds) {
for (TournamentPairing pairing : round.getPairs()) {
TournamentPlayer player1 = pairing.getPlayer1();
TournamentPlayer player2 = pairing.getPlayer2();
Integer id1 = map.get(player1);
Integer id2 = map.get(player2);
// a player could have left the tournament, so we should check if id is not null
if (id1 != null) {
swissPlayers.get(id1).sosPoints += player2.getPoints();
}
if (id2 != null) {
swissPlayers.get(id2).sosPoints += player1.getPoints();
}
// todo: sos points for byes? maybe add player points?
}
}
// sort by points and then by sos points
swissPlayers.sort((p1, p2) -> {
int result = p2.points - p1.points;
if (result != 0) {
return result;
}
return p2.sosPoints - p1.sosPoints;
});
// order could be changed, update ids and mapping
map.clear();
for (int i = 0; i < playersCount; i++) {
swissPlayers.get(i).id = i;
map.put(swissPlayers.get(i).tournamentPlayer, i);
}
// count ties and matches between players
int[][] duels = new int[playersCount][playersCount];
int[] byes = new int[playersCount];
for (Round round : rounds) {
for (TournamentPairing pairing : round.getPairs()) {
TournamentPlayer player1 = pairing.getPlayer1();
TournamentPlayer player2 = pairing.getPlayer2();
Integer id1 = map.get(player1);
Integer id2 = map.get(player2);
if (id1 != null && id2 != null) {
duels[id1][id2]++;
duels[id2][id1]++;
}
}
for (TournamentPlayer playerBye : round.getPlayerByes()) {
Integer id = map.get(playerBye);
if (id != null) {
byes[id]++;
}
}
}
// set vertex count
// add vertex for bye if we have odd number of players
n = (playersCount % 2 == 1 ? playersCount + 1 : playersCount);
// calculate weight
// try to pair players with equal scores
w = new int[n][n];
int pointsDiffMultiplier = 10;
if (isLastRound) {
// for the last round, for each unpaired player starting with the first place, pair
// against the highest ranked player they haven't played against
for (int i = 0; i < playersCount; i++) {
for (int j = 0; j < i; j++) {
w[i][j] = Math.abs(i - j) +
pointsDiffMultiplier * Math.abs(swissPlayers.get(i).points - swissPlayers.get(j).points);
w[j][i] = w[i][j];
}
}
} else {
for (int i = 0; i < playersCount; i++) {
PlayerInfo player = swissPlayers.get(i);
for (int p = player.points; p >= 0; p--) {
int first = -1;
int last = -1;
for (int j = 0; j < playersCount; j++) {
if (swissPlayers.get(j).points == p) {
if (first < 0) {
first = j;
}
last = j;
}
}
if (first < 0) {
continue;
}
int self = (p == player.points ? i : first - 1);
int diff = pointsDiffMultiplier * (player.points - p);
for (int j = Math.max(first, i); j <= last; j++) {
w[i][j] = Math.abs(j - (last + first - self)) + diff;
w[j][i] = w[i][j];
}
}
}
}
// avoid pairing players that have played each other already
for (int i = 0; i < playersCount; i++) {
for (int j = 0; j < i; j++) {
w[i][j] += duels[i][j] * 500;
w[j][i] = w[i][j];
}
}
// try to give bye to a player with a low score
// try to avoid giving the same person multiple byes
if (n > playersCount) {
for (int i = 0; i < playersCount; i++) {
w[i][n - 1] = 10 * (swissPlayers.get(i).points - swissPlayers.get(playersCount - 1).points) + (playersCount - i - 1);
w[i][n - 1] += byes[i] * 2000;
w[n - 1][i] = w[i][n - 1];
}
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
w[i][j] *= w[i][j];
}
}
// initialize variables for backtrack
used = new boolean[n];
pairs = new int[n];
Arrays.fill(pairs, -1);
result = new int[n];
weight = 0;
minCost = -1;
makePairings(0);
}
public RoundPairings getRoundPairings() {
// return round pairings with minimal weight
List<TournamentPairing> pairings = new ArrayList<>();
List<TournamentPlayer> playerByes = new ArrayList<>();
Map<Integer, TournamentPlayer> map = new HashMap<>();
for (PlayerInfo player : swissPlayers) {
map.put(player.id, player.tournamentPlayer);
}
if (n > playersCount) {
// last vertex -- bye
playerByes.add(map.get(result[n - 1]));
result[result[n - 1]] = -1;
result[n - 1] = -1;
}
for (int i = 0; i < playersCount; i++) {
if (result[i] >= 0) {
pairings.add(new TournamentPairing(map.get(i), map.get(result[i])));
result[result[i]] = -1;
result[i] = -1;
}
}
return new RoundPairings(pairings, playerByes);
}
boolean[] used;
// current pairs
int[] pairs;
// current weight
int weight;
int[] result;
int minCost;
// backtrack all possible pairings and choose one with minimal weight
private void makePairings(int t) {
if (t >= n) {
if (minCost < 0 || minCost > weight) {
minCost = weight;
System.arraycopy(pairs, 0, result, 0, n);
}
return;
}
if (!used[t]) {
for (int i = t + 1; i < n; i++) {
if (!used[i]) {
pairs[t] = i;
pairs[i] = t;
used[t] = true;
used[i] = true;
weight += w[t][i];
makePairings(t + 1);
pairs[t] = -1;
pairs[i] = -1;
used[t] = false;
used[i] = false;
weight -= w[t][i];
}
}
} else {
makePairings(t + 1);
}
}
static class PlayerInfo {
public int id;
public TournamentPlayer tournamentPlayer;
public int points;
public int sosPoints;
}
}