Implement play() and scoreHand()

This commit is contained in:
Dave Burke
2022-02-09 21:19:04 -06:00
parent 0760f22494
commit 2b2f9327f7
4 changed files with 253 additions and 26 deletions

View File

@@ -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;

View File

@@ -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<Card> 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<Card> 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<Card> handA, LinkedList<Card> handB) {
return 0;
}

View File

@@ -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.

View File

@@ -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<Card> 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<Card> 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<Card> 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<Card> 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<Card> 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<Card> 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);
}
}