package org.royaldev.thehumanity.game;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.kitteh.irc.client.library.element.Channel;
import org.kitteh.irc.client.library.element.User;
import org.kitteh.irc.client.library.util.Format;
import org.royaldev.thehumanity.TheHumanity;
import org.royaldev.thehumanity.cards.Deck;
import org.royaldev.thehumanity.cards.packs.CAHCardPack;
import org.royaldev.thehumanity.cards.types.BlackCard;
import org.royaldev.thehumanity.cards.types.WhiteCard;
import org.royaldev.thehumanity.exceptions.MissingCzarException;
import org.royaldev.thehumanity.game.round.CAHRound;
import org.royaldev.thehumanity.game.round.CAHRound.RoundEndCause;
import org.royaldev.thehumanity.game.round.CAHRound.RoundState;
import org.royaldev.thehumanity.game.round.CurrentRound;
import org.royaldev.thehumanity.game.round.RoundSnapshot;
import org.royaldev.thehumanity.player.TheHumanityPlayer;
import org.royaldev.thehumanity.util.DescendingValueComparator;
import org.royaldev.thehumanity.util.FakeUser;
import org.royaldev.thehumanity.util.Snapshottable;
import org.royaldev.thehumanity.util.json.JSONSerializable;
import xyz.cardstock.cardstock.configuration.Server;
import xyz.cardstock.cardstock.extensions.channel.ExtensionsKt;
import xyz.cardstock.cardstock.extensions.channel.UserModeData;
import xyz.cardstock.cardstock.games.Game;
import xyz.cardstock.cardstock.interfaces.states.State;
import xyz.cardstock.cardstock.players.hands.PlayerHand;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public class TheHumanityGame extends Game<TheHumanityPlayer> implements JSONSerializable, Snapshottable<GameSnapshot> {
private final TheHumanity humanity;
/**
* A list of all the players that have ever played in this game.
*/
private final List<TheHumanityPlayer> historicPlayers = Collections.synchronizedList(new ArrayList<>());
private final List<HouseRule> houseRules = Lists.newArrayList();
private final List<RoundSnapshot> previousRounds = Lists.newArrayList();
private final TheHumanityPlayer randoCardrissian = new TheHumanityPlayer(new FakeUser("Rando Cardrissian"));
private final List<Function1<State, Unit>> stateListeners = Lists.newArrayList();
private State state = xyz.cardstock.cardstock.extensions.enums.ExtensionsKt.toState(GameStatus.IDLE);
private Channel channel;
private Deck deck;
private CurrentRound currentRound = null;
private TheHumanityPlayer host = null;
private ScheduledFuture countdownTask;
private boolean hostWasVoiced = false;
private long startTime, endTime;
private GameEndCause endCause = GameEndCause.NOT_ENDED;
public TheHumanityGame(@NotNull final TheHumanity humanity, @NotNull final Channel channel) {
super(humanity, channel);
Preconditions.checkNotNull(humanity, "humanity was null");
Preconditions.checkNotNull(channel, "channel was null");
this.humanity = humanity;
this.channel = channel;
this.addHouseRule(HouseRule.REBOOTING_THE_UNIVERSE);
this.setUpListeners();
}
/**
* Processes adding a house rule. Useful for setting up various aspects of the game to work with the rule.
*
* @param hr Rule being added
*/
private void processAddingHouseRule(@NotNull final HouseRule hr) {
Preconditions.checkNotNull(hr, "hr was null");
switch (hr) {
case RANDO_CARDRISSIAN:
this.historicPlayers.add(this.randoCardrissian);
break;
}
}
/**
* Adds a CardPack to this TheHumanityGame.
*
* @param cp CardPack to add.
* @return true if pack was added, false if otherwise
*/
public boolean addCardPack(@NotNull final CAHCardPack cp) {
Preconditions.checkNotNull(cp, "cp was null");
return this.deck.addCardPack(cp);
}
/**
* Adds a house rule to the game.
*
* @param rule Rule to add
* @return true if rule was added, false if otherwise
*/
public boolean addHouseRule(@NotNull final HouseRule rule) {
Preconditions.checkNotNull(rule, "rule was null");
if (this.hasHouseRule(rule)) return false;
this.processAddingHouseRule(rule);
return this.houseRules.add(rule);
}
/**
* Adds a player to the game. If there are not enough white cards to deal this player in, the game will end. If this
* Player has previously played, all cards and points will be restored to it.
*
* @param player Player to add to the game
*/
public void addPlayer(@NotNull final TheHumanityPlayer player) {
Preconditions.checkNotNull(player, "player was null");
if (this.hasPlayer(player)) return;
if (!this.setOldUserData(player)) {
synchronized (this.get_players()) {
this.get_players().add(player);
}
synchronized (this.historicPlayers) {
this.historicPlayers.add(player);
}
}
this.update();
final int totalCards = this.getDeck().getCardPacks().stream().mapToInt(cp -> cp.getWhiteCards().size()).sum();
if (this.get_players().size() * 10 >= totalCards) {
this.sendMessage(Format.BOLD + "Not enough white cards to play!");
this.stop(GameEndCause.NOT_ENOUGH_WHITE_CARDS);
return;
}
this.deal(player);
if (!this.state.getUniqueName().equals(GameStatus.JOINING.name())) this.showCards(player);
this.sendMessage(Format.BOLD + player.getUser().getNick() + Format.RESET + " has joined the game!");
}
/**
* Reformats a message to ensure no User in IRC is pinged by this message.
*
* @param message Message to reformat
* @return Reformatted message
*/
@NotNull
public String antiPing(@NotNull String message) {
Preconditions.checkNotNull(message, "message was null");
for (final String nickname : this.channel.getNicknames()) {
if (nickname.length() <= 1) continue;
message = message.replace(nickname, nickname.substring(0, 1) + "\u200b" + nickname.substring(1));
}
return message;
}
/**
* Creates a Player from a User.
*
* @param u User to create Player from
* @return Player
*/
@Nullable
public TheHumanityPlayer createPlayer(@NotNull final User u) {
Preconditions.checkNotNull(u, "u was null");
if (this.hasPlayer(u.getNick())) return this.getPlayer(u.getNick());
final TheHumanityPlayer p = new TheHumanityPlayer(u);
this.addPlayer(p);
return p;
}
/**
* Deals cards to the given Player until it has ten in its hand.
*
* @param player Player to deal to
*/
public void deal(@NotNull final TheHumanityPlayer player) {
Preconditions.checkNotNull(player, "player was null");
final PlayerHand<WhiteCard> hand = player.getHand();
while (hand.size() < 10) hand.add(this.getDeck().getRandomWhiteCard(null));
}
/**
* Deals to each Player in the game until they all have ten cards.
*/
public void deal() {
synchronized (this.getPlayers()) {
this.getPlayers().stream().forEach(this::deal);
}
}
/**
* Creates a String with the current card counts.
*
* @return String
*/
@NotNull
public String getCardCounts() {
return Format.BOLD + "Card counts: "
+ Format.RESET + this.getDeck().getUnusedBlackCardCount() + " unused/" + this.getDeck().getBlackCardCount() + " black cards, "
+ this.getDeck().getUnusedWhiteCardCount() + " unused/" + this.getDeck().getWhiteCardCount() + " white cards";
}
/**
* Sets the Channel that this TheHumanityGame is in.
*
* @param channel Channel
*/
private void setChannel(@NotNull final Channel channel) {
Preconditions.checkNotNull(channel, "channel was null");
this.channel = channel;
}
/**
* Gets the current CAHRound.
*
* @return CAHRound
*/
@Nullable
public CurrentRound getCurrentRound() {
return this.currentRound;
}
/**
* Gets the Deck being used for this game.
*
* @return Deck
*/
@NotNull
public Deck getDeck() {
return this.deck;
}
@NotNull
public GameEndCause getEndCause() {
return this.endCause;
}
public void setEndCause(@NotNull final GameEndCause endCause) {
Preconditions.checkNotNull(endCause, "endCause was null");
this.endCause = endCause;
}
public long getEndTime() {
return this.endTime;
}
/**
* Returns the internal list of all players that have ever played in this game.
*
* @return List
*/
@NotNull
public List<TheHumanityPlayer> getHistoricPlayers() {
return this.historicPlayers;
}
/**
* Gets the host of this game.
*
* @return Player
*/
@NotNull
public TheHumanityPlayer getHost() {
return this.host;
}
/**
* Sets the host of this game. This player will be given voice.
*
* @param host Player to set as host
*/
public void setHost(@NotNull final TheHumanityPlayer host) {
Preconditions.checkNotNull(host, "host was null");
this.host = host;
this.hostWasVoiced = this.humanity.hasChannelMode(this.channel, this.getHost().getUser(), 'v');
if (!this.hostWasVoiced) {
ExtensionsKt.userMode(this.getChannel(), new UserModeData(true, 'v', this.getHost().getUser()));
}
this.showHost();
}
/**
* Gets a list of {@link HouseRule HouseRules} being used for this game.
*
* @return House rules
*/
@NotNull
public List<HouseRule> getHouseRules() {
return Collections.unmodifiableList(this.houseRules);
}
/**
* Gets the instance of the bot that this game is running under.
*
* @return TheHumanity
*/
@NotNull
public TheHumanity getHumanity() {
return this.humanity;
}
/**
* Gets a Player for the given User. If there is no corresponding Player, null is returned.
*
* @param u User to get Player for
* @return Corresponding Player
*/
@Nullable
public TheHumanityPlayer getPlayer(final User u) {
if (u == null) return null;
return this.getPlayers().stream()
.filter(p -> this.humanity.usersMatch(u, p.getUser()))
.findFirst()
.orElse(null);
}
/**
* Gets a player in this game from the list of players.
*
* @param p Player to get
* @return Player or null
*/
@Nullable
public TheHumanityPlayer getPlayer(final TheHumanityPlayer p) {
if (p == null) return null;
final int index = this.getPlayers().indexOf(p);
if (index < 0) return null;
return this.getPlayers().get(index);
}
/**
* Gets a Player in this game based on its name.
*
* @param name Name of the Player to retrieve
* @return Player or null
*/
@Nullable
public TheHumanityPlayer getPlayer(final String name) {
if (name == null) return null;
return this.getPlayer(
this.channel.getUsers().stream()
.filter(user -> user.getNick().equalsIgnoreCase(name))
.findFirst()
.orElse(null)
);
}
/**
* Gets an unmodifiable list of previous rounds as snapshots.
*
* @return Unmodifiable list of previous round snapshots
*/
public List<RoundSnapshot> getPreviousRounds() {
return Collections.unmodifiableList(this.previousRounds);
}
/**
* Gets the Player that represents Rando Cardrissian. If Rando Cardrissian is not enabled, this will return null.
*
* @return Player or null
*/
@Nullable
public TheHumanityPlayer getRandoCardrissian() {
if (!this.hasHouseRule(HouseRule.RANDO_CARDRISSIAN)) return null;
return this.randoCardrissian;
}
public long getStartTime() {
return this.startTime;
}
/**
* Checks to see if the game has enough players to continue. If it does not, the game will end.
*
* @return true if enough players, false if otherwise
*/
public boolean hasEnoughPlayers() {
if (this.getPlayers().size() < 3) {
this.sendMessage(Format.BOLD + "Not enough players to continue!");
this.stop(GameEndCause.NOT_ENOUGH_PLAYERS);
return false;
}
return true;
}
/**
* Convenience method to check if this game has a house rule in effect.
*
* @param rule Rule to check
* @return true if the rule is being used, false if otherwise
*/
public boolean hasHouseRule(final HouseRule rule) {
return this.getHouseRules().contains(rule);
}
/**
* Checks to see if the given player name is in this game.
*
* @param name Name to check
* @return true if name is in the game, false if otherwise
*/
public boolean hasPlayer(final String name) {
return this.getPlayer(name) != null;
}
/**
* Checks to see if the given Player is in this game.
*
* @param p Player to check
* @return true if player is in the game, false if otherwise
*/
public boolean hasPlayer(final TheHumanityPlayer p) {
return this.getPlayers().contains(p);
}
/**
* Devoices the current host and sets a new host (voicing him).
*/
public void nextHost() {
if (this.host != null && !this.hostWasVoiced) {
ExtensionsKt.userMode(this.getChannel(), new UserModeData(false, 'v', this.host.getUser()));
this.host = null;
}
synchronized (this.getPlayers()) {
if (this.getPlayers().size() < 1) return; // should never happen without the game stopping
this.setHost(this.getPlayers().get(0));
}
}
public void setUpListeners() {
this.stateListeners.add(state -> {
if (!state.getUniqueName().equals(GameStatus.JOINING.name())) return Unit.INSTANCE;
this.countdownTask = this.humanity.getThreadPool().scheduleAtFixedRate(new GameCountdown(), 0L, 15L, TimeUnit.SECONDS);
this.startTime = System.currentTimeMillis();
final StringBuilder sb = new StringBuilder();
sb.append(Format.BOLD).append("Card packs for this game:").append(Format.RESET).append(" ");
this.getDeck().getCardPacks().forEach(cp -> sb.append(cp.getName()).append(", "));
this.sendMessage(Format.BOLD + "A new game is starting!");
this.sendMessage(sb.toString().substring(0, sb.length() - 2));
this.showCardCounts();
final Server server = this.humanity.getClientServerMap().get(this.channel.getClient());
Preconditions.checkNotNull(server);
this.sendMessage("Use " + Format.BOLD + server.getPrefix() + "join" + Format.RESET + " to join.");
return Unit.INSTANCE;
});
this.stateListeners.add(state -> {
if (!state.getUniqueName().equals(GameStatus.PLAYING.name())) return Unit.INSTANCE;
if (!this.hasEnoughPlayers()) return Unit.INSTANCE;
this.startNextRound();
return Unit.INSTANCE;
});
}
public void startNextRound() {
final CurrentRound currentRound = this.getCurrentRound();
final boolean hadRound = currentRound != null;
if (hadRound) {
this.previousRounds.add(currentRound.takeSnapshot());
this.showScores();
this.showCardCounts();
}
int index = !hadRound ? 0 : this.getPlayers().indexOf(currentRound.getCzar()) + 1;
if (index >= this.getPlayers().size()) index = 0;
BlackCard blackCard = null;
do {
if (blackCard != null) {
this.sendMessage("Black card " + Format.BOLD + blackCard.getText() + Format.RESET + " was skipped because it is invalid.");
}
blackCard = this.getDeck().getRandomBlackCard();
if (blackCard == null) {
this.sendMessage(" ");
this.sendMessage(Format.BOLD + "There are no more black cards!");
this.stop(GameEndCause.RAN_OUT_OF_BLACK_CARDS);
return;
}
} while (blackCard.getBlanks() > 10 || blackCard.getBlanks() < 1);
if (hadRound) currentRound.cancelReminderTask();
this.currentRound = new CurrentRound(this, !hadRound ? 1 : currentRound.getNumber() + 1, blackCard, this.hasHouseRule(HouseRule.GOD_IS_DEAD) ? null : this.getPlayers().get(index));
this.deal();
this.sendMessage(" ");
this.sendMessage(Format.BOLD + "Round " + this.getCurrentRound().getNumber() + Format.RESET + "!");
if (!this.hasHouseRule(HouseRule.GOD_IS_DEAD)) {
final TheHumanityPlayer czar = this.getCurrentRound().getCzar();
if (czar == null) {
throw new MissingCzarException();
}
this.sendMessage(Format.BOLD + czar.getUser().getNick() + Format.RESET + " is the card czar.");
}
this.sendMessage(Format.BOLD + this.getCurrentRound().getBlackCard().getText());
this.getCurrentRound().advanceState();
}
/**
* Removes a CardPack from this TheHumanityGame. If sweep is true, any cards in players' hands will be removed if they belonged
* to the removed pack. If cards are removed, new cards will be dealt, and the affected players will have their
* hands shown to them.
*
* @param cp CardPack to remove
* @param sweep Remove cards in hands?
* @return true if the pack was removed, false if otherwise
*/
public boolean removeCardPack(final CAHCardPack cp, final boolean sweep) {
if (!this.deck.removeCardPack(cp)) return false;
if (!sweep) return true;
this.historicPlayers.forEach(p -> {
final int original = p.getHand().hashCode();
cp.getWhiteCards().forEach(p.getHand()::remove);
if (original == p.getHand().hashCode()) return;
this.deal(p);
p.getUser().sendNotice("Your hand has changed as a result of card pack changes. Here's your new hand!");
this.showCards(p);
});
return true;
}
/**
* Removes a house rule from the game.
*
* @param rule Rule to remove
* @return true if rule was removed, false if otherwise
*/
public boolean removeHouseRule(final HouseRule rule) {
return this.hasHouseRule(rule) && this.houseRules.remove(rule);
}
/**
* Removes a Player from this game. If there are not enough Players to continue, the game will end.
*
* @param p Player to remove
*/
@Override
public void removePlayer(@NotNull final TheHumanityPlayer p) {
Preconditions.checkNotNull(p, "p was null");
synchronized (this.get_players()) {
if (!this.get_players().remove(p)) return;
}
this.sendMessage(Format.BOLD + p.getUser().getNick() + Format.RESET + " has left the game.");
if (this.host.equals(p)) this.nextHost();
this.update();
if (this.getCurrentRound() != null) {
if (!this.hasEnoughPlayers()) return;
if (p.equals(this.getCurrentRound().getCzar())) {
this.sendMessage(Format.BOLD + "The czar has left!" + Format.RESET + " Returning your cards and starting a new round.");
this.getCurrentRound().returnCards();
this.getCurrentRound().setEndCause(RoundEndCause.CZAR_LEFT);
this.getCurrentRound().advanceState();
return;
}
if (this.getCurrentRound().hasAllPlaysMade() && this.getCurrentRound().getState().getUniqueName().equals(RoundState.WAITING_FOR_PLAYERS.name())) {
this.getCurrentRound().advanceState();
}
}
}
/**
* Removes a Player from this game given his name.
*
* @param name Name of Player to remove
*/
public void removePlayer(@NotNull final String name) {
Preconditions.checkNotNull(name, "name was null");
final TheHumanityPlayer player = this.getPlayer(name);
if (player != null) {
this.removePlayer(player);
}
}
/**
* Adds old data about a player to a rejoining player.
*
* @param newPlayer Player that is rejoining
* @return true if data was applied, false if otherwise
*/
public boolean setOldUserData(final TheHumanityPlayer newPlayer) {
final TheHumanityPlayer oldPlayer;
synchronized (this.historicPlayers) {
if (this.historicPlayers.contains(newPlayer)) {
oldPlayer = this.historicPlayers.get(this.historicPlayers.indexOf(newPlayer));
} else return false;
}
final PlayerHand<WhiteCard> hand = newPlayer.getHand();
hand.forEach(hand::remove);
oldPlayer.getHand().forEach(hand::add);
newPlayer.clearWins();
oldPlayer.getWins().forEach(newPlayer::addWin);
synchronized (this.get_players()) {
this.get_players().add(oldPlayer);
}
return true;
}
/**
* Displays the current card counts.
*/
public void showCardCounts() {
this.sendMessage(this.getCardCounts());
}
private String mask(@NotNull final User user) {
Preconditions.checkNotNull(user, "user was null");
return user.getNick() + "!" + user.getUserString() + "@" + user.getHost();
}
/**
* Shows a Player his cards.
*
* @param p Player to show cards to
*/
public void showCards(@NotNull final TheHumanityPlayer p) {
Preconditions.checkNotNull(p, "p was null");
final StringBuilder sb = new StringBuilder();
final List<String> toSend = Lists.newArrayList();
final PlayerHand<WhiteCard> hand = p.getHand();
for (int i = 0; i < hand.size(); i++) {
final WhiteCard wc = hand.get(i);
final String toAppend = Format.BOLD.toString() + (i + 1) + ". " + Format.RESET + wc.getText() + " ";
if ((":" + this.mask(this.getChannel().getClient().getUser().get()) + " NOTICE " + p.getUser().getNick() + " :").length() + sb.length() + toAppend.length() > 510) {
toSend.add(sb.toString());
sb.setLength(0);
}
sb.append(toAppend);
}
toSend.add(sb.toString());
toSend.forEach(p.getUser()::sendNotice);
}
/**
* Shows each Player his hand.
*/
public void showCards() {
final CAHRound currentRound = this.getCurrentRound();
if (currentRound == null) return;
this.getPlayers().stream().filter(p -> !p.equals(currentRound.getCzar())).forEach(this::showCards);
}
/**
* Sends a message to the game channel, declaring who the host is.
*/
public void showHost() {
this.sendMessage("The host is " + Format.BOLD + this.host.getUser().getNick() + Format.RESET + ".");
}
/**
* Shows the ordered scores in the game channel.
*/
public void showScores() {
final Map<TheHumanityPlayer, Integer> scores = new HashMap<>();
synchronized (this.historicPlayers) {
this.historicPlayers.stream().forEach(p -> scores.put(p, p.getScore()));
}
final Map<TheHumanityPlayer, Integer> sortedScores = new TreeMap<>(new DescendingValueComparator<>(scores));
sortedScores.putAll(scores);
final StringBuilder sb = new StringBuilder();
sb.append(Format.BOLD).append("Scores:").append(Format.RESET).append(" ");
for (final Map.Entry<TheHumanityPlayer, Integer> entry : sortedScores.entrySet()) {
sb.append(entry.getKey().getUser().getNick()).append(": ").append(entry.getValue() == null ? 0 : entry.getValue()).append(", ");
}
this.sendMessage(sb.toString().substring(0, sb.length() - 2));
}
/**
* Skips the countdown to start this TheHumanityGame and immediately starts the TheHumanityGame.
*
* @return true if countdown was skipped, false if otherwise
*/
public boolean skipCountdown() {
if (this.countdownTask == null || this.countdownTask.isCancelled() || this.countdownTask.isDone()) return false;
if (this.getPlayers().size() < 3) return false;
this.advanceState();
this.countdownTask.cancel(false);
return true;
}
/**
* Starts the game.
*/
public void start(@NotNull final List<CAHCardPack> cardPacks) {
Preconditions.checkNotNull(cardPacks, "cardPacks cannot be null");
this.deck = new Deck(cardPacks);
if (!this.state.getUniqueName().equals(GameStatus.IDLE.name())) return;
this.advanceState();
}
/**
* Stops the game.
*/
public void stop(@NotNull final GameEndCause endCause) {
this.setEndCause(endCause);
if (this.getCurrentRound() != null) {
this.getCurrentRound().setEndTime(System.currentTimeMillis());
this.getCurrentRound().setEndCause(RoundEndCause.GAME_ENDED);
this.previousRounds.add(this.getCurrentRound().takeSnapshot());
}
this.humanity.getGameRegistrar().end(this);
if (this.host != null && !this.hostWasVoiced) {
ExtensionsKt.userMode(this.getChannel(), new UserModeData(false, 'v', this.host.getUser()));
}
if (this.countdownTask != null) this.countdownTask.cancel(false);
if (this.getCurrentRound() != null) {
this.getCurrentRound().cancelReminderTask();
}
if (!this.state.getUniqueName().equals(GameStatus.IDLE.name())) {
this.state = xyz.cardstock.cardstock.extensions.enums.ExtensionsKt.toState(GameStatus.IDLE);
this.sendMessage(Format.BOLD + "The game has ended.");
if (!this.state.getUniqueName().equals(GameStatus.JOINING.name())) this.showScores();
}
this.endTime = System.currentTimeMillis();
this.state = xyz.cardstock.cardstock.extensions.enums.ExtensionsKt.toState(GameStatus.ENDED);
this.humanity.getHistory().saveGameSnapshot(this.takeSnapshot());
}
@NotNull
@Override
public GameSnapshot takeSnapshot() {
return new GameSnapshot(
this.getChannel().getName(),
this.endCause.name(),
this.startTime,
this.endTime,
this.getPlayers().stream().map(p -> p.getUser().getNick()).collect(Collectors.toList()),
this.getHistoricPlayers().stream().map(p -> p.getUser().getNick()).collect(Collectors.toList()),
this.getHouseRules().stream().map(HouseRule::getFriendlyName).collect(Collectors.toList()),
this.getDeck().getCardPacks().stream().map(CAHCardPack::getName).collect(Collectors.toList()),
this.getPreviousRounds(),
this.getHistoricPlayers().stream().collect(Collectors.toMap(p -> p.getUser().getNick(), TheHumanityPlayer::getScore)),
this.getHost().getUser().getNick(),
this.getCurrentRound() == null ? 0 : this.getCurrentRound().getNumber()
);
}
@NotNull
@Override
public String toJSON() {
return this.takeSnapshot().toJSON();
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("historicPlayers", this.historicPlayers)
.add("players", this.getPlayers())
.add("deck", this.deck)
.add("houseRules", this.houseRules)
.add("channel", this.channel)
.add("currentRound", this.currentRound)
.add("host", this.host)
.add("state", this.state.getUniqueName())
.toString();
}
/**
* Updates the Channel and Users.
*/
public void update() {
this.updateChannel();
this.updateUsers();
}
/**
* Updates the stored Channel snapshot.
*/
public void updateChannel() {
this.setChannel(
this.channel.getClient().getChannels().stream()
.filter(c -> c.getName().equalsIgnoreCase(this.getChannel().getName()))
.findFirst()
.orElseThrow(IllegalStateException::new)
);
}
/**
* Updates the Users stored in all Players.
*/
public void updateUsers() {
this.getHistoricPlayers().stream()
.forEach(p -> {
final User newUser = this.channel.getUsers().stream()
.filter(u -> u.getNick().equalsIgnoreCase(p.getUser().getNick()))
.findFirst()
.orElse(null);
if (newUser == null) return;
p.setUser(newUser);
}
);
}
@Nullable
@Override
public TheHumanityPlayer getPlayer(@NotNull User user, boolean b) {
return null;
}
@NotNull
@Override
public State getState() {
return this.state;
}
@Override
public void setState(@NotNull final State state) {
Preconditions.checkNotNull(state);
this.state = state;
}
@NotNull
@Override
public List<Function1<State, Unit>> getStateListeners() {
return this.stateListeners;
}
public enum GameStatus {
/**
* The game is not started. Players are not joining. Nothing is happening.
*/
IDLE,
/**
* The game is started. Players are joining.
*/
JOINING,
/**
* The game is now in play. Cards are being played.
*/
PLAYING,
/**
* The game has terminated. Players are not joining. Nothing is happening.
*/
ENDED
}
public enum GameEndCause {
NOT_ENDED,
NOT_ENOUGH_PLAYERS,
RAN_OUT_OF_BLACK_CARDS,
NOT_ENOUGH_WHITE_CARDS,
STOPPED_BY_COMMAND,
JAVA_SHUTDOWN
}
private class GameCountdown implements Runnable {
private int runCount = 3; // 3 default
@Override
public void run() {
final int seconds = this.runCount * 15;
if (seconds > 0) {
TheHumanityGame.this.sendMessage(Format.BOLD.toString() + (this.runCount * 15) + Format.RESET + " seconds remain to join the game!");
}
if (this.runCount-- < 1) {
if (TheHumanityGame.this.getPlayers().size() >= 3) {
TheHumanityGame.this.advanceState();
} else {
TheHumanityGame.this.sendMessage(Format.BOLD + "Not enough players." + Format.RESET + " At least three people are required for the game to begin.");
TheHumanityGame.this.stop(GameEndCause.NOT_ENOUGH_PLAYERS);
}
TheHumanityGame.this.countdownTask.cancel(false);
}
}
}
}