diff --git a/10_Blackjack/java/src/Blackjack.java b/10_Blackjack/java/src/Blackjack.java index 102cfd17..7552b02d 100644 --- a/10_Blackjack/java/src/Blackjack.java +++ b/10_Blackjack/java/src/Blackjack.java @@ -1,9 +1,6 @@ -import java.io.EOFException; -import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; -import java.io.UncheckedIOException; import java.io.Writer; import java.util.Collections; diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index 6dcc99bd..a79e986f 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -2,8 +2,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; -import java.io.Reader; -import java.io.Writer; public class Game { @@ -74,8 +72,10 @@ public class Game { printInitialDeal(players, dealer); + // TODO if dealer has an ACE, prompt "ANY INSURANCE" and deal with insurance + for(Player player : players){ - play(player, deck); + play(player); } // only play the dealer if at least one player has not busted or gotten a natural blackjack (21 in the first two cards) @@ -122,23 +122,72 @@ public class Game { * @param player * @param deck */ - private void play(Player player, Deck deck) { - // TODO implement play(player, deck) - // If the player hits, deal another card. If the player stays, return. If the player busts, return. - // delegate to evaluateHand(hand) to determine whether the player busted. - // Use promptBoolean and promptInt as examples to start with for prompting actions - // initially prompt with "PLAYER [x] ?" where x is the player number and accept H, S, D, or / - // after hitting, prompt "RECEIVED A [c] HIT? " where c is the card received and only accept H or S - // handle splitting and doubling down, or feel free to skip implementing - // split/double down for now, but leave a todo if that is unfinished - // after the first pass. + protected void play(Player player) { + String 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){ + 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()); + return; + } else if(player.getHand().size() == 2 && action.equalsIgnoreCase("/")) { // SPLIT + if(player.getHand().get(0).equals(player.getHand().get(1))){ + // TODO split = split into two hands that play separately. only allowed for pairs + // TODO implement player.split that takes one card from 'hand' and adds it to a new 'splitHand' field. + // TODO determine if the original code allowed re-splitting, splitting on aces, or doubling down on a split and if it requires cards + } 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 { + action = userIo.prompt("TYPE H,S,D, OR /, PLEASE"); + } + } + } + } - private int evaluateHand(LinkedList hand){ - // TODO implement evaluateHand - // 'int' is maybe the wrong return type. We need to indicate a bust and somehow communicate the ambiguity of aces. - // OR maybe we stick with 'int' and use -1 for a bust and otherwise determine the value of aces that gives the highest non-bust score. - // but note that we also need a distinction between a natural Blackjack (21 in only 2 cards) and a 21 with more than 2 cards (the natural blackjack wins) + /** + * Calculates the value of a hand. + * + * @param hand the hand to evaluate + * @return The numeric value of a hand. + */ + protected int scoreHand(LinkedList hand){ + int nAces = (int) hand.stream().filter(c -> c.getValue() == 1).count(); + int value = hand.stream() + .mapToInt(Card::getValue) + .filter(v -> v != 1) // start without aces + .map(v -> v > 10 ? 10 : v) // all face cards are worth 10. The 'expr ? a : b' syntax is called the 'ternary operator' + .sum(); + value += nAces; // start by treating all aces as 1 + if(nAces > 0 && value <= 11) { + value += 10; // We can use one of the aces to an 11 + // You can never use more than one ace as 11, since that would be 22 and a bust. + } + return value; + } + + /** + * Compares two hands accounting for natural blackjacks + * + * @param handA hand to compare + * @param handB other hand to compare + * @return a negative integer, zero, or a positive integer as handA is less than, equal to, or greater than handB. + */ + private int compareHands(LinkedList handA, LinkedList handB) { return 0; } diff --git a/10_Blackjack/java/src/UserIo.java b/10_Blackjack/java/src/UserIo.java index bfef4cd7..57becd12 100644 --- a/10_Blackjack/java/src/UserIo.java +++ b/10_Blackjack/java/src/UserIo.java @@ -58,6 +58,11 @@ public class UserIo { } } + public String prompt(String prompt) { + print(prompt + "? "); + return readLine(); + } + /** * Prompts the user for a "Yes" or "No" answer. * @param prompt The prompt to display to the user on STDOUT. diff --git a/10_Blackjack/java/test/GameTest.java b/10_Blackjack/java/test/GameTest.java index c5cf158c..ad0d625b 100644 --- a/10_Blackjack/java/test/GameTest.java +++ b/10_Blackjack/java/test/GameTest.java @@ -1,15 +1,16 @@ -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.EOFException; -import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.io.UncheckedIOException; +import java.util.Arrays; +import java.util.LinkedList; public class GameTest { @@ -17,14 +18,32 @@ public class GameTest { private StringWriter out; private Game game; - private void givenInput(String input) { - Reader in = new StringReader("\u2404"); // U+2404 is "End of Transmission" sent by CTRL+D (or CTRL+Z on Windows) - StringWriter out = new StringWriter(); + private void givenStubGame() { + in = new StringReader(""); + out = new StringWriter(); UserIo userIo = new UserIo(in, out); Deck deck = new Deck((cards) -> cards); game = new Game(deck, userIo); } + private void givenInput(String input) { + in = new StringReader(input); + out = new StringWriter(); + UserIo userIo = new UserIo(in, out); + Deck deck = new Deck((cards) -> cards); + game = new Game(deck, userIo); + } + + private void givenInput(String input, Card... customCards) { + in = new StringReader(input); + out = new StringWriter(); + UserIo userIo = new UserIo(in, out); + LinkedList cardList = new LinkedList<>(); + cardList.addAll(Arrays.asList(customCards)); + Deck deck = new Deck((cards) -> cardList); + game = new Game(deck, userIo); + } + @Test public void shouldQuitOnCtrlD() { // Given @@ -37,4 +56,161 @@ public class GameTest { assertTrue(e.getCause() instanceof EOFException); assertEquals("!END OF INPUT", e.getMessage()); } + + @Test + @DisplayName("play() should end on STAY") + public void playEndOnStay(){ + // Given + Player player = new Player(1); + player.dealCard(new Card(3, Card.Suit.CLUBS)); + player.dealCard(new Card(2, Card.Suit.SPADES)); + givenInput("S\n"); // "I also like to live dangerously." + + // When + game.play(player); + + // Then + assertEquals("PLAYER 1 ? ", out.toString()); + } + + @Test + @DisplayName("play() should allow HIT until BUST") + public void playHitUntilBust() { + // Given + Player player = new Player(1); + player.dealCard(new Card(10, Card.Suit.HEARTS)); + player.dealCard(new Card(9, Card.Suit.SPADES)); + + givenInput("H\nH\nH\n", + new Card(1, Card.Suit.SPADES), // 20 + new Card(1, Card.Suit.HEARTS), // 21 + new Card(1, Card.Suit.CLUBS)); // 22 - D'oh! + + // When + game.play(player); + + // Then + assertTrue(out.toString().contains("BUSTED")); + } + + @Test + @DisplayName("Should allow double down on initial turn") + public void playDoubleDown(){ + // Given + Player player = new Player(1); + player.setCurrentBet(100); + player.dealCard(new Card(10, Card.Suit.HEARTS)); + player.dealCard(new Card(4, Card.Suit.SPADES)); + + givenInput("D\n", new Card(7, Card.Suit.SPADES)); + + // When + game.play(player); + + // Then + assertTrue(player.getCurrentBet() == 200); + assertTrue(player.getHand().size() == 3); + } + + @Test + @DisplayName("Should NOT allow double down after initial deal") + public void playDoubleDownLate(){ + // Given + Player player = new Player(1); + player.setCurrentBet(100); + player.dealCard(new Card(10, Card.Suit.HEARTS)); + player.dealCard(new Card(2, Card.Suit.SPADES)); + + givenInput("H\nD\nS\n", new Card(7, Card.Suit.SPADES)); + + // When + game.play(player); + + // Then + assertTrue(out.toString().contains("TYPE H, OR S, PLEASE")); + } + + @Test + @DisplayName("scoreHand should sum non-ace values normally") + public void scoreHandNormally() { + // Given + givenStubGame(); + LinkedList hand = new LinkedList<>(); + hand.add(new Card(4, Card.Suit.SPADES)); + hand.add(new Card(6, Card.Suit.SPADES)); + hand.add(new Card(10, Card.Suit.SPADES)); + + // When + int result = game.scoreHand(hand); + + // Then + assertEquals(20, result); + } + + @Test + @DisplayName("scoreHand should treat face cards as 10") + public void scoreHandFaceCards() { + // Given + givenStubGame(); + LinkedList hand = new LinkedList<>(); + hand.add(new Card(11, Card.Suit.SPADES)); + hand.add(new Card(12, Card.Suit.SPADES)); + hand.add(new Card(13, Card.Suit.SPADES)); + + // When + int result = game.scoreHand(hand); + + // Then + assertEquals(30, result); + } + + @Test + @DisplayName("scoreHand should score aces as 11 when possible") + public void scoreHandSoftAce() { + // Given + givenStubGame(); + LinkedList hand = new LinkedList<>(); + hand.add(new Card(10, Card.Suit.SPADES)); + hand.add(new Card(1, Card.Suit.SPADES)); + + // When + int result = game.scoreHand(hand); + + // Then + assertEquals(21, result); + } + + @Test + @DisplayName("scoreHand should score aces as 1 when using 11 would bust") + public void scoreHandHardAce() { + // Given + givenStubGame(); + LinkedList hand = new LinkedList<>(); + hand.add(new Card(10, Card.Suit.SPADES)); + hand.add(new Card(9, Card.Suit.SPADES)); + hand.add(new Card(1, Card.Suit.SPADES)); + + // When + int result = game.scoreHand(hand); + + // Then + assertEquals(20, result); + } + + @Test + @DisplayName("scoreHand should score 3 aces as 13") + public void scoreHandMultipleAces() { + // Given + givenStubGame(); + LinkedList hand = new LinkedList<>(); + hand.add(new Card(1, Card.Suit.SPADES)); + hand.add(new Card(1, Card.Suit.CLUBS)); + hand.add(new Card(1, Card.Suit.HEARTS)); + + // When + int result = game.scoreHand(hand); + + // Then + assertEquals(13, result); + } }