From 5e950275fa4f990d09a3b8dc92786c44e2a9be55 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Wed, 2 Mar 2022 22:15:44 -0600 Subject: [PATCH] Refactor Game.play() to handle split hands more elegantly --- 10_Blackjack/java/src/Game.java | 135 +++++++++------------------ 10_Blackjack/java/src/Player.java | 88 ++++++++++++----- 10_Blackjack/java/test/GameTest.java | 94 ++++++++++++++----- 3 files changed, 180 insertions(+), 137 deletions(-) diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index a46f00db..6ec5f101 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -138,119 +138,72 @@ public class Game { * @param player */ protected void play(Player player) { - String action = userIo.prompt("PLAYER " + player.getPlayerNumber() + " "); + play(player, 1); + } + + private void play(Player player, int handNumber) { + List hand = player.getHand(handNumber); + + String action; + if(player.isSplit()){ + action = userIo.prompt("HAND #" + handNumber); + } else { + action = userIo.prompt("PLAYER " + player.getPlayerNumber() + " "); + } while(true){ if(action.equalsIgnoreCase("H")){ // HIT Card c = deck.deal(); - player.dealCard(c); - if(scoreHand(player.getHand()) > 21){ + player.dealCard(c, handNumber); + if(scoreHand(hand) > 21){ userIo.println("...BUSTED"); return; } action = userIo.prompt("RECEIVED A " + c.toString() + " HIT"); } else if(action.equalsIgnoreCase("S")){ // STAY return; - } else if(player.getHand().size() == 2 && action.equalsIgnoreCase("D")) { // DOUBLE DOWN - player.setCurrentBet(player.getCurrentBet() * 2); - player.dealCard(deck.deal()); + } else if(action.equalsIgnoreCase("D") && player.canDoubleDown(handNumber)) { // DOUBLE DOWN + player.doubleDown(deck.deal(), handNumber); return; - } else if(player.getHand().size() == 2 && action.equalsIgnoreCase("/")) { // SPLIT - if(player.getHand().get(0).equals(player.getHand().get(1))){ - playSplit(player); + } else if(action.equalsIgnoreCase("/")) { // SPLIT + if(player.isSplit()) { + // The original basic code printed different output + // if a player tries to split twice vs if they try to split + // a non-pair hand. + action = userIo.prompt("TYPE H, S OR D, PLEASE"); + } else if(player.canSplit()) { + player.split(); + Card card = deck.deal(); + player.dealCard(card, 1); + // TODO move the "a" vs "an" logic to userIo or Card + if(card.getValue() == 1 || card.getValue() == 8) { + userIo.println("FIRST HAND RECEIVES AN " + card.toString()); + } else { + userIo.println("FIRST HAND RECEIVES A " + card.toString()); + } + play(player, 1); + card = deck.deal(); + player.dealCard(card, 2); + if(card.getValue() == 1 || card.getValue() == 8) { + userIo.println("SECOND HAND RECEIVES AN " + card.toString()); + } else { + userIo.println("SECOND HAND RECEIVES A " + card.toString()); + } + play(player, 2); return; } else { userIo.println("SPLITTING NOT ALLOWED"); action = userIo.prompt("PLAYER " + player.getPlayerNumber() + " "); } } else { - if(player.getHand().size() > 2) { - action = userIo.prompt("TYPE H, OR S, PLEASE"); - } else { + if(player.getHand(handNumber).size() == 2) { action = userIo.prompt("TYPE H,S,D, OR /, PLEASE"); + } else { + action = userIo.prompt("TYPE H, OR S, PLEASE"); } } } } - /** - * Splits the players hand and deals a card to each hand then prompts the user to - * hit (H), stay (S), or double down (D), and then performs those actions. - * - * @param player - */ - protected void playSplit(Player player) { - // TODO refactor to avoid so much logic duplication - - player.split(); - // DEAL CARDS - Card card = deck.deal(); - player.dealCard(card); - if(card.getValue() == 1 || card.getValue() == 8) { - userIo.println("FIRST HAND RECEIVES AN " + card.toString()); - } else { - userIo.println("FIRST HAND RECEIVES A " + card.toString()); - } - card = deck.deal(); - player.dealSplitHandCard(card); - if(card.getValue() == 1 || card.getValue() == 8) { - userIo.println("SECOND HAND RECEIVES AN " + card.toString()); - } else { - userIo.println("SECOND HAND RECEIVES A " + card.toString()); - } - - // Play hand 1 - String action = userIo.prompt("HAND 1"); - while(true){ - if(action.equalsIgnoreCase("H")){ // HIT - Card c = deck.deal(); - player.dealCard(c); - if(scoreHand(player.getHand()) > 21){ - userIo.println("...BUSTED"); - break; - } - action = userIo.prompt("RECEIVED A " + c.toString() + " HIT"); - } else if(action.equalsIgnoreCase("S")){ // STAY - break; - } else if(player.getHand().size() == 2 && action.equalsIgnoreCase("D")) { // DOUBLE DOWN - player.setCurrentBet(player.getCurrentBet() * 2); - player.dealCard(deck.deal()); - break; - } else { - if(player.getHand().size() > 2) { - action = userIo.prompt("TYPE H, OR S, PLEASE"); - } else { - action = userIo.prompt("TYPE H, S OR D, PLEASE"); - } - } - } - // Play hand 2 - action = userIo.prompt("HAND 2"); - while(true){ - if(action.equalsIgnoreCase("H")){ // HIT - Card c = deck.deal(); - player.dealSplitHandCard(card); - if(scoreHand(player.getSplitHand()) > 21){ - userIo.println("...BUSTED"); - break; - } - action = userIo.prompt("RECEIVED A " + c.toString() + " HIT"); - } else if(action.equalsIgnoreCase("S")){ // STAY - break; - } else if(player.getSplitHand().size() == 2 && action.equalsIgnoreCase("D")) { // DOUBLE DOWN - player.setSplitBet(player.getSplitBet() * 2); - player.dealSplitHandCard(card); - break; - } else { - if(player.getSplitHand().size() > 2) { - action = userIo.prompt("TYPE H, OR S, PLEASE"); - } else { - action = userIo.prompt("TYPE H, S OR D, PLEASE"); - } - } - } - //TODO Uncomment playSplit tests and adjust as needed - } - /** * Calculates the value of a hand. When the hand contains aces, it will diff --git a/10_Blackjack/java/src/Player.java b/10_Blackjack/java/src/Player.java index e130a9e6..747125e0 100644 --- a/10_Blackjack/java/src/Player.java +++ b/10_Blackjack/java/src/Player.java @@ -1,4 +1,6 @@ +import java.util.Collections; import java.util.LinkedList; +import java.util.List; public class Player { @@ -20,31 +22,23 @@ public class Player { splitBet = 0; total = 0; hand = new LinkedList<>(); - splitHand = new LinkedList<>(); - } - - public void setPlayerNumber(int playerNumber) { - this.playerNumber = playerNumber; + splitHand = null; } public int getPlayerNumber() { return this.playerNumber; } - public void setCurrentBet(double currentBet) { - this.currentBet = currentBet; - } - public double getCurrentBet() { return this.currentBet; } - public double getSplitBet() { - return splitBet; + public void setCurrentBet(double currentBet) { + this.currentBet = currentBet; } - public void setSplitBet(double splitBet) { - this.splitBet = splitBet; + public double getSplitBet() { + return splitBet; } public double getInsuranceBet() { @@ -79,31 +73,81 @@ public class Player { // dealCard adds the given card to the player's hand public void dealCard(Card card) { - hand.add(card); + dealCard(card, 1); } - public void dealSplitHandCard(Card card) { - splitHand.add(card); + public void dealCard(Card card, int handNumber) { + if(handNumber == 1) { + hand.add(card); + } else if (handNumber == 2) { + splitHand.add(card); + } else { + throw new IllegalArgumentException("Invalid hand number " + handNumber); + } } + + public boolean canSplit() { + if(isSplit()) { + // Can't split twice + return false; + } else { + boolean isPair = this.hand.get(0).getValue() == this.hand.get(1).getValue(); + return isPair; + } + } + + public boolean isSplit() { + return this.splitHand != null; + } + /** - * Removes first card from hand to adds it to split hand + * Removes first card from hand to add it to new split hand */ public void split() { this.splitBet = this.currentBet; + this.splitHand = new LinkedList<>(); splitHand.add(hand.pop()); } + public boolean canDoubleDown(int handNumber) { + if(handNumber == 1){ + return this.hand.size() == 2; + } else if(handNumber == 2){ + return this.splitHand.size() == 2; + } else { + throw new IllegalArgumentException("Invalid hand number " + handNumber); + } + } + + public void doubleDown(Card card, int handNumber) { + if(handNumber == 1){ + this.currentBet = this.currentBet * 2; + } else if(handNumber == 2){ + this.splitBet = this.splitBet * 2; + } else { + throw new IllegalArgumentException("Invalid hand number " + handNumber); + } + this.dealCard(card, handNumber); + } + // resetHand resets 'hand' & 'splitHand' to empty lists public void resetHand() { this.hand = new LinkedList<>(); - this.splitHand = new LinkedList<>(); + this.splitHand = null; } - public LinkedList getHand() { - return this.hand; + public List getHand() { + return getHand(1); } - public LinkedList getSplitHand() { - return this.splitHand; + public List getHand(int handNumber) { + if(handNumber == 1){ + return Collections.unmodifiableList(this.hand); + } else if(handNumber == 2){ + return Collections.unmodifiableList(this.splitHand); + } else { + throw new IllegalArgumentException("Invalid hand number " + handNumber); + } } + } \ No newline at end of file diff --git a/10_Blackjack/java/test/GameTest.java b/10_Blackjack/java/test/GameTest.java index 3bd35f31..61e17e58 100644 --- a/10_Blackjack/java/test/GameTest.java +++ b/10_Blackjack/java/test/GameTest.java @@ -1,6 +1,8 @@ import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -44,6 +46,11 @@ public class GameTest { game = new Game(deck, userIo); } + @AfterEach + private void printOutput() { + System.out.println(out.toString()); + } + @Test public void shouldQuitOnCtrlD() { // Given @@ -322,16 +329,16 @@ public class GameTest { } @Test - @DisplayName("playSplit() should end on STAY") + @DisplayName("play() should end on STAY after split") public void playSplitEndOnStay(){ // Given Player player = new Player(1); player.dealCard(new Card(1, Card.Suit.CLUBS)); player.dealCard(new Card(1, Card.Suit.SPADES)); - givenInput("S\nS\n"); + givenInput("/\nS\nS\n"); // When - game.playSplit(player); + game.play(player); // Then assertTrue(out.toString().contains("FIRST HAND RECEIVES")); @@ -339,28 +346,47 @@ public class GameTest { } @Test - @DisplayName("playSplit() should allow HIT until BUST") + @DisplayName("play() should allow HIT until BUST after split") public void playSplitHitUntilBust() { // Given Player player = new Player(1); player.dealCard(new Card(10, Card.Suit.HEARTS)); player.dealCard(new Card(10, Card.Suit.SPADES)); - givenInput("H\nH\n", - new Card(12, Card.Suit.SPADES), // 20 - new Card(12, Card.Suit.HEARTS), // Split hand 20 - new Card(12, Card.Suit.DIAMONDS), // 30 - new Card(12, Card.Suit.CLUBS)); // Split hand 30 + givenInput("/\nH\nS\n", + new Card(12, Card.Suit.SPADES), // First hand has 20. Player hits. + new Card(12, Card.Suit.HEARTS), // First hand busted + new Card(10, Card.Suit.HEARTS)); // Second hand gets a 10. Player stays. // When - game.playSplit(player); + game.play(player); // Then assertTrue(out.toString().contains("BUSTED")); } @Test - @DisplayName("playSplit should allow double down") + @DisplayName("play() should allow HIT on split hand until BUST") + public void playSplitHitUntilBustHand2() { + // Given + Player player = new Player(1); + player.dealCard(new Card(10, Card.Suit.HEARTS)); + player.dealCard(new Card(10, Card.Suit.SPADES)); + + givenInput("/\nS\nH\nH\n", + new Card(1, Card.Suit.CLUBS), // Dealt to first split hand. Player stays. + new Card(12, Card.Suit.SPADES), // Split hand = 20 + new Card(12, Card.Suit.HEARTS)); // Split hand busted + + // When + game.play(player); + + // Then + assertTrue(out.toString().contains("BUSTED")); + } + + @Test + @DisplayName("play() should allow double down on split hands") public void playSplitDoubleDown(){ // Given Player player = new Player(1); @@ -368,40 +394,60 @@ public class GameTest { player.dealCard(new Card(9, Card.Suit.HEARTS)); player.dealCard(new Card(9, Card.Suit.SPADES)); - givenInput("D\nD\n", + givenInput("/\nD\nD\n", + new Card(5, Card.Suit.DIAMONDS), new Card(6, Card.Suit.HEARTS), - new Card(7, Card.Suit.HEARTS), - new Card(6, Card.Suit.CLUBS), new Card(7, Card.Suit.CLUBS)); // When - game.playSplit(player); + game.play(player); // Then - assertTrue(player.getCurrentBet() == 200); - assertTrue(player.getSplitBet() == 200); - assertTrue(player.getHand().size() == 3); - assertTrue(player.getSplitHand().size() == 3); + assertAll( + () -> assertEquals(200, player.getCurrentBet(), "Current bet should be doubled"), + () -> assertEquals(200, player.getSplitBet(), "Split bet should be doubled"), + () -> assertEquals(3, player.getHand(1).size(), "First hand should have exactly three cards"), + () -> assertEquals(3, player.getHand(2).size(), "Second hand should have exactly three cards") + ); } @Test - @DisplayName("playSplit should NOT allow re-splitting") - public void playSplitDoubleDownLate(){ + @DisplayName("play() should NOT allow re-splitting first split hand") + public void playSplitTwice(){ // Given Player player = new Player(1); player.setCurrentBet(100); player.dealCard(new Card(1, Card.Suit.HEARTS)); player.dealCard(new Card(1, Card.Suit.SPADES)); - givenInput("/\nS\nS\n", - new Card(13, Card.Suit.HEARTS), + givenInput("/\n/\nS\nS\n", + new Card(13, Card.Suit.CLUBS), new Card(13, Card.Suit.SPADES)); // When - game.playSplit(player); + game.play(player); // Then assertTrue(out.toString().contains("TYPE H, S OR D, PLEASE")); } + @Test + @DisplayName("play() should NOT allow re-splitting second split hand") + public void playSplitTwiceHand2(){ + // Given + Player player = new Player(1); + player.setCurrentBet(100); + player.dealCard(new Card(1, Card.Suit.HEARTS)); + player.dealCard(new Card(1, Card.Suit.SPADES)); + + givenInput("/\nS\n/\nS\n", + new Card(13, Card.Suit.SPADES), + new Card(13, Card.Suit.SPADES)); + + // When + game.play(player); + + // Then + assertTrue(out.toString().contains("TYPE H, S OR D, PLEASE")); + } }