package org.royaldev.thehumanity.cards;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.royaldev.thehumanity.cards.packs.CAHCardPack;
import org.royaldev.thehumanity.cards.packs.MemoryCardPack;
import org.royaldev.thehumanity.cards.types.BlackCard;
import org.royaldev.thehumanity.cards.types.WhiteCard;
import xyz.cardstock.cardstock.players.hands.Hand;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* Deck class. A deck is a collection of {@link MemoryCardPack CardPacks}. When playing, the Deck pays no heed to which pack a
* card belongs to when dealing.
* <p/>
* Each game should have one deck, which contains both white and black cards. Decks are repopulating,
* meaning that if cards of either type run out, that type of card will be replenished from the card pack sources. This
* happens automatically for white cards when drawing randomly. Black cards must be manually repopulated.
*/
public class Deck {
private final List<CAHCardPack> cardPacks = Collections.synchronizedList(new ArrayList<>());
private final List<WhiteCard> whiteCards = Collections.synchronizedList(new ArrayList<>());
private final List<BlackCard> blackCards = Collections.synchronizedList(new ArrayList<>());
/**
* Creates a new Deck with the given card packs as sources.
*
* @param cardPacks BaseCard packs to add to this deck
*/
public Deck(@NotNull final Collection<CAHCardPack> cardPacks) {
Preconditions.checkNotNull(cardPacks, "cardPacks was null");
synchronized (this.cardPacks) {
this.cardPacks.addAll(cardPacks);
}
this.repopulateBlackCards();
this.repopulateWhiteCards();
}
/**
* Adds a CardPack to this Deck. All cards in the pack will be added to this Deck.
*
* @param cp CardPack to add
* @return true if the pack was added, false if otherwise
*/
public boolean addCardPack(@NotNull final CAHCardPack cp) {
Preconditions.checkNotNull(cp, "cp was null");
if (!this.cardPacks.add(cp)) return false;
cp.getWhiteCards().forEach(this.whiteCards::add);
cp.getBlackCards().forEach(this.blackCards::add);
return true;
}
/**
* Gets the total amount of black cards contained in this deck.
*
* @return Total amount of black cards
*/
public int getBlackCardCount() {
return this.cardPacks.stream().mapToInt(cp -> cp.getBlackCards().size()).sum();
}
/**
* Gets an unmodifiable copy of the list of card packs that this Deck was created with.
*
* @return Unmodifiable list of CardPacks
*/
@NotNull
public List<CAHCardPack> getCardPacks() {
synchronized (this.cardPacks) {
return Collections.unmodifiableList(this.cardPacks);
}
}
/**
* Gets a random black card. If there are none left in this Deck, null will be returned. A manual repopulation can
* be done using {@link #repopulateBlackCards()}, but the default game behavior is to end when black cards are
* exhausted.
*
* @return A random black card or null
*/
@Nullable
public BlackCard getRandomBlackCard() {
synchronized (this.blackCards) {
if (this.blackCards.size() < 1) return null;
Collections.shuffle(this.blackCards);
return this.blackCards.remove(0);
}
}
/**
* Gets a random white card. If there are none left in this Deck, the pile will be repopulated. An optional
* parameter may be included to specify Hands containing cards that are not to be included when the pile is
* repopulated. If it is null, the hand will be repopulated normally.
*
* @param repopulateExcludes Hands with cards not to include or null
* @return A random white card
*/
@NotNull
public WhiteCard getRandomWhiteCard(@Nullable final Collection<Hand> repopulateExcludes) {
synchronized (this.whiteCards) {
if (this.whiteCards.size() < 1) this.repopulateWhiteCards(repopulateExcludes);
Collections.shuffle(this.whiteCards);
return this.whiteCards.remove(0);
}
}
/**
* Gets the amount of unused black cards contained in this deck.
*
* @return Total amount of unused black cards
*/
public int getUnusedBlackCardCount() {
return this.blackCards.size();
}
/**
* Gets the amount of unused white cards contained in this deck.
*
* @return Total amount of unused white cards
*/
public int getUnusedWhiteCardCount() {
return this.whiteCards.size();
}
/**
* Gets the total amount of white cards contained in this deck.
*
* @return Total amount of white cards
*/
public int getWhiteCardCount() {
return this.cardPacks.stream().mapToInt(cp -> cp.getWhiteCards().size()).sum();
}
/**
* Removes a CardPack from this Deck, removing all the cards it has, as well.
*
* @param cp CardPack to remove
* @return true if pack was removed, false if otherwise
*/
public boolean removeCardPack(@NotNull final CAHCardPack cp) {
Preconditions.checkNotNull(cp, "cp was null");
return !(!this.cardPacks.contains(cp) || !this.cardPacks.remove(cp)) && this.whiteCards.removeAll(this.whiteCards.stream().filter(wc -> wc.getCardPack().equals(cp)).collect(Collectors.toCollection(ArrayList::new)));
}
/**
* Adds all the black cards from the card packs back into the draw pile.
*/
public void repopulateBlackCards() {
synchronized (this.cardPacks) {
synchronized (this.blackCards) {
this.cardPacks.forEach(cp -> this.blackCards.addAll(cp.getBlackCards()));
}
}
}
/**
* Adds all the white cards from the card packs back into the draw pile, excluding any in the given collection of
* Hands.
*
* @param exclude Hands of Cards to exclude
*/
public void repopulateWhiteCards(@Nullable final Collection<Hand> exclude) {
synchronized (this.cardPacks) {
synchronized (this.whiteCards) {
for (final CAHCardPack cp : this.cardPacks) {
thisCard:
for (final WhiteCard wc : cp.getWhiteCards()) {
if (exclude != null && !exclude.isEmpty()) {
for (final Hand h : exclude) {
if (h.getCards().contains(wc)) continue thisCard;
}
}
this.whiteCards.add(wc);
}
}
}
}
}
/**
* Adds all the white cards from the card packs back into the draw pile.
*/
public void repopulateWhiteCards() {
this.repopulateWhiteCards(null);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("cardPacks", this.cardPacks)
.toString();
}
}