package io.mazenmc.skypebot.game.cah;
import com.samczsun.skype4j.chat.messages.ReceivedMessage;
import com.samczsun.skype4j.user.User;
import io.mazenmc.skypebot.engine.bot.Command;
import io.mazenmc.skypebot.engine.bot.Module;
import io.mazenmc.skypebot.engine.bot.ModuleManager;
import io.mazenmc.skypebot.game.BaseGame;
import io.mazenmc.skypebot.game.GameLang;
import io.mazenmc.skypebot.game.GameManager;
import io.mazenmc.skypebot.game.GameState;
import io.mazenmc.skypebot.utils.Resource;
import io.mazenmc.skypebot.utils.Utils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.*;
// special thanks to @stuntguy3000 for writing the original crappy code for this
// PS. GET A LICENSE ON YOUR PROJECTS, FOOL.
public class CardsAgainstHumanity extends BaseGame implements Module {
private HashMap<String, CAHCardPack> allCardPacks = new HashMap<>();
private List<CAHCard> blackCards = new ArrayList<>();
private String cardCzar;
private List<CAHCardPack> chosenDecks = new ArrayList<>();
private boolean continueGame = true;
private CAHCard currentBlackCard;
private boolean czarChoosing = false;
private List<CzarOption> czarOptions = new ArrayList<>();
private LinkedHashMap<String, Boolean> extrasPacks = new LinkedHashMap<>();
private HashMap<String, LinkedList<CAHCard>> playedCards = new HashMap<>();
private int playerOrderIndex = 0;
private int round = 1;
public int secondsSincePlay = 0;
private HashMap<String, List<CAHCard>> userCards = new HashMap<>();
private List<CAHCard> whiteCards = new ArrayList<>();
private boolean countingDown = false;
int attempts = 0;
CAHJoinTask task;
// Init Class
public CardsAgainstHumanity() {
setState(GameState.WAITING_FOR_PLAYERS);
setMinPlayers(3);
loadPacks();
for (int i = 1; i < 6; i++) {
extrasPacks.put("Extra " + i, false);
}
ModuleManager.registerModule(CardsAgainstHumanity.class);
}
@Override
public String description() {
return "Cards Against Humanity.\n" +
"Please select your card pack by doing @pack <number>\n" +
"You may add extras by doing @extra <number>\n" +
"Once you've selected your card pack, you can do @cahstart to start the join timer\n" +
"To join CAH, just do @join\n" +
"To leave, do @leave";
}
@Override
public String name() {
return "CAH";
}
@Override
public void end() {
ModuleManager.removeModule(CardsAgainstHumanity.class);
if (task != null) {
task.cancel();
}
}
public boolean checkPlayers(boolean forcePlay) {
if (minPlayers() > activePlayers().size()) {
sendToAll(GameLang.ERROR_NOT_ENOUGH_PLAYERS);
return false;
}
if (state() == GameState.CHOOSING || forcePlay) {
int toPlay = activePlayers().size() - 1; // exclude czar
if (playedCards.size() == toPlay || forcePlay) {
if (!forcePlay) {
for (Map.Entry<String, LinkedList<CAHCard>> cardPlay : playedCards.entrySet()) {
if (cardPlay.getValue().size() < currentBlackCard.blanks()) {
return false;
}
}
}
setState(GameState.INGAME);
String[] blackCardSplit = currentBlackCard.getRawText().split("_");
for (Map.Entry<String, LinkedList<CAHCard>> playerCards : playedCards.entrySet()) {
int segmentID = 0;
int cardCount = 0;
CzarOption czarOption = new CzarOption(Utils.getUser(playerCards.getKey()));
StringBuilder modifiedBlackCard = new StringBuilder();
modifiedBlackCard.append(blackCardSplit[segmentID]);
for (CAHCard playerCard : playerCards.getValue()) {
segmentID++;
cardCount++;
modifiedBlackCard.append("*");
modifiedBlackCard.append(playerCard.getText());
modifiedBlackCard.append("*");
if (segmentID < blackCardSplit.length) {
modifiedBlackCard.append(blackCardSplit[segmentID]);
}
}
if (cardCount != currentBlackCard.blanks()) {
continue;
}
modifiedBlackCard.append("\n");
czarOption.setText(modifiedBlackCard.toString());
czarOptions.add(czarOption);
}
StringBuilder options = new StringBuilder();
Collections.shuffle(czarOptions);
int index = 1;
for (CzarOption czarOption : czarOptions) {
czarOption.setOptionNumber(index);
options.append(czarOption.text());
index++;
}
if (index == 1) {
sendToAll(GameLang.GAME_CAH_NOPLAYERS);
new CAHRoundDelay(this);
return true;
}
sendToAll(GameLang.GAME_CAH_ALLPLAYED);
sendToAll("\n" + options.toString());
send(cardCzar, createCzarKeyboard());
czarChoosing = true;
}
}
return true;
}
private String createCzarKeyboard() {
StringBuilder keyboard = new StringBuilder();
keyboard.append(currentBlackCard.getText()).append("\n");
for (CzarOption czarOption : czarOptions) {
keyboard.append(czarOption.optionNumber())
.append(". ")
.append(czarOption.text())
.append("\n");
}
keyboard.append("You may select your card by using @select <number> in the group chat");
return keyboard.toString();
}
private String createUserKeyboard(String user) {
List<CAHCard> cards = userCards.get(user);
StringBuilder keyboard = new StringBuilder();
keyboard.append(currentBlackCard.getText()).append("\n");
int index = 1;
for (CAHCard card : cards) {
keyboard.append(index)
.append(". ")
.append(card.getText())
.append("\n");
index++;
}
keyboard.append("You may select your card by using @select <number> in the group chat");
return keyboard.toString();
}
private void fillHands() {
for (String user : activePlayers()) {
giveWhiteCard(user, 10);
}
}
@Override
public void onSecond() {
secondsSincePlay++;
if (!czarChoosing) {
if (secondsSincePlay == 60) {
sendToAll(GameLang.GAME_CAH_TIMEWARNING);
} else if (secondsSincePlay == 80) {
sendToAll(GameLang.GAME_CAH_TIMENOTICE);
checkPlayers(true);
}
} else {
secondsSincePlay = 0;
}
}
@Override
public void removePlayer(String username) {
super.removePlayer(username);
if (username.equals(cardCzar) && checkPlayers(false)) {
nextRound();
}
if (state() == GameState.INGAME && activePlayers().size() < 3) {
sendToAll("There are no longer enough players to continue CAH! Ending game...");
GameManager.instance().stopGame();
}
}
@Override
public void startGame() {
if (chosenDecks.isEmpty()) {
chosenDecks.add(allCardPacks.get("cah.x1.cards")); // if no decks were explicitly chosen, choose the first one
}
setState(GameState.INGAME);
StringBuilder stringBuilder = new StringBuilder();
for (CAHCardPack pack : chosenDecks) {
for (CAHCard card : pack.cards()) {
if (card.cahCardType() == CAHCardType.WHITE) {
whiteCards.add(card);
} else if (card.cahCardType() == CAHCardType.BLACK) {
blackCards.add(card);
}
}
stringBuilder.append(pack.metadata().get("name")).append(", ");
}
sendToAll(String.format(GameLang.GAME_CAH_ACTIVECARDS, stringBuilder.toString().substring(0, stringBuilder.length() - 2)));
Collections.shuffle(whiteCards);
Collections.shuffle(blackCards);
fillHands();
nextRound();
}
private void giveWhiteCard(String user, int amount) {
List<CAHCard> playerCardDeck = userCards.get(user);
if (playerCardDeck == null) {
playerCardDeck = new ArrayList<>();
}
for (int i = 0; i < amount; i++) {
playerCardDeck.add(whiteCards.remove(0));
}
userCards.put(user, playerCardDeck);
}
private boolean isPlaying(String user) {
return activePlayers().contains(user);
}
// Load Card Packs from Jar resources
public void loadPacks() {
List<String> knownPacks = new ArrayList<>();
for (int i = 1; i < 4; i++) {
knownPacks.add("cah.v" + i + ".cards");
}
for (int i = 1; i < 6; i++) {
knownPacks.add("cah.x" + i + ".cards");
}
for (String name : knownPacks) {
InputStream is = getClass().getResourceAsStream("/" + name);
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
CAHCardPack cahCardPack = new CAHCardPack();
String packLine;
CAHPackProperty cahPackProperty = null;
try {
while ((packLine = reader.readLine()) != null) {
switch (packLine) {
case "___METADATA___": {
cahPackProperty = CAHPackProperty.METADATA;
continue;
}
case "___BLACK___": {
cahPackProperty = CAHPackProperty.BLACKCARDS;
continue;
}
case "___WHITE___": {
cahPackProperty = CAHPackProperty.WHITECARDS;
continue;
}
default: {
if (cahPackProperty != null) {
switch (cahPackProperty) {
case METADATA: {
if (packLine.contains(": ")) {
String[] packData = packLine.split(": ");
cahCardPack.addMetadata(packData[0], packData[1]);
}
continue;
}
case BLACKCARDS: {
cahCardPack.addCard(packLine.replaceAll("~", "\n"), CAHCardType.BLACK);
continue;
}
case WHITECARDS: {
cahCardPack.addCard(packLine.replaceAll("~", "\n"), CAHCardType.WHITE);
}
}
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
allCardPacks.put(name, cahCardPack);
}
}
// Play the next round
public void nextRound() {
secondsSincePlay = 0;
if (!continueGame) {
GameManager.instance().stopGame();
} else {
for (String user : activePlayers()) {
int size = userCards.get(user).size();
giveWhiteCard(user, 10 - size);
}
playerOrderIndex++;
if (playerOrderIndex >= activePlayers().size()) {
playerOrderIndex = 0;
}
czarOptions.clear();
playedCards.clear();
czarChoosing = false;
blackCards.add(currentBlackCard);
currentBlackCard = blackCards.remove(0);
cardCzar = activePlayers().get(playerOrderIndex);
sendToAll(String.format(GameLang.GAME_CAH_STARTROUND_CZAR, round, cardCzar));
setState(GameState.CHOOSING);
sendToAll(currentBlackCard.getText());
activePlayers().stream().filter((s) -> !cardCzar.equals(s)).forEach((user) -> send(user, createUserKeyboard(user)));
if (currentBlackCard.blanks() > 1) {
sendToAll(String.format(GameLang.GAME_CAH_WHITECARDS, currentBlackCard.blanks()));
}
round++;
}
}
private boolean playCard(String sender, String message) {
if (czarChoosing) {
if (sender.equals(cardCzar)) {
try {
int number = Integer.parseInt(message.split(" ")[0]);
if (!tryCzar(number)) {
sendToAll(GameLang.ERROR_INVALID_SELECTION);
}
} catch (Exception ignored) {
}
return true;
} else {
System.out.println("not the czar");
return false;
}
}
if (state() != GameState.CHOOSING) {
System.out.println("not choosing time");
return false;
}
if (sender.equals(cardCzar)) {
return false;
}
int number;
List<CAHCard> userCards = this.userCards.get(sender);
try {
number = Integer.parseInt(message.split(" ")[0]);
} catch (NumberFormatException e) {
return false;
}
if (number < 0 || userCards == null || number > userCards.size()) {
return false;
}
CAHCard cahCard = userCards.get(number - 1);
LinkedList<CAHCard> cards = new LinkedList<>();
if (playedCards.containsKey(sender)) {
cards = playedCards.get(sender);
}
int cardsNeeded = currentBlackCard.blanks();
if (cards.size() < cardsNeeded) {
for (CAHCard userCard : cards) {
if (userCard.getText().equals(cahCard.getText())) {
send(sender, GameLang.ERROR_ALREADY_PLAYED_CARD);
return true;
}
}
cards.add(cahCard);
playedCards.put(sender, cards);
// Remove from players hand
new ArrayList<>(userCards).forEach((userCard) -> {
if (userCard.getText().equals(cahCard.getText())) {
userCards.remove(userCard);
}
});
if (cards.size() == cardsNeeded) {
sendToAll(String.format(GameLang.GAME_CAH_USERPLAY, sender));
checkPlayers(false);
} else {
send(sender, createUserKeyboard(sender));
send(sender, String.format(GameLang.GAME_CAH_PLAY_MORE, cardsNeeded - cards.size()));
}
} else {
send(sender, GameLang.GAME_GENERAL_NOT_TURN);
}
return true;
}
boolean tryCzar(int number) {
if (czarChoosing) {
User winner = null;
for (CzarOption czarOption : czarOptions) {
if (czarOption.optionNumber() == number) {
winner = czarOption.owner();
czarChoosing = false;
break;
}
}
if (winner != null) {
sendToAll(String.format(GameLang.GAME_CAH_WIN_ROUND, String.valueOf(number), winner.getUsername()));
nextRound();
incrementScore(winner.getUsername());
String gameWinner = null;
for (String user : activePlayers()) {
if (scoreFor(user) >= 10) {
gameWinner = user;
}
}
if (gameWinner != null) {
continueGame = false;
GameManager.instance().stopGame();
} else {
new CAHRoundDelay(this);
}
return true;
}
}
return false;
}
CAHCardPack base() {
return Utils.optionalGet(allCardPacks.entrySet().stream()
.filter((e) -> chosenDecks.contains(e.getValue()) && e.getKey().contains("v"))
.map(Map.Entry::getValue)
.findFirst());
}
/* Commands Section */
public static CardsAgainstHumanity current() {
return (CardsAgainstHumanity) GameManager.instance().current();
}
@Command(name = "pack")
public static void pack(ReceivedMessage message, Integer number) {
CardsAgainstHumanity cah = current();
CAHCardPack pack = cah.allCardPacks.get("cah.v" + number + ".cards");
CAHCardPack base = cah.base();
if (cah.countingDown) {
Resource.sendMessage(message, "No modifications may be made after countdown started!");
return;
}
if (pack == null) {
Resource.sendMessage(message, "That base pack does not exist!");
return;
}
if (base != null) {
cah.chosenDecks.remove(base); // remove old base pack
return;
}
cah.chosenDecks.add(pack);
Resource.sendMessage(message, "That base pack has been selected!");
}
@Command(name = "extra")
public static void extra(ReceivedMessage message, Integer number) {
CardsAgainstHumanity cah = current();
CAHCardPack pack = cah.allCardPacks.get("cah.x" + number + ".cards");
if (cah.countingDown) {
Resource.sendMessage(message, "No modifications may be made after countdown started!");
return;
}
if (pack == null) {
Resource.sendMessage(message, "That extra pack does not exist!");
return;
}
if (cah.chosenDecks.contains(pack)) {
Resource.sendMessage(message, "CAH already contains this extra pack!");
return;
}
cah.chosenDecks.add(pack);
Resource.sendMessage(message, "That extra pack has been added!");
}
@Command(name = "cahstart")
public static void start(ReceivedMessage message) {
CardsAgainstHumanity cah = current();
cah.countingDown = true;
new CAHJoinTask(cah);
Resource.sendMessage(message, "Timer has started!");
}
@Command(name = "join")
public static void join(ReceivedMessage message) throws Exception {
String senderId = message.getSender().getUsername();
CardsAgainstHumanity cah = current();
if (cah.isPlaying(senderId)) {
Resource.sendMessage(message, "You are already in the game!");
return;
}
cah.addPlayer(message.getSender());
Resource.sendMessage(message, "You have joined the current CAH game!");
}
@Command(name = "leave")
public static void leave(ReceivedMessage message) throws Exception {
String senderId = message.getSender().getUsername();
CardsAgainstHumanity cah = current();
if (!cah.isPlaying(senderId)) {
Resource.sendMessage(message, "You are not in the game! Yay!");
return;
}
cah.removePlayer(senderId);
Resource.sendMessage(message, "You have successfully left the game");
}
@Command(name = "select")
public static void select(ReceivedMessage message, Integer card) throws Exception {
String senderId = message.getSender().getUsername();
CardsAgainstHumanity cah = current();
if (!cah.isPlaying(senderId)) {
Resource.sendMessage(message, "You are not in the game! Too bad, too sad!");
return;
}
if (!cah.playCard(senderId, String.valueOf(card))) {
Resource.sendMessage(message, "Yeah.. about playing that card, I wasn't really able to do that.");
}
}
@Command(name = "forcestart", admin = true)
public static void forceStart(ReceivedMessage message) throws Exception {
CardsAgainstHumanity cah = current();
if (cah.activePlayers().size() < 3) {
Resource.sendMessage(message, "There are not enough players! You idiot!");
return;
}
if (cah.task != null) {
cah.task.cancel();
cah.task = null;
}
cah.sendToAll("Let the games begin!");
cah.startGame();
}
@Command(name = "cahlist")
public static void cahList(ReceivedMessage message) {
StringBuilder builder = new StringBuilder();
for (String s : current().activePlayers()) {
builder.append(s).append(", ");
}
Resource.sendMessage(message, builder.toString());
}
@Command(name = "cahscores")
public static void cahScores(ReceivedMessage message) throws Exception {
CardsAgainstHumanity cah = current();
StringBuilder sb = new StringBuilder();
sb.append("---------------------------\n");
for (Map.Entry<String, Integer> e : cah.scores.entrySet()) {
sb.append(e.getKey())
.append(": ")
.append(e.getValue())
.append("\n");
}
sb.append("---------------------------");
current().send(message.getSender().getUsername(), sb.toString());
}
}
class CzarOption {
private int optionNumber;
private User owner;
private String text;
CzarOption(User owner) {
this.owner = owner;
}
public int optionNumber() {
return optionNumber;
}
public void setOptionNumber(int optionNumber) {
this.optionNumber = optionNumber;
}
public User owner() {
return owner;
}
public void setOwner(User owner) {
this.owner = owner;
}
public String text() {
return text;
}
public void setText(String text) {
this.text = text;
}
}
class CAHRoundDelay extends TimerTask {
private CardsAgainstHumanity cardsAgainstHumanity;
public CAHRoundDelay(CardsAgainstHumanity cardsAgainstHumanity) {
this.cardsAgainstHumanity = cardsAgainstHumanity;
this.cardsAgainstHumanity.secondsSincePlay = 0;
new Timer().schedule(this, 60000);
}
@Override
public void run() {
if (cardsAgainstHumanity.state() == GameState.INGAME) {
cardsAgainstHumanity.nextRound();
}
}
}
class CAHJoinTask extends TimerTask {
private CardsAgainstHumanity cardsAgainstHumanity;
public CAHJoinTask(CardsAgainstHumanity cah) {
this.cardsAgainstHumanity = cah;
new Timer().schedule(this, 60000);
cardsAgainstHumanity.task = this;
}
@Override
public void run() {
if (cardsAgainstHumanity.activePlayers().size() <= 3) {
if (cardsAgainstHumanity.attempts < 3) {
cardsAgainstHumanity.attempts++;
cardsAgainstHumanity.sendToAll("Not enough players to start CAH, trying again.\nAttempt: " + cardsAgainstHumanity.attempts +
"\nResending description...");
cardsAgainstHumanity.sendToAll(cardsAgainstHumanity.description());
new CAHJoinTask(cardsAgainstHumanity);
} else {
cardsAgainstHumanity.sendToAll("Attempt 3 has been reached! Cancelling CAH.");
GameManager.instance().stopGame();
}
return;
}
cardsAgainstHumanity.sendToAll("Let the games begin!");
cardsAgainstHumanity.startGame();
cardsAgainstHumanity.task = null;
}
}