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); } } } }