/*
* 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;
import mage.cards.ExpansionSet;
import mage.cards.decks.Deck;
import mage.constants.TournamentPlayerState;
import mage.game.draft.Draft;
import mage.game.draft.DraftCube;
import mage.game.events.*;
import mage.game.events.TableEvent.EventType;
import mage.game.match.Match;
import mage.game.match.MatchPlayer;
import mage.game.result.ResultProtos.*;
import mage.players.Player;
import mage.players.PlayerType;
import mage.util.RandomUtil;
import org.apache.log4j.Logger;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
/**
*
* @author BetaSteward_at_googlemail.com
*/
public abstract class TournamentImpl implements Tournament {
protected UUID id = UUID.randomUUID();
protected List<Round> rounds = new CopyOnWriteArrayList<>();
protected Map<UUID, TournamentPlayer> players = new HashMap<>();
protected String matchName;
protected TournamentOptions options;
protected TournamentType tournamentType;
protected List<ExpansionSet> sets = new ArrayList<>();
protected String setsInfoShort;
protected TableEventSource tableEventSource = new TableEventSource();
protected PlayerQueryEventSource playerQueryEventSource = new PlayerQueryEventSource();
protected Date startTime;
protected Date endTime;
protected Date stepStartTime;
protected boolean abort;
protected String tournamentState;
protected Draft draft;
public TournamentImpl(TournamentOptions options) {
this.options = options;
draft = null;
startTime = new Date(); // will be overwritten again as the tournament really starts
abort = false;
}
@Override
public UUID getId() {
return id;
}
@Override
public void addPlayer(Player player, PlayerType playerType) {
players.put(player.getId(), new TournamentPlayer(player, playerType));
}
@Override
public void removePlayer(UUID playerId) {
players.remove(playerId);
}
@Override
public TournamentPlayer getPlayer(UUID playerId) {
return players.get(playerId);
}
@Override
public void autoSubmit(UUID playerId, Deck deck) {
if (players.containsKey(playerId)) {
players.get(playerId).submitDeck(deck);
}
synchronized (this) {
this.notifyAll();
}
}
@Override
public TournamentOptions getOptions() {
return options;
}
@Override
public Collection<TournamentPlayer> getPlayers() {
return players.values();
}
@Override
public int getNumberRounds() {
return options.getNumberRounds();
}
@Override
public Collection<Round> getRounds() {
return rounds;
}
@Override
public List<ExpansionSet> getSets() {
return sets;
}
@Override
public void setBoosterInfo(String setsInfoShort) {
this.setsInfoShort = setsInfoShort;
}
@Override
public String getBoosterInfo() {
return setsInfoShort;
}
@Override
public void quit(UUID playerId) {
synchronized (this) {
this.notifyAll();
}
}
// can only be used, if tournament did not start yet?
@Override
public void leave(UUID playerId) {
if (players.containsKey(playerId)) {
players.remove(playerId);
}
}
@Override
public void submitDeck(UUID playerId, Deck deck) {
if (players.containsKey(playerId)) {
players.get(playerId).submitDeck(deck);
}
synchronized (this) {
this.notifyAll();
}
}
@Override
public void updateDeck(UUID playerId, Deck deck) {
if (players.containsKey(playerId)) {
players.get(playerId).updateDeck(deck);
}
}
protected Round createRoundRandom() {
Round round = new Round(rounds.size() + 1, this);
rounds.add(round);
List<TournamentPlayer> roundPlayers = getActivePlayers();
// search the player with a bye last round
List<TournamentPlayer> playerWithByes = getTournamentPlayersWithBye(roundPlayers);
while (roundPlayers.size() > 1) {
TournamentPlayer player1 = getNextAvailablePlayer(roundPlayers, playerWithByes);
TournamentPlayer player2 = getNextAvailablePlayer(roundPlayers, playerWithByes);
round.addPairing(new TournamentPairing(player1, player2));
}
if (!roundPlayers.isEmpty()) {
// player free round - add to bye players of this round
TournamentPlayer player1 = roundPlayers.get(0);
round.getPlayerByes().add(player1);
player1.setState(TournamentPlayerState.WAITING);
player1.setStateInfo("Round Bye");
updateResults();
}
return round;
}
private TournamentPlayer getNextAvailablePlayer(List<TournamentPlayer> roundPlayers, List<TournamentPlayer> playerWithByes) {
TournamentPlayer nextPlayer;
if (playerWithByes.isEmpty()) {
int i = RandomUtil.nextInt(roundPlayers.size());
nextPlayer = roundPlayers.get(i);
roundPlayers.remove(i);
} else { // prefer players with byes to pair
Iterator<TournamentPlayer> iterator = playerWithByes.iterator();
nextPlayer = iterator.next();
iterator.remove();
roundPlayers.remove(nextPlayer);
}
return nextPlayer;
}
private List<TournamentPlayer> getTournamentPlayersWithBye(List<TournamentPlayer> roundPlayers) {
List<TournamentPlayer> playersWithBye = new ArrayList<>();
if (rounds.size() > 1) {
for (int i = rounds.size() - 2; i >= 0; i--) {
Round oldRound = rounds.get(i);
if (oldRound != null && !oldRound.getPlayerByes().isEmpty()) {
TournamentPlayer tournamentPlayerWithBye = oldRound.getPlayerByes().iterator().next();
if (roundPlayers.contains(tournamentPlayerWithBye)) {
playersWithBye.add(tournamentPlayerWithBye);
}
}
}
}
return playersWithBye;
}
protected void playRound(Round round) {
for (TournamentPairing pair : round.getPairs()) {
playMatch(pair);
}
updateResults(); // show points from byes
while (!round.isRoundOver()) {
try {
//TODO: improve this
Thread.sleep(1000);
} catch (InterruptedException ex) {
Logger.getLogger(TournamentImpl.class).warn("TournamentImpl playRound error ", ex);
break;
}
}
updateResults();
}
protected void playMultiplayerRound(MultiplayerRound round) {
playMultiPlayerMatch(round);
updateResults(); // show points from byes
}
protected List<TournamentPlayer> getActivePlayers() {
List<TournamentPlayer> activePlayers = new ArrayList<>();
for (TournamentPlayer player : players.values()) {
if (!player.isEliminated()) {
activePlayers.add(player);
}
}
return activePlayers;
}
/**
*
*/
@Override
public void updateResults() {
for (TournamentPlayer player : players.values()) {
player.setResults("");
player.setPoints(0);
player.setStateInfo("");
}
for (Round round : rounds) {
for (TournamentPairing pair : round.getPairs()) {
Match match = pair.getMatch();
if (match != null && match.hasEnded()) {
TournamentPlayer tp1 = pair.getPlayer1();
TournamentPlayer tp2 = pair.getPlayer2();
MatchPlayer mp1 = match.getPlayer(pair.getPlayer1().getPlayer().getId());
MatchPlayer mp2 = match.getPlayer(pair.getPlayer2().getPlayer().getId());
// set player state if he finished the round
if (round.getRoundNumber() == rounds.size()) { // for elimination getRoundNumber = 0 so never true here
match.setTournamentRound(round.getRoundNumber());
if (tp1.getState() == TournamentPlayerState.DUELING) {
if (round.getRoundNumber() == getNumberRounds()) {
tp1.setState(TournamentPlayerState.FINISHED);
} else {
tp1.setState(TournamentPlayerState.WAITING);
}
}
if (tp2.getState() == TournamentPlayerState.DUELING) {
if (round.getRoundNumber() == getNumberRounds()) {
tp2.setState(TournamentPlayerState.FINISHED);
} else {
tp2.setState(TournamentPlayerState.WAITING);
}
}
}
// Add round result
tp1.setResults(addRoundResult(round.getRoundNumber(), pair, tp1, tp2));
tp2.setResults(addRoundResult(round.getRoundNumber(), pair, tp2, tp1));
// Add points
if ((!mp1.hasQuit() && mp1.getWins() > mp2.getWins()) || mp2.hasQuit()) {
tp1.setPoints(tp1.getPoints() + 3);
} else if ((!mp2.hasQuit() && mp1.getWins() < mp2.getWins()) || mp1.hasQuit()) {
tp2.setPoints(tp2.getPoints() + 3);
} else {
tp1.setPoints(tp1.getPoints() + 1);
tp2.setPoints(tp2.getPoints() + 1);
}
}
}
for (TournamentPlayer tp : round.getPlayerByes()) {
tp.setResults(new StringBuilder(tp.getResults()).append('R').append(round.getRoundNumber()).append(' ').append("Bye ").toString());
tp.setPoints(tp.getPoints() + 3);
}
}
}
private static String addRoundResult(int round, TournamentPairing pair, TournamentPlayer tournamentPlayer, TournamentPlayer opponentPlayer) {
StringBuilder playerResult = new StringBuilder(tournamentPlayer.getResults());
playerResult.append('R').append(round).append(' ');
playerResult.append(getMatchResultString(tournamentPlayer, opponentPlayer, pair.getMatch()));
return playerResult.toString();
}
private static String getMatchResultString(TournamentPlayer p1, TournamentPlayer p2, Match match) {
MatchPlayer mp1 = match.getPlayer(p1.getPlayer().getId());
MatchPlayer mp2 = match.getPlayer(p2.getPlayer().getId());
StringBuilder matchResult = new StringBuilder();
matchResult.append(p2.getPlayer().getName());
matchResult.append(" [").append(mp1.getWins());
if (mp1.hasQuit()) {
matchResult.append(mp1.getPlayer().hasIdleTimeout() ? "I" : (mp1.getPlayer().hasTimerTimeout() ? "T" : "Q"));
}
if (match.getDraws() > 0) {
matchResult.append('-').append(match.getDraws());
}
matchResult.append('-').append(mp2.getWins());
if (mp2.hasQuit()) {
matchResult.append(mp2.getPlayer().hasIdleTimeout() ? "I" : (mp2.getPlayer().hasTimerTimeout() ? "T" : "Q"));
}
matchResult.append("] ");
return matchResult.toString();
}
@Override
public boolean isDoneConstructing() {
for (TournamentPlayer player : this.players.values()) {
if (!player.isDoneConstructing()) {
return false;
}
}
return true;
}
@Override
public boolean allJoined() {
for (TournamentPlayer player : this.players.values()) {
if (!player.isJoined()) {
return false;
}
}
return true;
}
@Override
public void addTableEventListener(Listener<TableEvent> listener) {
tableEventSource.addListener(listener);
}
@Override
public void addPlayerQueryEventListener(Listener<PlayerQueryEvent> listener) {
playerQueryEventSource.addListener(listener);
}
@Override
public void fireConstructEvent(UUID playerId) {
playerQueryEventSource.construct(playerId, "Construct", getOptions().getLimitedOptions().getConstructionTime());
}
public void construct() {
tableEventSource.fireTableEvent(EventType.CONSTRUCT);
if (!isAbort()) {
for (final TournamentPlayer player : players.values()) {
player.setConstructing();
new Thread(
() -> player.getPlayer().construct(TournamentImpl.this, player.getDeck())
).start();
}
// add autosubmit trigger
synchronized (this) {
while (!isDoneConstructing()) {
try {
this.wait();
} catch (InterruptedException ex) {
}
}
}
}
nextStep();
}
protected void openBoosters() {
for (TournamentPlayer player : this.players.values()) {
player.setDeck(new Deck());
if (options.getLimitedOptions().getDraftCube() != null) {
DraftCube cube = options.getLimitedOptions().getDraftCube();
for (int i = 0; i < options.getLimitedOptions().getNumberBoosters(); i++) {
player.getDeck().getSideboard().addAll(cube.createBooster());
}
} else {
for (ExpansionSet set : sets) {
player.getDeck().getSideboard().addAll(set.createBooster());
}
}
}
resetBufferedCards();
nextStep();
}
public void resetBufferedCards() {
HashSet<ExpansionSet> setsDone = new HashSet<>();
for (ExpansionSet set : sets) {
if (!setsDone.contains(set)) {
set.removeSavedCards();
setsDone.add(set);
}
}
}
public void playMatch(TournamentPairing pair) {
options.getMatchOptions().getPlayerTypes().clear();
options.getMatchOptions().getPlayerTypes().add(pair.getPlayer1().getPlayerType());
options.getMatchOptions().getPlayerTypes().add(pair.getPlayer2().getPlayerType());
tableEventSource.fireTableEvent(EventType.START_MATCH, pair, options.getMatchOptions());
}
public void playMultiPlayerMatch(MultiplayerRound round) {
tableEventSource.fireTableEvent(EventType.START_MULTIPLAYER_MATCH, round, options.getMatchOptions());
}
public void end() {
endTime = new Date();
tableEventSource.fireTableEvent(EventType.END);
}
protected abstract void runTournament();
@Override
public Date getStartTime() {
return new Date(startTime.getTime());
}
@Override
public Date getEndTime() {
if (endTime == null) {
return null;
}
return new Date(endTime.getTime());
}
@Override
public TournamentType getTournamentType() {
return tournamentType;
}
@Override
public void setTournamentType(TournamentType tournamentType) {
this.tournamentType = tournamentType;
}
protected void winners() {
List<TournamentPlayer> winners = new ArrayList<>();
int pointsWinner = 1; // with less than 1 point you can't win
for (TournamentPlayer tournamentPlayer : this.getPlayers()) {
if (pointsWinner < tournamentPlayer.getPoints()) {
winners.clear();
winners.add(tournamentPlayer);
pointsWinner = tournamentPlayer.getPoints();
} else if (pointsWinner == tournamentPlayer.getPoints()) {
winners.add(tournamentPlayer);
}
}
// set winner state for the players with the most points > 0
for (TournamentPlayer tournamentPlayer : winners) {
tournamentPlayer.setStateInfo("Winner");
}
}
@Override
public void cleanUpOnTournamentEnd() {
for (TournamentPlayer tournamentPlayer : players.values()) {
tournamentPlayer.CleanUpOnTournamentEnd();
}
}
@Override
public boolean isAbort() {
return abort;
}
@Override
public void setAbort(boolean abort) {
this.abort = abort;
}
@Override
public String getTournamentState() {
return tournamentState;
}
@Override
public void setTournamentState(String tournamentState) {
this.tournamentState = tournamentState;
}
@Override
public Date getStepStartTime() {
if (stepStartTime != null) {
return new Date(stepStartTime.getTime());
}
return null;
}
@Override
public void setStartTime() {
this.startTime = new Date();
}
@Override
public void setStepStartTime(Date stepStartTime) {
this.stepStartTime = stepStartTime;
}
@Override
public void clearDraft() {
draft = null;
}
@Override
public Draft getDraft() {
return draft;
}
@Override
public TourneyProto toProto() {
TourneyProto.Builder tourneyBuilder = TourneyProto.newBuilder()
.setBoosterInfo(this.getBoosterInfo());
for (TournamentPlayer player : players.values()) {
TournamentPlayer replacedPlayer = player.getReplacedTournamentPlayer();
if (replacedPlayer != null) {
player = replacedPlayer;
}
tourneyBuilder.addPlayersBuilder().mergeFrom(player.toProto());
}
for (Round round : rounds) {
TourneyRoundProto.Builder roundBuilder = tourneyBuilder.addRoundsBuilder()
.setRound(round.getRoundNumber());
for (TournamentPairing pair : round.getPairs()) {
Match match = pair.getMatch();
if (match != null && match.hasEnded()) {
MatchProto.Builder matchBuilder = roundBuilder.addMatchesBuilder()
.setName(match.getName())
.setGameType(match.getOptions().getGameType())
.setDeckType(match.getOptions().getDeckType())
.setGames(match.getNumGames())
.setDraws(match.getDraws())
.addPlayers(matchToProto(match, pair.getPlayer1()))
.addPlayers(matchToProto(match, pair.getPlayer2()))
.setMatchOptions(match.getOptions().toProto())
.setEndTimeMs((match.getEndTime() != null ? match.getEndTime() : new Date()).getTime());
}
}
for (TournamentPlayer tp : round.getPlayerByes()) {
roundBuilder.addByes(tp.getPlayer().getName());
}
}
return tourneyBuilder.build();
}
private MatchPlayerProto matchToProto(Match match, TournamentPlayer player) {
MatchPlayer matchPlayer = match.getPlayer(player.getPlayer().getId());
MatchQuitStatus quit = !matchPlayer.hasQuit() ? MatchQuitStatus.NO_MATCH_QUIT :
matchPlayer.getPlayer().hasIdleTimeout() ? MatchQuitStatus.IDLE_TIMEOUT :
matchPlayer.getPlayer().hasTimerTimeout() ? MatchQuitStatus.TIMER_TIMEOUT :
MatchQuitStatus.QUIT;
return MatchPlayerProto.newBuilder()
.setName(player.getPlayer().getName())
.setHuman(player.getPlayer().isHuman())
.setWins(matchPlayer.getWins())
.setQuit(quit)
.build();
}
}