diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/evaluator/HandEvaluator.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/evaluator/HandEvaluator.java new file mode 100644 index 0000000..c5d4fb3 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/evaluator/HandEvaluator.java @@ -0,0 +1,413 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.domain.game.evaluator; + +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Card; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Suit; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * The HandEvaluator class provides functionality to evaluate a poker hand and + * determine its rank. It analyzes a list of cards and identifies the best + * possible hand according to standard poker rules, such as flushes, straights, + * pairs, and more. The evaluation process involves counting card ranks, + * grouping + * by suits, and checking for specific hand combinations to assign the correct + * hand rank. + */ +public class HandEvaluator { + private static final int RANK_OFFSET = 2; + private static final int MIN_FLUSH_SIZE = 5; + private static final int ROYAL_FLUSH_ACE = 14; + private static final int ROYAL_FLUSH_KING = 13; + private static final int ROYAL_FLUSH_QUEEN = 12; + private static final int ROYAL_FLUSH_JACK = 11; + private static final int ROYAL_FLUSH_TEN = 10; + private static final long FOUR_OF_A_KIND_COUNT = 4L; + private static final int FOUR_OF_A_KIND = 4; + private static final int THREE_OF_A_KIND = 3; + private static final int PAIR = 2; + private static final int FIRST_INDEX = 0; + private static final int CARD_VALUE_OFFSET = 2; + private static final int HAND_SIZE = 5; + private static final long THREE_OF_A_KIND_COUNT = 3L; + private static final int THREE_OF_A_KIND_RANK = 3; + private static final int ONE_PAIR = 1; + private static final int DEFAULT_VALUE = 0; + private static final int STRAIGHT_LENGTH = 5; + private static final int INITIAL_STREAK = 1; + private static final int START_INDEX = 1; + private static final int CARD_DIFFERENCE = 1; + private static final int PREVIOUS_INDEX_OFFSET = -1; + + /** + * Evaluates a list of cards and determines the best possible hand rank. + * + * @param cards A list of Card objects representing the player's hand. + * @return A HandRank object representing the evaluated hand rank. + */ + public static HandRank evaluate(List cards) { + + List ranks = getSortedRanks(cards); + Map rankCount = getRankCount(ranks); + Map> suits = groupBySuit(cards); + + List flushCards = getFlushCards(suits); + boolean flush = flushCards != null; + boolean straight = isStraight(ranks); + + HandRank straightFlush = checkStraightFlush(flushCards); + if (straightFlush != null) { + return straightFlush; + } + + HandRank fourKind = checkFourOfAKind(rankCount); + if (fourKind != null) { + return fourKind; + } + + HandRank fullHouse = checkFullHouse(rankCount); + if (fullHouse != null) { + return fullHouse; + } + + if (flush) { + return buildFlush(flushCards); + } + + if (straight) { + return new HandRank(HandRank.Type.STRAIGHT, ranks); + } + + HandRank threeKind = checkThreeOfAKind(rankCount); + if (threeKind != null) { + return threeKind; + } + + HandRank twoPair = checkTwoPair(rankCount); + if (twoPair != null) { + return twoPair; + } + + HandRank onePair = checkOnePair(rankCount); + if (onePair != null) { + return onePair; + } + + return new HandRank( + HandRank.Type.HIGH_CARD, + ranks.stream().limit(HAND_SIZE).toList()); + } + + /** + * Helper method to extract and sort the ranks of the cards in descending order. + * + * @param cards A list of Card objects representing the player's hand. + * @return A sorted list of integer ranks corresponding to the cards. + */ + private static List getSortedRanks(List cards) { + return cards.stream() + .map(c -> c.getRank().ordinal() + RANK_OFFSET) + .sorted(Comparator.reverseOrder()) + .toList(); + } + + /** + * Helper method to count the occurrences of each card rank in the hand. + * + * @param ranks A list of integer ranks representing the cards in the hand. + * @return A map where the key is the card rank and the value is the count of + * occurrences. + */ + private static Map getRankCount(List ranks) { + return ranks.stream() + .collect(Collectors.groupingBy(r -> r, Collectors.counting())); + } + + /** + * Helper method to group the cards by their suits. + * + * @param cards A list of Card objects representing the player's hand. + * @return A map where the key is the Suit and the value is a list of Cards + * belonging to that suit. + */ + private static Map> groupBySuit(List cards) { + return cards.stream() + .collect(Collectors.groupingBy(Card::getSuit)); + } + + /** + * Helper method to identify if there is a flush in the hand and return the + * corresponding cards. + * + * @param suits A map where the key is the Suit and the value is a list of Cards + * belonging to that suit. + * @return A list of Card objects that form a flush, or null if no flush is + * found. + */ + private static List getFlushCards(Map> suits) { + return suits.values() + .stream() + .filter(s -> s.size() >= MIN_FLUSH_SIZE) + .findFirst() + .orElse(null); + } + + /** + * Helper method to build a HandRank object for a flush hand. + * + * @param flushCards A list of Card objects that form a flush. + * @return A HandRank object representing the flush hand rank. + */ + private static HandRank buildFlush(List flushCards) { + List flushRanks = flushCards.stream() + .map(c -> c.getRank().ordinal() + CARD_VALUE_OFFSET) + .sorted(Comparator.reverseOrder()) + .limit(HAND_SIZE) + .toList(); + + return new HandRank(HandRank.Type.FLUSH, flushRanks); + } + + /** + * Helper method to check for a straight flush or royal flush in the hand. + * + * @param flushCards A list of Card objects that form a flush. + * @return A HandRank object representing the straight flush or royal flush hand + * rank, or null if neither is found. + */ + private static HandRank checkStraightFlush(List flushCards) { + if (flushCards == null) { + return null; + } + + List flushRanks = flushCards.stream() + .map(c -> c.getRank().ordinal() + RANK_OFFSET) + .sorted(Comparator.reverseOrder()) + .toList(); + + if (!isStraight(flushRanks)) { + return null; + } + + if (flushRanks.containsAll(List.of( + ROYAL_FLUSH_ACE, + ROYAL_FLUSH_KING, + ROYAL_FLUSH_QUEEN, + ROYAL_FLUSH_JACK, + ROYAL_FLUSH_TEN))) { + + return new HandRank(HandRank.Type.ROYAL_FLUSH, flushRanks); + } + + return new HandRank(HandRank.Type.STRAIGHT_FLUSH, flushRanks); + } + + /** + * Helper method to check for a four of a kind hand rank. + * + * @param rankCount A map where the key is the card rank and the value is the + * count of occurrences. + * @return A HandRank object representing the four of a kind hand rank, or null + * if not found. + */ + private static HandRank checkFourOfAKind(Map rankCount) { + if (!rankCount.containsValue(FOUR_OF_A_KIND_COUNT)) { + return null; + } + + int quad = getRank(rankCount, FOUR_OF_A_KIND); + + return new HandRank( + HandRank.Type.FOUR_OF_A_KIND, + List.of(quad)); + } + + /** + * Helper method to check for a full house hand rank. + * + * @param rankCount A map where the key is the card rank and the value is the + * count of occurrences. + * @return A HandRank object representing the full house hand rank, or null if + * not found. + */ + private static HandRank checkFullHouse(Map rankCount) { + + List trips = rankCount.entrySet() + .stream() + .filter(e -> e.getValue() >= THREE_OF_A_KIND) + .map(Map.Entry::getKey) + .sorted(Comparator.reverseOrder()) + .toList(); + + List pairs = rankCount.entrySet() + .stream() + .filter(e -> e.getValue() >= PAIR) + .map(Map.Entry::getKey) + .sorted(Comparator.reverseOrder()) + .toList(); + + if (trips.isEmpty()) { + return null; + } + + int three = trips.get(FIRST_INDEX); + + Integer pair = pairs.stream() + .filter(p -> p != three) + .findFirst() + .orElse(null); + + if (pair == null) { + return null; + } + + return new HandRank( + HandRank.Type.FULL_HOUSE, + List.of(three, pair)); + } + + /** + * Helper method to check for a three of a kind hand rank. + * + * @param rankCount A map where the key is the card rank and the value is the + * count of occurrences. + * @return A HandRank object representing the three of a kind hand rank, or null + * if not found. + */ + private static HandRank checkThreeOfAKind(Map rankCount) { + if (!rankCount.containsValue(THREE_OF_A_KIND_COUNT)) { + return null; + } + + int tripsRank = getRank(rankCount, THREE_OF_A_KIND_RANK); + + return new HandRank( + HandRank.Type.THREE_OF_A_KIND, + List.of(tripsRank)); + } + + /** + * Helper method to check for a two pair hand rank. + * + * @param rankCount A map where the key is the card rank and the value is the + * count of occurrences. + * @return A HandRank object representing the two pair hand rank, or null if + * not found. + */ + private static HandRank checkTwoPair(Map rankCount) { + + long pairCount = rankCount.values() + .stream() + .filter(v -> v == PAIR) + .count(); + + if (pairCount < PAIR) { + return null; + } + + List pairRanks = rankCount.entrySet() + .stream() + .filter(e -> e.getValue() == PAIR) + .map(Map.Entry::getKey) + .sorted(Comparator.reverseOrder()) + .limit(PAIR) + .toList(); + + return new HandRank( + HandRank.Type.TWO_PAIR, + pairRanks); + } + + /** + * Helper method to check for a one pair hand rank. + * + * @param rankCount A map where the key is the card rank and the value is the + * count of occurrences. + * @return A HandRank object representing the one pair hand rank, or null if + * not found. + */ + private static HandRank checkOnePair(Map rankCount) { + + long pairCount = rankCount.values() + .stream() + .filter(v -> v == PAIR) + .count(); + + if (pairCount != ONE_PAIR) { + return null; + } + + int pair = getRank(rankCount, PAIR); + + return new HandRank( + HandRank.Type.ONE_PAIR, + List.of(pair)); + } + + /** + * Helper method to get the rank of a card based on the count of occurrences in + * the hand. + * + * @param map A map where the key is the card rank and the value is the count + * of occurrences. + * @param count The specific count to look for (e.g., 2 for pairs, 3 for three + * of a kind). + * @return The rank of the card that matches the specified count, or 0 if none + * found. + */ + private static int getRank(Map map, int count) { + + return map.entrySet() + .stream() + .filter(e -> e.getValue() == count) + .map(Map.Entry::getKey) + .max(Integer::compare) + .orElse(DEFAULT_VALUE); + } + + /** + * Helper method to determine if a list of card ranks forms a straight. + * + * @param ranks A list of integer ranks representing the cards in the hand. + * @return True if the ranks form a straight, false otherwise. + */ + private static boolean isStraight(List ranks) { + + Set unique = new HashSet<>(ranks); + + // Wheel Straight A-2-3-4-5 + if (unique.containsAll(List.of( + ROYAL_FLUSH_ACE, + ROYAL_FLUSH_KING, + ROYAL_FLUSH_QUEEN, + ROYAL_FLUSH_JACK, + ROYAL_FLUSH_TEN))) { + return true; + } + + List sorted = unique.stream().sorted().collect(Collectors.toList()); + + int streak = INITIAL_STREAK; + + for (int i = START_INDEX; i < sorted.size(); i++) { + + if (sorted.get(i) == sorted.get(i + PREVIOUS_INDEX_OFFSET) + CARD_DIFFERENCE) { + + streak++; + + if (streak >= STRAIGHT_LENGTH) { + return true; + } + + } else { + streak = INITIAL_STREAK; + } + } + + return false; + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/evaluator/HandRank.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/evaluator/HandRank.java new file mode 100644 index 0000000..b783677 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/evaluator/HandRank.java @@ -0,0 +1,106 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.domain.game.evaluator; + +import java.util.List; + +/** + * HandRank represents the rank of a poker hand, including its type (e.g., + * flush, + * straight) and the kickers used for tie-breaking. It implements the + * Comparable interface to allow for easy comparison between different hand + * ranks. + */ +public class HandRank implements Comparable { + + /** + * The Type enumeration defines the different types of poker hands, each with + * an associated strength value for comparison purposes. + */ + public enum Type { + + HIGH_CARD(1), + ONE_PAIR(2), + TWO_PAIR(3), + THREE_OF_A_KIND(4), + STRAIGHT(5), + FLUSH(6), + FULL_HOUSE(7), + FOUR_OF_A_KIND(8), + STRAIGHT_FLUSH(9), + ROYAL_FLUSH(10); + + private final int strength; + + Type(int strength) { + this.strength = strength; + } + + public int getStrength() { + return strength; + } + } + + private final Type type; + private final List kickers; + + /** + * Constructs a HandRank with the specified type and kickers. + * + * @param type The type of the hand (e.g., flush, straight). + * @param kickers A list of integers representing the kickers for tie-breaking. + */ + public HandRank(Type type, List kickers) { + this.type = type; + this.kickers = kickers; + } + + /** + * Retrieves the type of the hand. + * + * @return The type of the hand (e.g., flush, straight). + */ + public Type getType() { + return type; + } + + /** + * Retrieves the list of kickers for tie-breaking. + * + * @return A list of integers representing the kickers for tie-breaking. + */ + public List getKickers() { + return kickers; + } + + /** + * Compares this HandRank with another HandRank for ordering. The comparison is + * based first on the type of the hand and then on the kickers for tie-breaking. + * + * @param other The other HandRank to compare against. + * @return A negative integer, zero, or a positive integer as this HandRank is + * less than, equal to, or greater than the specified HandRank. + */ + @Override + public int compareTo(HandRank other) { + + int typeCompare = Integer.compare( + this.type.getStrength(), + other.type.getStrength()); + + if (typeCompare != 0) { + return typeCompare; + } + + for (int i = 0; i < Math.min(kickers.size(), other.kickers.size()); i++) { + + int cmp = Integer.compare( + kickers.get(i), + other.kickers.get(i)); + + if (cmp != 0) { + return cmp; + } + } + + return 0; + } +}