Refactor Game.play() to handle split hands more elegantly

This commit is contained in:
Dave Burke
2022-03-02 22:15:44 -06:00
parent ccbed873e5
commit 5e950275fa
3 changed files with 180 additions and 137 deletions

View File

@@ -138,119 +138,72 @@ public class Game {
* @param player * @param player
*/ */
protected void play(Player player) { protected void play(Player player) {
String action = userIo.prompt("PLAYER " + player.getPlayerNumber() + " "); play(player, 1);
}
private void play(Player player, int handNumber) {
List<Card> hand = player.getHand(handNumber);
String action;
if(player.isSplit()){
action = userIo.prompt("HAND #" + handNumber);
} else {
action = userIo.prompt("PLAYER " + player.getPlayerNumber() + " ");
}
while(true){ while(true){
if(action.equalsIgnoreCase("H")){ // HIT if(action.equalsIgnoreCase("H")){ // HIT
Card c = deck.deal(); Card c = deck.deal();
player.dealCard(c); player.dealCard(c, handNumber);
if(scoreHand(player.getHand()) > 21){ if(scoreHand(hand) > 21){
userIo.println("...BUSTED"); userIo.println("...BUSTED");
return; return;
} }
action = userIo.prompt("RECEIVED A " + c.toString() + " HIT"); action = userIo.prompt("RECEIVED A " + c.toString() + " HIT");
} else if(action.equalsIgnoreCase("S")){ // STAY } else if(action.equalsIgnoreCase("S")){ // STAY
return; return;
} else if(player.getHand().size() == 2 && action.equalsIgnoreCase("D")) { // DOUBLE DOWN } else if(action.equalsIgnoreCase("D") && player.canDoubleDown(handNumber)) { // DOUBLE DOWN
player.setCurrentBet(player.getCurrentBet() * 2); player.doubleDown(deck.deal(), handNumber);
player.dealCard(deck.deal());
return; return;
} else if(player.getHand().size() == 2 && action.equalsIgnoreCase("/")) { // SPLIT } else if(action.equalsIgnoreCase("/")) { // SPLIT
if(player.getHand().get(0).equals(player.getHand().get(1))){ if(player.isSplit()) {
playSplit(player); // 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; return;
} else { } else {
userIo.println("SPLITTING NOT ALLOWED"); userIo.println("SPLITTING NOT ALLOWED");
action = userIo.prompt("PLAYER " + player.getPlayerNumber() + " "); action = userIo.prompt("PLAYER " + player.getPlayerNumber() + " ");
} }
} else { } else {
if(player.getHand().size() > 2) { if(player.getHand(handNumber).size() == 2) {
action = userIo.prompt("TYPE H, OR S, PLEASE");
} else {
action = userIo.prompt("TYPE H,S,D, OR /, PLEASE"); 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 * Calculates the value of a hand. When the hand contains aces, it will

View File

@@ -1,4 +1,6 @@
import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List;
public class Player { public class Player {
@@ -20,31 +22,23 @@ public class Player {
splitBet = 0; splitBet = 0;
total = 0; total = 0;
hand = new LinkedList<>(); hand = new LinkedList<>();
splitHand = new LinkedList<>(); splitHand = null;
}
public void setPlayerNumber(int playerNumber) {
this.playerNumber = playerNumber;
} }
public int getPlayerNumber() { public int getPlayerNumber() {
return this.playerNumber; return this.playerNumber;
} }
public void setCurrentBet(double currentBet) {
this.currentBet = currentBet;
}
public double getCurrentBet() { public double getCurrentBet() {
return this.currentBet; return this.currentBet;
} }
public double getSplitBet() { public void setCurrentBet(double currentBet) {
return splitBet; this.currentBet = currentBet;
} }
public void setSplitBet(double splitBet) { public double getSplitBet() {
this.splitBet = splitBet; return splitBet;
} }
public double getInsuranceBet() { public double getInsuranceBet() {
@@ -79,31 +73,81 @@ public class Player {
// dealCard adds the given card to the player's hand // dealCard adds the given card to the player's hand
public void dealCard(Card card) { public void dealCard(Card card) {
hand.add(card); dealCard(card, 1);
} }
public void dealSplitHandCard(Card card) { public void dealCard(Card card, int handNumber) {
splitHand.add(card); 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() { public void split() {
this.splitBet = this.currentBet; this.splitBet = this.currentBet;
this.splitHand = new LinkedList<>();
splitHand.add(hand.pop()); 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 // resetHand resets 'hand' & 'splitHand' to empty lists
public void resetHand() { public void resetHand() {
this.hand = new LinkedList<>(); this.hand = new LinkedList<>();
this.splitHand = new LinkedList<>(); this.splitHand = null;
} }
public LinkedList<Card> getHand() { public List<Card> getHand() {
return this.hand; return getHand(1);
} }
public LinkedList<Card> getSplitHand() { public List<Card> getHand(int handNumber) {
return this.splitHand; 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);
}
} }
} }

View File

@@ -1,6 +1,8 @@
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName; 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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -44,6 +46,11 @@ public class GameTest {
game = new Game(deck, userIo); game = new Game(deck, userIo);
} }
@AfterEach
private void printOutput() {
System.out.println(out.toString());
}
@Test @Test
public void shouldQuitOnCtrlD() { public void shouldQuitOnCtrlD() {
// Given // Given
@@ -322,16 +329,16 @@ public class GameTest {
} }
@Test @Test
@DisplayName("playSplit() should end on STAY") @DisplayName("play() should end on STAY after split")
public void playSplitEndOnStay(){ public void playSplitEndOnStay(){
// Given // Given
Player player = new Player(1); Player player = new Player(1);
player.dealCard(new Card(1, Card.Suit.CLUBS)); player.dealCard(new Card(1, Card.Suit.CLUBS));
player.dealCard(new Card(1, Card.Suit.SPADES)); player.dealCard(new Card(1, Card.Suit.SPADES));
givenInput("S\nS\n"); givenInput("/\nS\nS\n");
// When // When
game.playSplit(player); game.play(player);
// Then // Then
assertTrue(out.toString().contains("FIRST HAND RECEIVES")); assertTrue(out.toString().contains("FIRST HAND RECEIVES"));
@@ -339,28 +346,47 @@ public class GameTest {
} }
@Test @Test
@DisplayName("playSplit() should allow HIT until BUST") @DisplayName("play() should allow HIT until BUST after split")
public void playSplitHitUntilBust() { public void playSplitHitUntilBust() {
// Given // Given
Player player = new Player(1); Player player = new Player(1);
player.dealCard(new Card(10, Card.Suit.HEARTS)); player.dealCard(new Card(10, Card.Suit.HEARTS));
player.dealCard(new Card(10, Card.Suit.SPADES)); player.dealCard(new Card(10, Card.Suit.SPADES));
givenInput("H\nH\n", givenInput("/\nH\nS\n",
new Card(12, Card.Suit.SPADES), // 20 new Card(12, Card.Suit.SPADES), // First hand has 20. Player hits.
new Card(12, Card.Suit.HEARTS), // Split hand 20 new Card(12, Card.Suit.HEARTS), // First hand busted
new Card(12, Card.Suit.DIAMONDS), // 30 new Card(10, Card.Suit.HEARTS)); // Second hand gets a 10. Player stays.
new Card(12, Card.Suit.CLUBS)); // Split hand 30
// When // When
game.playSplit(player); game.play(player);
// Then // Then
assertTrue(out.toString().contains("BUSTED")); assertTrue(out.toString().contains("BUSTED"));
} }
@Test @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(){ public void playSplitDoubleDown(){
// Given // Given
Player player = new Player(1); 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.HEARTS));
player.dealCard(new Card(9, Card.Suit.SPADES)); 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(6, Card.Suit.HEARTS),
new Card(7, Card.Suit.HEARTS),
new Card(6, Card.Suit.CLUBS),
new Card(7, Card.Suit.CLUBS)); new Card(7, Card.Suit.CLUBS));
// When // When
game.playSplit(player); game.play(player);
// Then // Then
assertTrue(player.getCurrentBet() == 200); assertAll(
assertTrue(player.getSplitBet() == 200); () -> assertEquals(200, player.getCurrentBet(), "Current bet should be doubled"),
assertTrue(player.getHand().size() == 3); () -> assertEquals(200, player.getSplitBet(), "Split bet should be doubled"),
assertTrue(player.getSplitHand().size() == 3); () -> 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 @Test
@DisplayName("playSplit should NOT allow re-splitting") @DisplayName("play() should NOT allow re-splitting first split hand")
public void playSplitDoubleDownLate(){ public void playSplitTwice(){
// Given // Given
Player player = new Player(1); Player player = new Player(1);
player.setCurrentBet(100); player.setCurrentBet(100);
player.dealCard(new Card(1, Card.Suit.HEARTS)); player.dealCard(new Card(1, Card.Suit.HEARTS));
player.dealCard(new Card(1, Card.Suit.SPADES)); player.dealCard(new Card(1, Card.Suit.SPADES));
givenInput("/\nS\nS\n", givenInput("/\n/\nS\nS\n",
new Card(13, Card.Suit.HEARTS), new Card(13, Card.Suit.CLUBS),
new Card(13, Card.Suit.SPADES)); new Card(13, Card.Suit.SPADES));
// When // When
game.playSplit(player); game.play(player);
// Then // Then
assertTrue(out.toString().contains("TYPE H, S OR D, PLEASE")); 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"));
}
} }