From daa12b0ef889fbbdfb6d00d72137797105a19817 Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:15:17 +0200 Subject: [PATCH] fix: settle blackjack naturals before comparing totals A natural (ace and ten-value card as the first two cards of an unsplit hand) now beats any other 21 on either side: a player natural is paid 3 to 2 against a dealer who draws to 21, a dealer natural takes a player hand that reached 21 with more cards or after a split, and two naturals push. Split ace-and-ten stays 21 paid even money. The help book gains a page saying so. Co-Authored-By: Claude Opus 5.5 --- .../tfminecraft/games/game/BlackjackGame.java | 18 +++++- src/main/resources/help.yml | 1 + .../games/game/BlackjackGameTest.java | 61 +++++++++++++++++++ 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/tfminecraft/games/game/BlackjackGame.java b/src/main/java/net/tfminecraft/games/game/BlackjackGame.java index bf12d59..d803052 100644 --- a/src/main/java/net/tfminecraft/games/game/BlackjackGame.java +++ b/src/main/java/net/tfminecraft/games/game/BlackjackGame.java @@ -1206,8 +1206,10 @@ private void settle(Table table) { table.setPhase(SETTLE); checkBoxBets(table, "settle"); TableManager manager = TableManager.get(); - int dealerTotal = bestTotal(table.tablePile(DEALER)); + List dealerPile = table.tablePile(DEALER); + int dealerTotal = bestTotal(dealerPile); boolean dealerBust = dealerTotal > 21; + boolean dealerNatural = isDealerNatural(dealerPile); Player dealer = dealerAtShoe(table); Map collect = new HashMap<>(); Map pay = new HashMap<>(); @@ -1220,12 +1222,14 @@ private void settle(Table table) { boolean bust = playerTotal > 21 || cards.isEmpty(); boolean natural = isNatural(hand, cards); Player player = Bukkit.getPlayer(hand.owner); - if (bust || (!dealerBust && playerTotal < dealerTotal)) { + // A natural is settled before any totals: it beats any other 21, and only another + // natural draws with it. Every other hand is decided on its total alone. + if (bust || (dealerNatural && !natural) || (!dealerBust && playerTotal < dealerTotal)) { collect.merge(hand.owner, hand.bet, Integer::sum); tell(player, Messages.get("bet.lose")); continue; } - if (!dealerBust && playerTotal == dealerTotal) { + if (natural == dealerNatural && !dealerBust && playerTotal == dealerTotal) { tell(player, Messages.get("bet.push")); continue; } @@ -1376,6 +1380,14 @@ private static boolean isNatural(BjHand hand, List cards) { return !hand.fromSplit && cards.size() == 2 && bestTotal(cards) == 21; } + /** + * The dealer's two dealt cards making twenty-one. The dealer never draws on twenty-one, so a + * pile that still holds exactly two cards at the settle holds the two that were dealt. + */ + private static boolean isDealerNatural(List pile) { + return pile.size() == 2 && bestTotal(pile) == 21; + } + /** Two cards of one rank, or two ten-value cards. Only ever asked about two cards. */ private static boolean isPair(List cards) { Card a = cards.get(0).card(); diff --git a/src/main/resources/help.yml b/src/main/resources/help.yml index 09a9235..316c688 100644 --- a/src/main/resources/help.yml +++ b/src/main/resources/help.yml @@ -37,6 +37,7 @@ blackjack: - "#6b3f1d&lSplitting aces\n\n#232323Aces are the exception. Each one gets a single card and that hand is done, whatever it comes to.\n\nAnother ace can be split again, if the table allows it and you have hands to spare.\n\nAn ace and a ten after a split is 21, but it is not blackjack and pays even money." - "#6b3f1d&lThe dealer's turn\n\n#232323Once everybody is done the dealer turns over the hidden card and keeps drawing until reaching 17, then stops there.\n\nNothing you can do at this point but watch." - "#6b3f1d&lGetting paid\n\n#232323Beat the dealer and your bet comes back doubled.\n\n21 on your first two cards pays 3 to 2.\n\nDraw with the dealer and your money comes back." + - "#6b3f1d&lBlackjack beats 21\n\n#232323Blackjack is 21 on your first two cards. It beats any other 21, yours or the dealer's.\n\nA dealer blackjack also takes any doubles and splits.\n\nTwo blackjacks draw." - "#6b3f1d&lNo insurance\n\n#232323This house does not offer insurance and it does not offer surrender. If the dealer shows an ace, that is simply your problem.\n\nCoins land back on the felt when you win." - "#6b3f1d&lDealing it yourself\n\n#232323Stand at the dealer's place and right click the shoe. The table is yours.\n\nRight click it again when you have had enough and somebody else can take over." - "#6b3f1d&lRunning a round\n\n#232323/games bet min 10\n/games bet max 500\n/games bet open\n\n#232323Then let people put their money down." diff --git a/src/test/java/net/tfminecraft/games/game/BlackjackGameTest.java b/src/test/java/net/tfminecraft/games/game/BlackjackGameTest.java index a5e77b9..755b3e8 100644 --- a/src/test/java/net/tfminecraft/games/game/BlackjackGameTest.java +++ b/src/test/java/net/tfminecraft/games/game/BlackjackGameTest.java @@ -309,6 +309,37 @@ void naturalTwentyOneSkipsActionAndPaysThreeToTwo() { assertMessage(player, "bet.natural"); } + @Test + void naturalBeatsADealerWhoDrawsToTwentyOne() { + start(1, 10, 13, 5, 6); + assertEquals(3, table.tablePile("dealer").size()); + assertEquals(21, BlackjackGame.bestTotal(table.tablePile("dealer"))); + verifyWin(30); + assertMessage(player, "bet.natural"); + } + + @Test + void dealerNaturalBeatsAPlayerWhoDrawsToTwentyOne() { + start(5, 1, 6, 13, 10); + game.onBetHit(table, player); + finishAnimations(); + assertEquals(21, BlackjackGame.bestTotal(table.handOf(player.getUniqueId()))); + assertEquals(BlackjackGame.SETTLE, table.phase()); + assertEquals(2, table.tablePile("dealer").size()); + verify(wagers).refund(eq(table), eq(player.getUniqueId()), eq(dealer), eq(20), anyList(), eq("loss to dealer")); + verify(wagers, never()).payWin(any(), any(), any(), anyInt(), any(), anyBoolean(), any(), anyList()); + assertMessage(player, "bet.lose"); + } + + @Test + void twoNaturalsPush() { + start(1, 1, 10, 13); + assertEquals(BlackjackGame.SETTLE, table.phase()); + verify(wagers, never()).payWin(any(), any(), any(), anyInt(), any(), anyBoolean(), any(), anyList()); + verify(wagers).refund(eq(table), eq(player.getUniqueId()), eq(player), eq(0), anyList(), eq("bet returned")); + assertMessage(player, "bet.push"); + } + @Test void equalTotalsReturnStakeWithoutWinPayment() { start(10, 10, 8, 8); @@ -443,6 +474,36 @@ void splitAcesReceiveOneCardEachAndTwentyOnePaysEvenMoney() { verifyWin(40); } + @Test + void splitAceAndTenIsTwentyOneButNotANatural() { + start(1, 10, 1, 8, 10, 13); + game.onBetSplit(table, player); + finishAnimations(); + assertEquals(List.of(1, 10), ranks(0)); + assertEquals(List.of(1, 13), ranks(1)); + assertEquals(BlackjackGame.SETTLE, table.phase()); + verifyWin(40); + List messages = new ArrayList<>(); + String message; + while ((message = player.nextMessage()) != null) { + messages.add(message); + } + assertFalse(messages.contains(Messages.get("bet.natural"))); + assertEquals(2, messages.stream().filter(Messages.get("bet.win")::equals).count()); + } + + @Test + void splitAceAndTenLosesBothStakesToADealerNatural() { + start(1, 1, 1, 13, 10, 10); + game.onBetSplit(table, player); + finishAnimations(); + assertEquals(List.of(1, 10), ranks(0)); + assertEquals(List.of(1, 10), ranks(1)); + assertEquals(BlackjackGame.SETTLE, table.phase()); + verify(wagers).refund(eq(table), eq(player.getUniqueId()), eq(dealer), eq(40), anyList(), eq("loss to dealer")); + verify(wagers, never()).payWin(any(), any(), any(), anyInt(), any(), anyBoolean(), any(), anyList()); + } + @Test void enabledResplitAcesOffersNewAcePairWithoutAllowingHitOrDouble() { configure(false, 3, true, 0);