Feat/Game Engine #233
+413
@@ -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<Card> cards) {
|
||||
|
||||
List<Integer> ranks = getSortedRanks(cards);
|
||||
Map<Integer, Long> rankCount = getRankCount(ranks);
|
||||
Map<Suit, List<Card>> suits = groupBySuit(cards);
|
||||
|
||||
List<Card> 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<Integer> getSortedRanks(List<Card> 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<Integer, Long> getRankCount(List<Integer> 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<Suit, List<Card>> groupBySuit(List<Card> 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<Card> getFlushCards(Map<Suit, List<Card>> 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<Card> flushCards) {
|
||||
List<Integer> 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<Card> flushCards) {
|
||||
if (flushCards == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Integer> 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<Integer, Long> 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<Integer, Long> rankCount) {
|
||||
|
||||
List<Integer> trips = rankCount.entrySet()
|
||||
.stream()
|
||||
.filter(e -> e.getValue() >= THREE_OF_A_KIND)
|
||||
.map(Map.Entry::getKey)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.toList();
|
||||
|
||||
List<Integer> 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<Integer, Long> 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<Integer, Long> rankCount) {
|
||||
|
||||
long pairCount = rankCount.values()
|
||||
.stream()
|
||||
.filter(v -> v == PAIR)
|
||||
.count();
|
||||
|
||||
if (pairCount < PAIR) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Integer> 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<Integer, Long> 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<Integer, Long> 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<Integer> ranks) {
|
||||
|
||||
Set<Integer> 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<Integer> 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;
|
||||
}
|
||||
}
|
||||
+106
@@ -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<HandRank> {
|
||||
|
||||
/**
|
||||
* 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<Integer> 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<Integer> 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<Integer> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user