package org.royaldev.thehumanity.game.round;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Lists;
import com.google.common.collect.Multiset;
import com.google.common.collect.Multisets;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.kitteh.irc.client.library.util.Format;
import org.royaldev.thehumanity.cards.play.Play;
import org.royaldev.thehumanity.cards.types.BlackCard;
import org.royaldev.thehumanity.cards.types.WhiteCard;
import org.royaldev.thehumanity.game.HouseRule;
import org.royaldev.thehumanity.game.TheHumanityGame;
import org.royaldev.thehumanity.player.TheHumanityPlayer;
import org.royaldev.thehumanity.util.Snapshottable;
import org.royaldev.thehumanity.util.json.JSONSerializable;
import xyz.cardstock.cardstock.configuration.Server;
import xyz.cardstock.cardstock.extensions.enums.ExtensionsKt;
import xyz.cardstock.cardstock.interfaces.states.State;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Represents a single round of the game.
*/
public class CurrentRound implements CAHRound, JSONSerializable, Snapshottable<RoundSnapshot> {
private final TheHumanityGame game;
private final int number;
private final BlackCard blackCard;
private final TheHumanityPlayer czar;
private final Set<TheHumanityPlayer> skippedPlayers = Collections.synchronizedSet(new HashSet<>());
private final List<Play> plays = Collections.synchronizedList(new ArrayList<>());
private final Multiset<Play> votes = HashMultiset.create();
private final Set<TheHumanityPlayer> voters = new HashSet<>();
private final List<Function1<State, Unit>> stateListeners = Lists.newArrayList();
private ScheduledFuture reminderTask;
private State state = ExtensionsKt.toState(RoundState.IDLE);
private Play winningPlay;
private long startTime, endTime;
private RoundEndCause endCause = RoundEndCause.NOT_ENDED;
/**
* Creates a new round for the given game.
*
* @param game TheHumanityGame the round belongs to
* @param number CAHRound number
* @param blackCard The black card used for this round
* @param czar The czar, or null if playing a mode with no czar
*/
public CurrentRound(@NotNull final TheHumanityGame game, final int number, @NotNull final BlackCard blackCard, @Nullable final TheHumanityPlayer czar) {
Preconditions.checkNotNull(game, "game was null");
Preconditions.checkNotNull(blackCard, "blackCard was null");
this.game = game;
this.number = number;
this.blackCard = blackCard;
this.czar = czar;
this.setUpListeners();
}
/**
* Makes a reminder for the czar. The reminder will ping the czar after 45 seconds, and continually every 22.5
* seconds after the initial period. This will return null if there is no czar. The task is canceled by a listener,
* which is fired when the czar speaks.
*
* @return ScheduledFuture or null
*/
@Nullable
private ScheduledFuture makeReminderTask() {
final TheHumanityPlayer czar = this.getCzar();
if (czar == null) {
return null;
}
return this.getGame().getHumanity().getThreadPool().scheduleAtFixedRate(
() -> this.getGame().getChannel().sendMessage(czar.getUser().getNick() + ": Wake up! You're the czar!"),
45000L,
22500L,
TimeUnit.MILLISECONDS
);
}
/**
* Processes tasks that house rules need to complete.
*/
private void processHouseRules() {
if (!this.state.getUniqueName().equals(RoundState.WAITING_FOR_PLAYERS.name())) return;
if (this.game.hasHouseRule(HouseRule.RANDO_CARDRISSIAN)) {
final List<WhiteCard> randoPlay = new ArrayList<>();
for (int i = 0; i < this.blackCard.getBlanks(); i++) {
randoPlay.add(this.game.getDeck().getRandomWhiteCard(null));
}
this.addPlay(new Play(this.game.getRandoCardrissian(), randoPlay));
}
if (this.game.hasHouseRule(HouseRule.PACKING_HEAT)) {
if (this.getBlackCard().getBlanks() > 1) {
this.game.getPlayers().stream().filter(p -> !p.equals(this.getCzar())).forEach(p -> p.getHand().add(this.game.getDeck().getRandomWhiteCard(null)));
}
}
}
/**
* Processes various tasks for each new stage the round enters.
*/
private void setUpListeners() {
final Server server = this.getGame().getHumanity().getClientServerMap().get(this.getGame().getChannel().getClient());
Preconditions.checkNotNull(server);
this.stateListeners.add(state -> {
if (!this.getGame().hasEnoughPlayers() || !state.getUniqueName().equals(RoundState.WAITING_FOR_PLAYERS.name())) {
return Unit.INSTANCE;
}
this.startTime = System.currentTimeMillis();
this.processHouseRules();
this.game.showCards();
return Unit.INSTANCE;
});
this.stateListeners.add(state -> {
if (!this.getGame().hasEnoughPlayers() || !state.getUniqueName().equals(RoundState.WAITING_FOR_CZAR.name())) {
return Unit.INSTANCE;
}
Collections.shuffle(this.plays);
if (this.game.hasHouseRule(HouseRule.GOD_IS_DEAD)) {
this.displayPlays();
this.getGame().sendMessage("Send " + Format.BOLD + server.getPrefix() + "pick" + Format.RESET + " followed by the number you think should win.");
}
final TheHumanityPlayer czar = this.getCzar();
if (czar == null) return Unit.INSTANCE;
this.displayPlays();
this.getGame().sendMessage(Format.BOLD + czar.getUser().getNick() + Format.RESET + " is picking a winner.");
czar.getUser().sendNotice("Send " + Format.BOLD + server.getPrefix() + "pick" + Format.RESET + " followed by the number you think should win.");
this.reminderTask = this.makeReminderTask();
return Unit.INSTANCE;
});
this.stateListeners.add(state -> {
if (!this.getGame().hasEnoughPlayers() || !state.getUniqueName().equals(RoundState.ENDED.name())) {
return Unit.INSTANCE;
}
this.endTime = System.currentTimeMillis();
this.getGame().startNextRound();
return Unit.INSTANCE;
});
}
/**
* Adds a play to this round.
*
* @param play Play to add
*/
public void addPlay(@NotNull final Play play) {
Preconditions.checkNotNull(play, "play was null");
synchronized (this.plays) {
this.plays.add(play);
}
play.getWhiteCards().forEach(play.getPlayer().getHand()::remove);
if (this.hasAllPlaysMade()) this.advanceState();
}
/**
* Adds a vote for a choice in the God is Dead house rule mode.
*
* @param player Player voting
* @param index Number of the play that is being voted for
* @return true if vote was successful, false if not
*/
public boolean addVote(@NotNull final TheHumanityPlayer player, int index) {
Preconditions.checkNotNull(player, "player was null");
if (this.hasVoted(player)) return false;
index--;
if (index < 0 || index >= this.getPlays().size()) return false;
this.voters.add(player);
final Play p = this.getPlays().get(index);
this.votes.add(p);
if (this.votes.size() >= this.getGame().getPlayers().size()) {
final Play winner = this.getMostVoted();
final int winningIndex = this.plays.indexOf(winner);
this.chooseWinningPlay(winningIndex + 1);
}
return true;
}
/**
* Cancels the reminder for the czar, if one has been started and it is not canceled or finished. This is always
* safe to call.
*/
public void cancelReminderTask() {
if (this.reminderTask == null || this.reminderTask.isCancelled() || this.reminderTask.isDone()) {
return;
}
this.reminderTask.cancel(true);
}
/**
* Chooses the winning play for this round. This ends the round.
*
* @param index Index of the winning play (starting at 1)
*/
public void chooseWinningPlay(int index) {
this.cancelReminderTask();
index--;
if (index < 0 || index >= this.getPlays().size()) return;
final Play p = this.winningPlay = this.getPlays().get(index);
p.getPlayer().addWin(this.getBlackCard());
this.getGame().sendMessage(Format.RESET + "Play " + Format.BOLD + (index + 1) + Format.RESET + " by " + Format.BOLD + p.getPlayer().getUser().getNick() + Format.RESET + " wins!");
this.setEndCause(RoundEndCause.CZAR_CHOSE_WINNER);
this.advanceState();
}
/**
* Displays all the plays made this round (without player names).
*/
public void displayPlays() {
for (int i = 0; i < this.getPlays().size(); i++) {
final Play p = this.getPlays().get(i);
this.getGame().sendMessage((i + 1) + ". " + this.getBlackCard().fillInBlanks(p));
}
}
/**
* Gets the plays that have been made by players still taking part in the game. Modifying this list will not change
* it in the round.
*
* @return Cloned list of active-player plays
*/
@NotNull
public List<Play> getActivePlayerPlays() { // TODO: Rename method?
return this.plays.stream().filter(p -> this.getGame().getPlayers().contains(p.getPlayer())).collect(Collectors.toList());
}
/**
* Gets the black card for this round.
*
* @return Black card
*/
@Override
@NotNull
public BlackCard getBlackCard() {
return this.blackCard;
}
/**
* Gets the czar for this round. This may change if the czar leaves.
*
* @return Czar
*/
@Override
@Nullable
public TheHumanityPlayer getCzar() {
return this.czar;
}
/**
* Gets the play that has the highest amount of votes in the God is Dead house rule mode. Ties are handled by
* returning the play that was first in the iteration.
*
* @return Play that had the highest amount of votes
*/
@Override
public Play getMostVoted() {
return Multisets.copyHighestCountFirst(this.votes).iterator().next();
}
/**
* Gets the number of this round.
*
* @return Number
*/
@Override
public int getNumber() {
return this.number;
}
/**
* Gets all of the plays that have been made this round. This is a copy of the list, not the real list. Modifying it
* will not change it in the round.
*
* @return Cloned list of plays
*/
@Override
@NotNull
public List<Play> getPlays() {
synchronized (this.plays) {
return new ArrayList<>(this.plays);
}
}
/**
* Gets all the skipped players for this round. Returns the actual list. Modifying the list returned will modify the
* round.
*
* @return List of skipped players
*/
@Override
@NotNull
public Set<TheHumanityPlayer> getSkippedPlayers() {
synchronized (this.skippedPlayers) {
return this.skippedPlayers;
}
}
@Override
@NotNull
public State getState() {
return this.state;
}
@Override
public void setState(@NotNull final State state) {
Preconditions.checkNotNull(state);
this.state = state;
}
@NotNull
public RoundEndCause getEndCause() {
return this.endCause;
}
public void setEndCause(@NotNull final RoundEndCause endCause) {
Preconditions.checkNotNull(endCause, "endCause was null");
this.endCause = endCause;
}
/**
* Gets the game that this round is associated with.
*
* @return TheHumanityGame
*/
@NotNull
public TheHumanityGame getGame() {
return this.game;
}
/**
* Checks if this round has all necessary plays made to advance the stage (meaning that every player has played).
* Skipped players are ignored.
*
* @return true if all plays have been made, false if otherwise
*/
public boolean hasAllPlaysMade() {
final List<TheHumanityPlayer> usersTakingPart = new ArrayList<>(this.getGame().getPlayers());
usersTakingPart.removeAll(this.getSkippedPlayers());
return this.getActivePlayerPlays().size() >= usersTakingPart.size() - (this.getGame().hasHouseRule(HouseRule.GOD_IS_DEAD) ? 0 : 1);
}
/**
* Checks if the given player has played in this round.
*
* @param p Player to check
* @return true if played,false if otherwise
*/
public boolean hasPlayed(@NotNull final TheHumanityPlayer p) {
Preconditions.checkNotNull(p, "p was null");
return this.plays.stream().anyMatch(play -> play.getPlayer().equals(p));
}
public boolean hasVoted(@NotNull final TheHumanityPlayer player) {
Preconditions.checkNotNull(player, "player was null");
return this.voters.contains(player);
}
/**
* Checks if the given player is skipped.
*
* @param p Player to check
* @return true if skipped, false if otherwise
*/
public boolean isSkipped(@NotNull final TheHumanityPlayer p) {
Preconditions.checkNotNull(p, "p was null");
return this.getSkippedPlayers().contains(p);
}
/**
* Returns all the played cards back to the hands of the players.
*/
public void returnCards() {
this.getPlays().forEach(p -> p.getWhiteCards().forEach(p.getPlayer().getHand()::add));
}
public void setEndTime(final long endTime) {
this.endTime = endTime;
}
public void setStartTime(final long startTime) {
this.startTime = startTime;
}
/**
* Skips a player. A skipped player will not need to submit a play in order for the stage to advance.
*
* @param p Player to skip
*/
public boolean skip(@NotNull final TheHumanityPlayer p) {
Preconditions.checkNotNull(p, "p was null");
if (this.isSkipped(p)) return false;
// If the total amount of players less the skipped players is less than the amount needed to play, don't skip.
// However, if this person is the czar, it's fine.
if (!p.equals(this.getCzar()) && this.getGame().getPlayers().size() - this.skippedPlayers.size() < 3) {
return false;
}
synchronized (this.skippedPlayers) {
this.skippedPlayers.add(p);
}
if (this.getState().getUniqueName().equals(RoundState.WAITING_FOR_PLAYERS.name())) {
if (this.hasAllPlaysMade()) this.advanceState();
} else if (this.getState().getUniqueName().equals(RoundState.WAITING_FOR_CZAR.name())) {
if (p.equals(this.getCzar())) {
this.getGame().sendMessage(Format.BOLD + "The czar has been skipped!" + Format.RESET + " Returning your cards and starting a new round.");
this.returnCards();
this.setEndCause(RoundEndCause.CZAR_SKIPPED);
this.advanceState();
}
}
return true;
}
@NotNull
@Override
public RoundSnapshot takeSnapshot() {
return new RoundSnapshot(
this.getNumber(),
this.startTime,
this.endTime,
this.getBlackCard().getText(),
this.getCzar() == null ? null : this.getCzar().getUser().getNick(),
this.winningPlay == null ? null : this.winningPlay.getPlayer().getUser().getNick(),
this.endCause.name(),
this.getPlays().stream().map(Play::takeSnapshot).collect(Collectors.toList()),
this.getGame().getPlayers().stream().map(p -> p.getUser().getNick()).collect(Collectors.toSet()),
this.getSkippedPlayers().stream().map(p -> p.getUser().getNick()).collect(Collectors.toSet()),
this.getGame().getHistoricPlayers().stream().collect(Collectors.toMap(p -> p.getUser().getNick(), p -> this.winningPlay == null ? 0 : p.equals(this.winningPlay.getPlayer()) ? 1 : 0)),
this.votes.stream().collect(Collectors.toMap(play -> play.getPlayer().getUser().getNick(), this.votes::count))
);
}
@NotNull
@Override
public String toJSON() {
return this.takeSnapshot().toJSON();
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("number", this.number)
.add("game", this.game)
.add("blackCard", this.blackCard)
.add("czar", this.czar)
.add("state", this.state)
.toString();
}
@NotNull
@Override
public List<Function1<State, Unit>> getStateListeners() {
return this.stateListeners;
}
@Override
public void advanceState() {
final State next = this.getState().getNext();
if (next == null) return;
this.setState(next);
this.runStateListeners();
}
@Override
public void runStateListeners() {
final State currentState = this.getState();
this.stateListeners.forEach(listener -> listener.invoke(currentState));
}
}