diff --git a/src/main/java/org/example/base/domain/components/Player.java b/src/main/java/org/example/base/domain/components/Player.java index 9d5aebd..3ec8e8b 100644 --- a/src/main/java/org/example/base/domain/components/Player.java +++ b/src/main/java/org/example/base/domain/components/Player.java @@ -13,10 +13,9 @@ @RequiredArgsConstructor public class Player { - public Player(String name, Integer chips, Integer position) { + public Player(String name, Integer chips) { this.name = name; this.chips = chips; - this.position = position; } @Getter @@ -35,6 +34,10 @@ public Player(String name, Integer chips, Integer position) { @Setter private List pocketCards; + @Getter + @Setter + public boolean isActive = true; + public Hand getBestHand(List communityCards) { ArrayList combinedCards = new ArrayList<>(); combinedCards.addAll(communityCards); @@ -44,15 +47,15 @@ public Hand getBestHand(List communityCards) { public Integer placeBet(Integer bet) { if (bet > chips) { - return null; + throw new IllegalArgumentException( + String.format("Player %s can not bet %s. Player chips: %s", name, bet, chips)); } chips -= bet; return chips; } - public Integer winPot(Integer pot) { + public void winPot(Integer pot) { chips += pot; - return chips; } public String logStatus() { diff --git a/src/main/java/org/example/base/domain/components/Round.java b/src/main/java/org/example/base/domain/components/Round.java index fe0e6c3..9d00da4 100644 --- a/src/main/java/org/example/base/domain/components/Round.java +++ b/src/main/java/org/example/base/domain/components/Round.java @@ -7,11 +7,13 @@ import org.example.base.domain.enums.Stage; import org.example.base.domain.utils.DealUtils; import org.example.base.domain.utils.GeneralUtils; +import org.example.base.domain.utils.RoundUtils; import java.util.ArrayList; import java.util.List; import static org.example.base.domain.utils.GeneralUtils.HOLDEM_POCKET_SIZE; +import static org.example.base.domain.utils.GeneralUtils.getLabelFromCards; @RequiredArgsConstructor public class Round { @@ -26,6 +28,9 @@ public class Round { @Getter private Stage stage = Stage.PREFLOP; + @Getter + private Integer pot = 0; + @Getter private List flop; @@ -85,21 +90,16 @@ private void progressStage() { } } - @SuppressWarnings("unused") public List getCommunityCards() { ArrayList communityCards = new ArrayList<>(flop); - if (stage == Stage.FLOP) { - return communityCards; - } if (stage == Stage.TURN) { communityCards.add(turn); - return communityCards; } if (stage == Stage.RIVER) { + communityCards.add(turn); communityCards.add(river); - return communityCards; } - return List.of(); + return communityCards; } private void logCardsDealt() { @@ -131,4 +131,38 @@ private void checkStage(Stage stage) { public String printRoundLog() { return String.join("", roundLog); } + + public void incrementPot(Integer amount) { + pot += amount; + } + + public List getActivePlayers() { + return players.stream().filter(Player::isActive).toList(); + } + + public List getWinningPlayers() { + return RoundUtils.getWinningPlayers(getActivePlayers(), getCommunityCards()); + } + + public void distributePot() { + List winningPlayers = getWinningPlayers(); + String winningPlayerSummary = formulateWinningPlayerSummary(winningPlayers); + Integer numWinners = winningPlayers.size(); + Integer potShare = pot / numWinners; + String plural = numWinners == 1 ? "" : "s"; + String potReport = String.format("Final pot size: %s | Winner%s: %s", pot, plural, winningPlayerSummary); + + roundLog.add(potReport); + winningPlayers.forEach(player -> player.winPot(potShare)); + } + + private String formulateWinningPlayerSummary(List winningPlayers) { + List winningPlayersSummaries = new ArrayList<>(); + winningPlayers.forEach(player -> { + Hand hand = player.getBestHand(getCommunityCards()); + String description = String.format("%s - %s - %s", player.getName(), hand.getHandType(), getLabelFromCards(hand.getCards())); + winningPlayersSummaries.add(description); + }); + return String.join(" // ", winningPlayersSummaries); + } } diff --git a/src/main/java/org/example/base/domain/enums/HandType.java b/src/main/java/org/example/base/domain/enums/HandType.java index ec138f8..030ce05 100644 --- a/src/main/java/org/example/base/domain/enums/HandType.java +++ b/src/main/java/org/example/base/domain/enums/HandType.java @@ -5,16 +5,16 @@ @RequiredArgsConstructor public enum HandType { - ROYAL_FLUSH("Royal flush", 10), - STRAIGHT_FLUSH("Straight flush", 9), - FOUR_KIND("Four of a kind", 8), - FULL_HOUSE("Full house", 7), + ROYAL_FLUSH("Royal Flush", 10), + STRAIGHT_FLUSH("Straight Flush", 9), + FOUR_KIND("Four of a Kind", 8), + FULL_HOUSE("Full House", 7), FLUSH("Flush", 6), STRAIGHT("Straight", 5), - THREE_KIND("Three of a kind", 4), - TWO_PAIR("Two pair", 3), + THREE_KIND("Three of a Kind", 4), + TWO_PAIR("Two Pair", 3), PAIR("Pair", 2), - HIGH_CARD("High card", 1); + HIGH_CARD("High Card", 1); @Getter final String name; @@ -22,4 +22,9 @@ public enum HandType { @Getter final Integer rank; + @Override + public final String toString() { + return name; + } + } diff --git a/src/main/java/org/example/base/domain/enums/PlayerAction.java b/src/main/java/org/example/base/domain/enums/PlayerAction.java new file mode 100644 index 0000000..f56c076 --- /dev/null +++ b/src/main/java/org/example/base/domain/enums/PlayerAction.java @@ -0,0 +1,8 @@ +package org.example.base.domain.enums; + +public enum PlayerAction { + CHECK, + BET, + RAISE, + FOLD +} diff --git a/src/main/java/org/example/base/domain/utils/RoundUtils.java b/src/main/java/org/example/base/domain/utils/RoundUtils.java new file mode 100644 index 0000000..5a9d560 --- /dev/null +++ b/src/main/java/org/example/base/domain/utils/RoundUtils.java @@ -0,0 +1,34 @@ +package org.example.base.domain.utils; + +import org.example.base.domain.components.Hand; +import org.example.base.domain.components.Player; +import org.example.base.domain.enums.Card; + +import static org.example.base.domain.utils.GeneralUtils.getLabelFromCards; + +import java.util.List; + +public class RoundUtils { + + private RoundUtils() { + throw new IllegalStateException("RoundUtils is a static class"); + } + + public static List getPlayerHands(List players, List communityCards) { + return players.stream().map(player -> player.getBestHand(communityCards)).toList(); + } + + public static List getWinningPlayers(List players, List communityCards) { + List activeHands = RoundUtils.getPlayerHands(players, communityCards); + List winningHands = CompareHandsUtils.getWinningHands(activeHands); + List winningHandsCardsLabels = winningHands.stream() + .map(hand -> getLabelFromCards(hand.getCards())) + .toList(); + return players.stream() + .filter(player -> { + String labelFromPlayerCards = getLabelFromCards(player.getBestHand(communityCards).getCards()); + return winningHandsCardsLabels.contains(labelFromPlayerCards); + }) + .toList(); + } +} diff --git a/src/test/java/org/example/base/domain/components/RoundTest.java b/src/test/java/org/example/base/domain/components/RoundTest.java index 1f0eb86..4b32eb8 100644 --- a/src/test/java/org/example/base/domain/components/RoundTest.java +++ b/src/test/java/org/example/base/domain/components/RoundTest.java @@ -5,7 +5,10 @@ import org.example.base.domain.utils.GeneralUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -25,8 +28,10 @@ class RoundTest { @BeforeEach void setUp() { round = new Round(); - player1 = new Player("Red", 100, 0); - player2 = new Player("Blue", 150, 1); + player1 = new Player("Red", 100); + player2 = new Player("Blue", 150); + player1.setPosition(0); + player2.setPosition(1); round.setPlayers(List.of(player1, player2)); round.dealPocketCards(); } @@ -96,4 +101,43 @@ void stageIsProgressed() { assertThat(round.getStage()).isEqualTo(Stage.RIVER); } + @Test + void incrementPot() { + assertThat(round.getPot()).isZero(); + round.incrementPot(5); + assertThat(round.getPot()).isEqualTo(5); + round.incrementPot(11); + assertThat(round.getPot()).isEqualTo(16); + } + + @Test + void getActivePlayers() { + Player player3 = new Player("Yellow", 0); + player3.setActive(false); + assertThat(round.getActivePlayers()) + .hasSize(2) + .allMatch(Player::isActive) + .containsOnly(player1, player2); + } + + @ParameterizedTest + @ValueSource(ints = {1,2,3,4,5,6,7,8,9,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}) + void distributePot() { + int initialPlayerChips = player1.getChips() + player2.getChips(); + round.incrementPot(120); + round.dealFlop(); + round.dealTurn(); + round.dealRiver(); + round.distributePot(); + + ArrayList roundLog = round.getRoundLog(); + String potReport = roundLog.get(roundLog.size() - 1); + String regex = "Final pot size: 120 \\| Winners?: (\\w+ - [\\w ]+ - (?>[AKQJTZ2-9][SCHD],?){5}( // )?)+"; + assertThat(potReport).matches(regex); + + int resultingPlayerChips = player1.getChips() + player2.getChips(); + assertThat(resultingPlayerChips).isEqualTo(initialPlayerChips + 120); + } + + } \ No newline at end of file diff --git a/src/test/java/org/example/base/domain/utils/RoundUtilsTest.java b/src/test/java/org/example/base/domain/utils/RoundUtilsTest.java new file mode 100644 index 0000000..5fcc5cf --- /dev/null +++ b/src/test/java/org/example/base/domain/utils/RoundUtilsTest.java @@ -0,0 +1,70 @@ +package org.example.base.domain.utils; + +import lombok.val; +import org.example.base.domain.components.Hand; +import org.example.base.domain.components.Player; +import org.example.base.domain.enums.Card; +import org.example.base.domain.enums.HandType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.example.base.domain.utils.GeneralUtils.getCardsFromLabel; + +import java.util.List; + + +class RoundUtilsTest { + + Player player1; + Player player2; + List communityCards; + + @BeforeEach + void setUp() { + player1 = new Player("Player1", 100); + player2 = new Player("Player2", 150); + communityCards = getCardsFromLabel("KC,TS,4H,9C,4C"); + player1.setPocketCards(getCardsFromLabel("4S,TD")); + player2.setPocketCards(getCardsFromLabel("AC,6C")); + } + + @Test + void getPlayerHands() { + Hand p1ExpectedHand = new Hand(HandType.FULL_HOUSE, getCardsFromLabel("4S,4C,4H,TS,TD")); + Hand p2ExpectedHand = new Hand(HandType.FLUSH, getCardsFromLabel("AC,KC,9C,6C,4C")); + + List result = RoundUtils.getPlayerHands(List.of(player1, player2), communityCards); + + assertThat(result).hasSize(2); + + val p1Hand = result.get(0); + assertThat(p1Hand.getHandType()).isEqualTo(p1ExpectedHand.getHandType()); + assertThat(p1Hand.getCards()).containsAll(p1ExpectedHand.getCards()); + + val p2Hand = result.get(1); + assertThat(p2Hand.getHandType()).isEqualTo(p2ExpectedHand.getHandType()); + assertThat(p2Hand.getCards()).containsAll(p2ExpectedHand.getCards()); + } + + @Test + void getWinningPlayers_returnsSingleWinner() { + val result = RoundUtils.getWinningPlayers(List.of(player1, player2), communityCards); + assertThat(result).hasSize(1); + Player winningPlayer = result.get(0); + assertThat(winningPlayer.getName()).isEqualTo(player1.getName()); + } + + @Test + void getWinningPlayers_returnsMultipleWinners() { + player1.setPocketCards(getCardsFromLabel("KS,2D")); + player2.setPocketCards(getCardsFromLabel("KH,3D")); + + val result = RoundUtils.getWinningPlayers(List.of(player1, player2), communityCards); + assertThat(result).hasSize(2); + Player winningPlayer1 = result.get(0); + assertThat(winningPlayer1.getName()).isEqualTo(player1.getName()); + Player winningPlayer2 = result.get(1); + assertThat(winningPlayer2.getName()).isEqualTo(player2.getName()); + } +} \ No newline at end of file