Feat/Game Engine #233

Merged
j.kropff merged 18 commits from feat/game-engine into main 2026-04-05 11:26:12 +02:00
2 changed files with 519 additions and 0 deletions
Showing only changes of commit a20ba7cef5 - Show all commits
@@ -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;
}
}
@@ -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;
}
}