From e460dac3a4c127aaa1c02a162a512b3dff3f61d9 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Tue, 18 Jan 2022 12:36:16 -0600 Subject: [PATCH 01/51] Add stub implementation of Blackjack in Java --- 10_Blackjack/java/src/Blackjack.java | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 10_Blackjack/java/src/Blackjack.java diff --git a/10_Blackjack/java/src/Blackjack.java b/10_Blackjack/java/src/Blackjack.java new file mode 100644 index 00000000..9c18ca76 --- /dev/null +++ b/10_Blackjack/java/src/Blackjack.java @@ -0,0 +1,5 @@ +public class Blackjack { + public static void main(String[] args) { + System.out.println("BLACK JACK"); + } +} From 3f42a86e6570822da9fc16800dfc9b7454cff069 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Tue, 18 Jan 2022 12:55:21 -0600 Subject: [PATCH 02/51] Implement instructions prompt --- 10_Blackjack/java/src/Blackjack.java | 41 ++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/10_Blackjack/java/src/Blackjack.java b/10_Blackjack/java/src/Blackjack.java index 9c18ca76..5f100c93 100644 --- a/10_Blackjack/java/src/Blackjack.java +++ b/10_Blackjack/java/src/Blackjack.java @@ -1,5 +1,46 @@ +import java.util.Scanner; + public class Blackjack { public static void main(String[] args) { System.out.println("BLACK JACK"); + System.out.println("CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n\n\n"); + System.out.println("Do you want instructions?"); + String input = getInput(); + if(input.toLowerCase().equals("y")){ + System.out.println("THIS IS THE GAME OF 21. AS MANY AS 7 PLAYERS MAY PLAY THE"); + System.out.println("GAME. ON EACH DEAL, BETS WILL BE ASKED FOR, AND THE"); + System.out.println("PLAYERS' BETS SHOULD BE TYPED IN. THE CARDS WILL THEN BE"); + System.out.println("DEALT, AND EACH PLAYER IN TURN PLAYS HIS HAND. THE"); + System.out.println("FIRST RESPONSE SHOULD BE EITHER 'D', INDICATING THAT THE"); + System.out.println("PLAYER IS DOUBLING DOWN, 'S', INDICATING THAT HE IS"); + System.out.println("STANDING, 'H', INDICATING HE WANTS ANOTHER CARD, OR '/',"); + System.out.println("INDICATING THAT HE WANTS TO SPLIT HIS CARDS. AFTER THE"); + System.out.println("INITIAL RESPONSE, ALL FURTHER RESPONSES SHOULD BE 'S' OR"); + System.out.println("'H', UNLESS THE CARDS WERE SPLIT, IN WHICH CASE DOUBLING"); + System.out.println("DOWN IS AGAIN PERMITTED. IN ORDER TO COLLECT FOR"); + System.out.println("BLACKJACK, THE INITIAL RESPONSE SHOULD BE 'S'."); + System.out.println("NUMBER OF PLAYERS"); + } + System.out.println(input); + } + + // TODO copied from Craps. Clean this up. + public static String getInput() { + Scanner scanner = new Scanner(System.in); + System.out.print("> "); + while (true) { + try { + return scanner.nextLine(); + } catch (Exception ex) { + try { + scanner.nextLine(); // flush whatever this non number stuff is. + } catch (Exception ns_ex) { // received EOF (ctrl-d or ctrl-z if windows) + System.out.println("END OF INPUT, STOPPING PROGRAM."); + System.exit(1); + } + } + System.out.println("!NUMBER EXPECTED - RETRY INPUT LINE"); + System.out.print("> "); + } } } From f941ef42fee406c0a25dcf74a5e32d2b68049495 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Thu, 20 Jan 2022 21:56:51 -0600 Subject: [PATCH 03/51] Refactor input prompts After experimenting with both versions, I find System.console() to produce more readable code than using a Scanner. --- 10_Blackjack/java/src/Blackjack.java | 73 +++++++++++++++++++--------- 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/10_Blackjack/java/src/Blackjack.java b/10_Blackjack/java/src/Blackjack.java index 5f100c93..53d34193 100644 --- a/10_Blackjack/java/src/Blackjack.java +++ b/10_Blackjack/java/src/Blackjack.java @@ -1,12 +1,8 @@ -import java.util.Scanner; - public class Blackjack { public static void main(String[] args) { System.out.println("BLACK JACK"); System.out.println("CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n\n\n"); - System.out.println("Do you want instructions?"); - String input = getInput(); - if(input.toLowerCase().equals("y")){ + if(promptBoolean("DO YOU WANT INSTRUCTIONS ")){ System.out.println("THIS IS THE GAME OF 21. AS MANY AS 7 PLAYERS MAY PLAY THE"); System.out.println("GAME. ON EACH DEAL, BETS WILL BE ASKED FOR, AND THE"); System.out.println("PLAYERS' BETS SHOULD BE TYPED IN. THE CARDS WILL THEN BE"); @@ -19,28 +15,61 @@ public class Blackjack { System.out.println("'H', UNLESS THE CARDS WERE SPLIT, IN WHICH CASE DOUBLING"); System.out.println("DOWN IS AGAIN PERMITTED. IN ORDER TO COLLECT FOR"); System.out.println("BLACKJACK, THE INITIAL RESPONSE SHOULD BE 'S'."); - System.out.println("NUMBER OF PLAYERS"); } - System.out.println(input); + int nPlayers = promptInt("NUMBER OF PLAYERS ", 1, 7); + System.out.println("You picked " + nPlayers); } - // TODO copied from Craps. Clean this up. - public static String getInput() { - Scanner scanner = new Scanner(System.in); - System.out.print("> "); - while (true) { - try { - return scanner.nextLine(); - } catch (Exception ex) { + /** + * Prompts the user for a "Yes" or "No" answer. + * @param prompt The prompt to display to the user on STDOUT. + * @return false if the user enters a value beginning with "N" (case insensitive), or true otherwise. + */ + public static boolean promptBoolean(String prompt) { + System.out.print(prompt); + + // Other ways to read input are + // new BufferedReader(new InputStreamReader(System.in)).readLine(); + // and new Scanner(System.in) + // But those are less expressive and care must be taken to close the + // Reader or Scanner resource. + String input = System.console().readLine(); + + // input will be null if the user presses CTRL+D or CTRL+Z in Windows + if(input != null && input.toLowerCase().startsWith("n")) { + return false; + } else { + return true; + } + } + + /** + * Prompts the user for an integer. Re-prompts if the input is not an int or outside the given range. + * @param prompt The prompt to display to the user on STDIN + * @param min The minimum allowed value (inclusive) + * @param max The maximum allowed value (inclusive) + * @return The number given by the user, or -1 for any non-numeric input. + */ + public static int promptInt(String prompt, int min, int max) { + while(true) { + System.out.print(prompt); + + String input = System.console().readLine(); + int numericInput; try { - scanner.nextLine(); // flush whatever this non number stuff is. - } catch (Exception ns_ex) { // received EOF (ctrl-d or ctrl-z if windows) - System.out.println("END OF INPUT, STOPPING PROGRAM."); - System.exit(1); + numericInput = Integer.parseInt(input); + } catch(NumberFormatException e) { + // Non-int input (including CTRL+D/CTRL+Z) + System.out.println(); + continue; + } + if(numericInput < min || numericInput > max) { + // Out of range. Clear input and re-prompt + System.out.println(); + continue; + } else { + return numericInput; } - } - System.out.println("!NUMBER EXPECTED - RETRY INPUT LINE"); - System.out.print("> "); } } } From 71c7dc4d9159ab081457b72148fcfdc3f0de6323 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Fri, 21 Jan 2022 13:00:35 -0600 Subject: [PATCH 04/51] Formatting fixes --- 10_Blackjack/java/src/Blackjack.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/10_Blackjack/java/src/Blackjack.java b/10_Blackjack/java/src/Blackjack.java index 53d34193..e8d33be9 100644 --- a/10_Blackjack/java/src/Blackjack.java +++ b/10_Blackjack/java/src/Blackjack.java @@ -2,7 +2,7 @@ public class Blackjack { public static void main(String[] args) { System.out.println("BLACK JACK"); System.out.println("CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n\n\n"); - if(promptBoolean("DO YOU WANT INSTRUCTIONS ")){ + if(promptBoolean("DO YOU WANT INSTRUCTIONS? ")){ System.out.println("THIS IS THE GAME OF 21. AS MANY AS 7 PLAYERS MAY PLAY THE"); System.out.println("GAME. ON EACH DEAL, BETS WILL BE ASKED FOR, AND THE"); System.out.println("PLAYERS' BETS SHOULD BE TYPED IN. THE CARDS WILL THEN BE"); @@ -16,6 +16,7 @@ public class Blackjack { System.out.println("DOWN IS AGAIN PERMITTED. IN ORDER TO COLLECT FOR"); System.out.println("BLACKJACK, THE INITIAL RESPONSE SHOULD BE 'S'."); } + int nPlayers = promptInt("NUMBER OF PLAYERS ", 1, 7); System.out.println("You picked " + nPlayers); } From 92da37d0d58679a4b28c8f2145b84dc028ee0543 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Fri, 21 Jan 2022 13:00:58 -0600 Subject: [PATCH 05/51] Prompt for bets and add notes on classes --- 10_Blackjack/java/src/Blackjack.java | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/10_Blackjack/java/src/Blackjack.java b/10_Blackjack/java/src/Blackjack.java index e8d33be9..74d11dd1 100644 --- a/10_Blackjack/java/src/Blackjack.java +++ b/10_Blackjack/java/src/Blackjack.java @@ -18,7 +18,32 @@ public class Blackjack { } int nPlayers = promptInt("NUMBER OF PLAYERS ", 1, 7); - System.out.println("You picked " + nPlayers); + + System.out.println("BETS: "); + for(int i = 1; i <= nPlayers; i++) { + // TODO that this will repeat the individual player's prompt if the number is out of range. + // The original BASIC code accepts all bets, then validates them together and prompts all + // players again if any inputs are invalid. This should be updated to behave like the original. + promptInt("#" + i, 1, 500); + } + + /* + Note that LinkedList is a Deque: https://docs.oracle.com/javase/8/docs/api/java/util/Deque.html + Player + CurrentBet + Total + Hand + Hand + cards LinkedList + evaluate() // see 300 in blackjack.bas for eval subroutine logic + Deck // note the game is played with more than one deck + cards LinkedList // instantiate cards and randomize in constructor via Collections.shuffle() + List dealHands(n) + discardPile Queue + Card + Value + Suit + */ } /** From 3411d33f182674004052c1f1ba6208615c5290c7 Mon Sep 17 00:00:00 2001 From: Mitch Peck Date: Sat, 22 Jan 2022 20:15:09 -0600 Subject: [PATCH 06/51] Add Player and Card objects --- 10_Blackjack/java/src/Card.java | 21 ++++++++++++++++++++ 10_Blackjack/java/src/Player.java | 32 +++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 10_Blackjack/java/src/Card.java create mode 100644 10_Blackjack/java/src/Player.java diff --git a/10_Blackjack/java/src/Card.java b/10_Blackjack/java/src/Card.java new file mode 100644 index 00000000..bd343dcc --- /dev/null +++ b/10_Blackjack/java/src/Card.java @@ -0,0 +1,21 @@ +public class Card { + private int value; + private String suit; + + public void setValue(int value) { + this.value = value; + } + + public int getValue() { + return this.value; + } + + public void setSuit(int suit) { + this.suit = suit; + } + + public int getSuit() { + return this.suit; + } + +} \ No newline at end of file diff --git a/10_Blackjack/java/src/Player.java b/10_Blackjack/java/src/Player.java new file mode 100644 index 00000000..6e7090eb --- /dev/null +++ b/10_Blackjack/java/src/Player.java @@ -0,0 +1,32 @@ +import Card; + +public class Player { + + private int currentBet; + private int total; + private LinkedList hand; + + public void setCurrentBet(int currentBet) { + this.currentBet = currentBet; + } + + public int getCurrentBet() { + return this.currentBet; + } + + public void setTotal(int total) { + this.total = total; + } + + public int getTotal() { + return this.total; + } + + public void setHand(LinkedList hand) { + this.hand = hand; + } + + public LinkedList getHand() { + return this.hand; + } +} \ No newline at end of file From bd00700d18c76642e5e0eec2e527d4a88a07e947 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Sun, 23 Jan 2022 21:07:19 -0600 Subject: [PATCH 07/51] Fix inputs to match original BASIC behavior. --- 10_Blackjack/java/src/Blackjack.java | 85 ++++++++++++++++++---------- 1 file changed, 55 insertions(+), 30 deletions(-) diff --git a/10_Blackjack/java/src/Blackjack.java b/10_Blackjack/java/src/Blackjack.java index 74d11dd1..dfb49548 100644 --- a/10_Blackjack/java/src/Blackjack.java +++ b/10_Blackjack/java/src/Blackjack.java @@ -1,3 +1,5 @@ +import java.util.Arrays; + public class Blackjack { public static void main(String[] args) { System.out.println("BLACK JACK"); @@ -17,14 +19,19 @@ public class Blackjack { System.out.println("BLACKJACK, THE INITIAL RESPONSE SHOULD BE 'S'."); } - int nPlayers = promptInt("NUMBER OF PLAYERS ", 1, 7); + int nPlayers = 0; + while(nPlayers < 1 || nPlayers > 7) { + nPlayers = promptInt("NUMBER OF PLAYERS"); + } - System.out.println("BETS: "); - for(int i = 1; i <= nPlayers; i++) { - // TODO that this will repeat the individual player's prompt if the number is out of range. - // The original BASIC code accepts all bets, then validates them together and prompts all - // players again if any inputs are invalid. This should be updated to behave like the original. - promptInt("#" + i, 1, 500); + int[] bets = new int[nPlayers]; // empty array initialized with all '0' valuses. + while(!betsAreValid(bets)){ + System.out.println("BETS:"); + for(int i = 0; i < nPlayers; i++) { + // Note that the bet for player "1" is at index "0" in the bets + // array and take care to avoid off-by-one errors. + bets[i] = promptInt("#" + (i + 1)); + } } /* @@ -49,7 +56,7 @@ public class Blackjack { /** * Prompts the user for a "Yes" or "No" answer. * @param prompt The prompt to display to the user on STDOUT. - * @return false if the user enters a value beginning with "N" (case insensitive), or true otherwise. + * @return false if the user enters a value beginning with "N" or "n"; true otherwise. */ public static boolean promptBoolean(String prompt) { System.out.print(prompt); @@ -60,9 +67,14 @@ public class Blackjack { // But those are less expressive and care must be taken to close the // Reader or Scanner resource. String input = System.console().readLine(); + if(input == null) { + // readLine returns null on CTRL-D or CTRL-Z + // this is how the original basic handled that. + System.out.println("!END OF INPUT"); + System.exit(0); + } - // input will be null if the user presses CTRL+D or CTRL+Z in Windows - if(input != null && input.toLowerCase().startsWith("n")) { + if(input.toLowerCase().startsWith("n")) { return false; } else { return true; @@ -70,32 +82,45 @@ public class Blackjack { } /** - * Prompts the user for an integer. Re-prompts if the input is not an int or outside the given range. - * @param prompt The prompt to display to the user on STDIN - * @param min The minimum allowed value (inclusive) - * @param max The maximum allowed value (inclusive) - * @return The number given by the user, or -1 for any non-numeric input. + * Prompts the user for an integer. As in Vintage Basic, "the optional + * prompt string is followed by a question mark and a space." and if the + * input is non-numeric, "an error will be generated and the user will be + * re-prompted."" + * + * @param prompt The prompt to display to the user. + * @return the number given by the user. */ - public static int promptInt(String prompt, int min, int max) { - while(true) { - System.out.print(prompt); + public static int promptInt(String prompt) { + System.out.print(prompt + "? "); + while(true) { String input = System.console().readLine(); - int numericInput; - try { - numericInput = Integer.parseInt(input); - } catch(NumberFormatException e) { - // Non-int input (including CTRL+D/CTRL+Z) - System.out.println(); - continue; + if(input == null) { + // readLine returns null on CTRL-D or CTRL-Z + // this is how the original basic handled that. + System.out.println("!END OF INPUT"); + System.exit(0); } - if(numericInput < min || numericInput > max) { - // Out of range. Clear input and re-prompt - System.out.println(); + try { + return Integer.parseInt(input); + } catch(NumberFormatException e) { + // Input was not numeric. + System.out.println("!NUMBER EXPECTED - RETRY INPUT LINE"); + System.out.print("? "); continue; - } else { - return numericInput; } } } + + /** + * Validates that all bets are between 1 and 500 (inclusive). + * + * @param bets The array of bets for each player. + * @return true if all bets are valid, false otherwise. + */ + public static boolean betsAreValid(int[] bets) { + return Arrays.stream(bets) + .allMatch(bet -> bet >= 1 && bet <= 500); + } + } From 83c1e6527897cb20a40645d12c619b08fe072a20 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Sun, 23 Jan 2022 21:17:10 -0600 Subject: [PATCH 08/51] Make Suit an enum in Card --- 10_Blackjack/java/src/Card.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/10_Blackjack/java/src/Card.java b/10_Blackjack/java/src/Card.java index bd343dcc..c4ccd50a 100644 --- a/10_Blackjack/java/src/Card.java +++ b/10_Blackjack/java/src/Card.java @@ -1,6 +1,11 @@ public class Card { + + public enum Suit { + HEARTS, DIAMONDS, SPADES, CLUBS; + } + private int value; - private String suit; + private Suit suit; public void setValue(int value) { this.value = value; @@ -10,11 +15,11 @@ public class Card { return this.value; } - public void setSuit(int suit) { + public void setSuit(Suit suit) { this.suit = suit; } - public int getSuit() { + public Suit getSuit() { return this.suit; } From 7bf2a0443b740c2335e89bdd09d17e54ed4fe875 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Sun, 23 Jan 2022 21:17:22 -0600 Subject: [PATCH 09/51] Fix imports in Player --- 10_Blackjack/java/src/Player.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/10_Blackjack/java/src/Player.java b/10_Blackjack/java/src/Player.java index 6e7090eb..f4a970fd 100644 --- a/10_Blackjack/java/src/Player.java +++ b/10_Blackjack/java/src/Player.java @@ -1,4 +1,4 @@ -import Card; +import java.util.LinkedList; public class Player { From 51f173c9da27d362f34bc0e249e0a7c88a98b95a Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Sun, 23 Jan 2022 21:18:32 -0600 Subject: [PATCH 10/51] Make Card immutable --- 10_Blackjack/java/src/Card.java | 46 +++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/10_Blackjack/java/src/Card.java b/10_Blackjack/java/src/Card.java index c4ccd50a..aa151742 100644 --- a/10_Blackjack/java/src/Card.java +++ b/10_Blackjack/java/src/Card.java @@ -1,26 +1,34 @@ -public class Card { +/** + * This is an example of an "immutable" class in Java. That's just a fancy way + * of saying the properties (value and suit) can't change after the object has + * been created (it has no 'setter' methods and the properties are 'final'). + * + * Immutability often makes it easier to reason about code logic and avoid + * certain classes of bugs. + * + * Since it would never make sense for a card to change in the middle of a game, + * this is a good candidate for immutability. + * + */ +public final class Card { - public enum Suit { - HEARTS, DIAMONDS, SPADES, CLUBS; - } + public enum Suit { + HEARTS, DIAMONDS, SPADES, CLUBS; + } - private int value; - private Suit suit; + // Since this class is immutable, there's no reason these couldn't be + // 'public', but the pattern of using 'getters' is more consistent with + // typical Java coding patterns. + private final int value; + private final Suit suit; - public void setValue(int value) { - this.value = value; - } + public Card(int value, Suit suit) { + this.value = value; + this.suit = suit; + } - public int getValue() { - return this.value; - } - - public void setSuit(Suit suit) { - this.suit = suit; - } - - public Suit getSuit() { - return this.suit; + public int getValue() { + return this.value; } } \ No newline at end of file From 080d6ccee4dd0862b913b97189f50b9e9f37d2ca Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Sun, 23 Jan 2022 21:36:57 -0600 Subject: [PATCH 11/51] Implement shuffled deck --- 10_Blackjack/java/src/Blackjack.java | 13 +++++++++++++ 10_Blackjack/java/src/Card.java | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/10_Blackjack/java/src/Blackjack.java b/10_Blackjack/java/src/Blackjack.java index dfb49548..cc8ec827 100644 --- a/10_Blackjack/java/src/Blackjack.java +++ b/10_Blackjack/java/src/Blackjack.java @@ -1,4 +1,8 @@ +import static java.util.stream.Collectors.joining; + import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; public class Blackjack { public static void main(String[] args) { @@ -24,6 +28,15 @@ public class Blackjack { nPlayers = promptInt("NUMBER OF PLAYERS"); } + System.out.println("RESHUFFLING"); + LinkedList deck = new LinkedList<>(); + for(Card.Suit suit : Card.Suit.values()) { + for(int value = 1; value < 14; value++) { + deck.add(new Card(value, suit)); + } + } + Collections.shuffle(deck); + int[] bets = new int[nPlayers]; // empty array initialized with all '0' valuses. while(!betsAreValid(bets)){ System.out.println("BETS:"); diff --git a/10_Blackjack/java/src/Card.java b/10_Blackjack/java/src/Card.java index aa151742..8fd36ad0 100644 --- a/10_Blackjack/java/src/Card.java +++ b/10_Blackjack/java/src/Card.java @@ -31,4 +31,24 @@ public final class Card { return this.value; } + public Suit getSuit() { + return this.suit; + } + + public String toString() { + StringBuilder result = new StringBuilder(2); + if(value < 11) { + result.append(value); + } else if(value == 11) { + result.append('J'); + } else if(value == 12) { + result.append('Q'); + } else if(value == 13) { + result.append('K'); + } + // Uncomment to include the suit in output. Useful for debugging, but + // doesn't match the original BASIC behavior. + // result.append(suit.name().charAt(0)); + return result.toString(); + } } \ No newline at end of file From ff2859e0d0b7628c5b4f225bc73adbe7fa16289b Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Tue, 25 Jan 2022 21:16:26 -0600 Subject: [PATCH 12/51] Add Blackjack/java outline with TODO placeholders --- 10_Blackjack/java/src/Blackjack.java | 142 ++++++++++++++++++++------- 10_Blackjack/java/src/Deck.java | 40 ++++++++ 10_Blackjack/java/src/Player.java | 14 ++- 3 files changed, 161 insertions(+), 35 deletions(-) create mode 100644 10_Blackjack/java/src/Deck.java diff --git a/10_Blackjack/java/src/Blackjack.java b/10_Blackjack/java/src/Blackjack.java index cc8ec827..d3791a29 100644 --- a/10_Blackjack/java/src/Blackjack.java +++ b/10_Blackjack/java/src/Blackjack.java @@ -1,8 +1,7 @@ -import static java.util.stream.Collectors.joining; - +import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.LinkedList; +import java.util.List; public class Blackjack { public static void main(String[] args) { @@ -28,42 +27,117 @@ public class Blackjack { nPlayers = promptInt("NUMBER OF PLAYERS"); } + Deck deck = new Deck(4); // TODO figure out how many decks the BASIC version uses System.out.println("RESHUFFLING"); - LinkedList deck = new LinkedList<>(); - for(Card.Suit suit : Card.Suit.values()) { - for(int value = 1; value < 14; value++) { - deck.add(new Card(value, suit)); - } - } - Collections.shuffle(deck); + deck.shuffle(); - int[] bets = new int[nPlayers]; // empty array initialized with all '0' valuses. - while(!betsAreValid(bets)){ - System.out.println("BETS:"); - for(int i = 0; i < nPlayers; i++) { - // Note that the bet for player "1" is at index "0" in the bets - // array and take care to avoid off-by-one errors. - bets[i] = promptInt("#" + (i + 1)); - } - } + List players = new ArrayList<>(); + // TODO instantiate Player instances and update below to set their current bets. Finish TODOs in Player.java first. + while(true) { // TODO is there a w + int[] bets = new int[nPlayers]; // empty array initialized with all '0' valuses. + while(!betsAreValid(bets)){ + System.out.println("BETS:"); + for(int i = 0; i < nPlayers; i++) { + // Note that the bet for player "1" is at index "0" in the bets + // array and take care to avoid off-by-one errors. + bets[i] = promptInt("#" + (i + 1)); + } + } + + printInitialDeal(); + + for(Player player : players){ + // TODO deal two cards to each player from the deck. + } + + // Consider adding a Dealer class to track the dealer's hand and running total. + LinkedList dealerHand = new LinkedList<>(); + // TODO deal two cards to the dealer + + // TODO handle 'insurance' if the dealer's card is an Ace. + + for(Player player : players){ + play(player, deck); + } + + // only play the dealer if at least one player has not busted or gotten a natural blackjack (21 in the first two cards) + // otherwise, just print the dealer's concealed card + dealerHand = playDealer(dealerHand, deck); + + evaluateRound(players, dealerHand); + } + } + + private static void printInitialDeal() { + // TODO implement printInitialDeal() + // Print the initial deal in the following format: /* - Note that LinkedList is a Deque: https://docs.oracle.com/javase/8/docs/api/java/util/Deque.html - Player - CurrentBet - Total - Hand - Hand - cards LinkedList - evaluate() // see 300 in blackjack.bas for eval subroutine logic - Deck // note the game is played with more than one deck - cards LinkedList // instantiate cards and randomize in constructor via Collections.shuffle() - List dealHands(n) - discardPile Queue - Card - Value - Suit + PLAYER 1 2 DEALER + 7 10 4 + 2 A + */ + } + + /** + * Plays the players turn. Prompts the user to hit (H), stay (S), or if + * appropriate, split (/) or double down (D), and then performs those + * actions. On a hit, prints "RECEIVED A [x] HIT? " + * + * @param player + * @param deck + */ + private static 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. + } + + private static 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) + return 0; + } + + /** + * Play the dealer's hand. The dealer draws until they have >=17 or busts. Prints each draw as in the following example: + * + * DEALER HAS A 5 CONCEALED FOR A TOTAL OF 11 + * DRAWS 10 ---TOTAL IS 21 + * + * TODO find out if the dealer draws on a "soft" 17 (17 using an ace as 11) or not in the original basic code. + * + * @param dealerHand + * @return + */ + private static LinkedList playDealer(LinkedList dealerHand, Deck deck) { + // TODO implement playDealer + return null; + } + + /** + * Evaluates the result of the round, prints the results, and updates player/dealer totals. + * @param players + * @param dealerHand + */ + private static void evaluateRound(List players, LinkedList dealerHand) { + // TODO implement evaluateRound + // print something like: + /* + PLAYER 1 LOSES 100 TOTAL=-100 + PLAYER 2 WINS 150 TOTAL= 150 + DEALER'S TOTAL= 200 */ + // this should probably take in a "Dealer" instance instead of just the dealer hand so we can update the dealer's total. + // remember to handle a "PUSH" when the dealer ties and the bet is returned. } /** diff --git a/10_Blackjack/java/src/Deck.java b/10_Blackjack/java/src/Deck.java new file mode 100644 index 00000000..c2363de4 --- /dev/null +++ b/10_Blackjack/java/src/Deck.java @@ -0,0 +1,40 @@ +import java.util.LinkedList; + +public class Deck { + + LinkedList cards; + + /** + * Initialize the game deck with the given number of standard decks. + * e.g. if you want to play with 2 decks, then {@code new Decks(2)} will + * initialize 'cards' with 2 copies of a standard 52 card deck. + * + * @param nDecks + */ + public Deck(int nDecks) { + // TODO implement Deck constructor + // See line 33 of Blackjack.java for the current version of this code + /* for each suit + * for each value 1-13 + * add new Card(value, suit) to cards + */ + } + + /** + * Deals one card from the deck, removing it from this object's state. + * @return The card that was dealt. + */ + public Card deal() { + // TODO implement Deck.deal() + return null; + } + + /** + * Shuffle the cards in this deck. + */ + public void shuffle() { + // TODO implement Deck.shuffle() + // Probably just call Collections.shuffle(cards); + } + +} diff --git a/10_Blackjack/java/src/Player.java b/10_Blackjack/java/src/Player.java index f4a970fd..17fa2770 100644 --- a/10_Blackjack/java/src/Player.java +++ b/10_Blackjack/java/src/Player.java @@ -2,9 +2,15 @@ import java.util.LinkedList; public class Player { + // TODO add 'playerNumber' property. e.g. playerNumber = 1 means "this is Player 1" private int currentBet; private int total; - private LinkedList hand; + private LinkedList hand; + // TODO we'll need to decide how to deal with a split hand or doubled down bet. + + public Player() { + // TODO initilize 'total' to zero and 'hand' to an empty List + } public void setCurrentBet(int currentBet) { this.currentBet = currentBet; @@ -14,6 +20,9 @@ public class Player { return this.currentBet; } + // TODO replace Player.setTotal with recordWin and recordLoss + // recordWin adds 'currentBet' to 'total' and then sets 'currentBet' to zero + // recordLoss subtracts 'currentBet' to 'total' and then sets 'currentBet' to zero public void setTotal(int total) { this.total = total; } @@ -22,6 +31,9 @@ public class Player { return this.total; } + // TODO replace Player.setHand with 'dealCard(Card card)' and resetHand() + // dealCard adds the given card to the player's hand + // resetHand resets 'hand' to an empty list public void setHand(LinkedList hand) { this.hand = hand; } From 9d71f4ea8c61b678b3e475d4c7102e1fa62dbbb1 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Tue, 25 Jan 2022 21:19:15 -0600 Subject: [PATCH 13/51] Add a couple more notes --- 10_Blackjack/java/src/Blackjack.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/10_Blackjack/java/src/Blackjack.java b/10_Blackjack/java/src/Blackjack.java index d3791a29..b779e15f 100644 --- a/10_Blackjack/java/src/Blackjack.java +++ b/10_Blackjack/java/src/Blackjack.java @@ -52,6 +52,7 @@ public class Blackjack { } // Consider adding a Dealer class to track the dealer's hand and running total. + // Alternately, the dealer could just be a Player instance where currentBet=0 and is ignored. LinkedList dealerHand = new LinkedList<>(); // TODO deal two cards to the dealer @@ -137,6 +138,7 @@ public class Blackjack { DEALER'S TOTAL= 200 */ // this should probably take in a "Dealer" instance instead of just the dealer hand so we can update the dealer's total. + // currentBets of each player are added/subtracted from the dealer total depending on whether they win/lose (accounting for doubling down, insurance etc.) // remember to handle a "PUSH" when the dealer ties and the bet is returned. } From ea6ec3504471debe1db52604af6e3810693f41e7 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Tue, 25 Jan 2022 21:25:16 -0600 Subject: [PATCH 14/51] Remove a junk TODO --- 10_Blackjack/java/src/Blackjack.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/10_Blackjack/java/src/Blackjack.java b/10_Blackjack/java/src/Blackjack.java index b779e15f..ff47705b 100644 --- a/10_Blackjack/java/src/Blackjack.java +++ b/10_Blackjack/java/src/Blackjack.java @@ -34,7 +34,7 @@ public class Blackjack { List players = new ArrayList<>(); // TODO instantiate Player instances and update below to set their current bets. Finish TODOs in Player.java first. - while(true) { // TODO is there a w + while(true) { int[] bets = new int[nPlayers]; // empty array initialized with all '0' valuses. while(!betsAreValid(bets)){ System.out.println("BETS:"); From 20c786923279c84ee71af3eeda169a4397bc2cff Mon Sep 17 00:00:00 2001 From: Mitch Peck Date: Tue, 1 Feb 2022 20:21:30 -0600 Subject: [PATCH 15/51] Update Player object and initialize Players --- 10_Blackjack/java/src/Blackjack.java | 14 +++++--- 10_Blackjack/java/src/Player.java | 52 +++++++++++++++++++++------- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/10_Blackjack/java/src/Blackjack.java b/10_Blackjack/java/src/Blackjack.java index ff47705b..65573b0a 100644 --- a/10_Blackjack/java/src/Blackjack.java +++ b/10_Blackjack/java/src/Blackjack.java @@ -32,7 +32,9 @@ public class Blackjack { deck.shuffle(); List players = new ArrayList<>(); - // TODO instantiate Player instances and update below to set their current bets. Finish TODOs in Player.java first. + for(int i = 0; i < nPlayers; i++) { + players.add(new Player(i + 1)); + } while(true) { int[] bets = new int[nPlayers]; // empty array initialized with all '0' valuses. @@ -41,14 +43,14 @@ public class Blackjack { for(int i = 0; i < nPlayers; i++) { // Note that the bet for player "1" is at index "0" in the bets // array and take care to avoid off-by-one errors. - bets[i] = promptInt("#" + (i + 1)); + bets[i] = promptInt("#" + (i + 1)); //TODO: If there isn't a need for a separate Bets in the future, combine these two lines and convert to enhanced FOR loop + Players.get(i).setCurrentBet(bets[i]); } } - printInitialDeal(); - for(Player player : players){ - // TODO deal two cards to each player from the deck. + player.dealCard(deck.deal()); + player.dealCard(deck.deal()); //TODO: This could be in a separate loop to more acurrately follow how a game would be dealt, I couldn't figure out of the BASIC version did it } // Consider adding a Dealer class to track the dealer's hand and running total. @@ -58,6 +60,8 @@ public class Blackjack { // TODO handle 'insurance' if the dealer's card is an Ace. + printInitialDeal(); + for(Player player : players){ play(player, deck); } diff --git a/10_Blackjack/java/src/Player.java b/10_Blackjack/java/src/Player.java index 17fa2770..f41fec83 100644 --- a/10_Blackjack/java/src/Player.java +++ b/10_Blackjack/java/src/Player.java @@ -2,16 +2,30 @@ import java.util.LinkedList; public class Player { - // TODO add 'playerNumber' property. e.g. playerNumber = 1 means "this is Player 1" + private int playerNumber; // e.g. playerNumber = 1 means "this is Player 1" private int currentBet; private int total; private LinkedList hand; // TODO we'll need to decide how to deal with a split hand or doubled down bet. - public Player() { - // TODO initilize 'total' to zero and 'hand' to an empty List + /** + * Represents a player in the game with cards, bets, total and a playerNumber. + */ + public Player(int playerNumber) { + this.playerNumber = playerNumber; + currentBet = 0; + total = 0; + hand = new LinkedList<>(); } + public void setPlayerNumber(int playerNumber) { //TODO: Is this needed if set in constructor? + this.playerNumber = playerNumber; + } + + public int getPlayerNumber() { + return this.playerNumber; + } + public void setCurrentBet(int currentBet) { this.currentBet = currentBet; } @@ -20,22 +34,36 @@ public class Player { return this.currentBet; } - // TODO replace Player.setTotal with recordWin and recordLoss - // recordWin adds 'currentBet' to 'total' and then sets 'currentBet' to zero - // recordLoss subtracts 'currentBet' to 'total' and then sets 'currentBet' to zero - public void setTotal(int total) { - this.total = total; + /** + * RecordWin adds 'currentBet' to 'total' and then sets 'currentBet' to zero + */ + public void recordWin() { + this.total = this.total + this.currentBet; + this.currentBet = 0; } - + /** + * RecordLoss subtracts 'currentBet' to 'total' and then sets 'currentBet' to zero + */ + public void recordLoss() { + total = total - currentBet; + currentBet = 0; + } + /** + * Returns the total of all bets won. + * @return Total value + */ public int getTotal() { return this.total; } - // TODO replace Player.setHand with 'dealCard(Card card)' and resetHand() // dealCard adds the given card to the player's hand + public void dealCard(Card card) { + hand.add(card); + } + // resetHand resets 'hand' to an empty list - public void setHand(LinkedList hand) { - this.hand = hand; + public void resetHand() { + this.hand = new LinkedList<>(); } public LinkedList getHand() { From 1b063963081fd81a1995eaecbb46ce8188279c06 Mon Sep 17 00:00:00 2001 From: Mitch Peck Date: Wed, 2 Feb 2022 19:15:37 -0600 Subject: [PATCH 16/51] Implement print initial deal --- 10_Blackjack/java/src/Blackjack.java | 31 +++++++++++++++++++++++----- 10_Blackjack/java/src/Deck.java | 5 +++-- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/10_Blackjack/java/src/Blackjack.java b/10_Blackjack/java/src/Blackjack.java index 65573b0a..47cb802b 100644 --- a/10_Blackjack/java/src/Blackjack.java +++ b/10_Blackjack/java/src/Blackjack.java @@ -44,7 +44,7 @@ public class Blackjack { // Note that the bet for player "1" is at index "0" in the bets // array and take care to avoid off-by-one errors. bets[i] = promptInt("#" + (i + 1)); //TODO: If there isn't a need for a separate Bets in the future, combine these two lines and convert to enhanced FOR loop - Players.get(i).setCurrentBet(bets[i]); + players.get(i).setCurrentBet(bets[i]); } } @@ -53,14 +53,17 @@ public class Blackjack { player.dealCard(deck.deal()); //TODO: This could be in a separate loop to more acurrately follow how a game would be dealt, I couldn't figure out of the BASIC version did it } + // Consider adding a Dealer class to track the dealer's hand and running total. // Alternately, the dealer could just be a Player instance where currentBet=0 and is ignored. LinkedList dealerHand = new LinkedList<>(); + Player dealer = new Player(0); //Dealer is Player 0 - this can be converted into a dealer class later on + dealer.dealCard(deck.deal()); // TODO deal two cards to the dealer // TODO handle 'insurance' if the dealer's card is an Ace. - printInitialDeal(); + printInitialDeal(players, dealer); for(Player player : players){ play(player, deck); @@ -74,14 +77,32 @@ public class Blackjack { } } - private static void printInitialDeal() { - // TODO implement printInitialDeal() - // Print the initial deal in the following format: + private static void printInitialDeal(List players, Player dealer) { + // Prints the initial deal in the following format: /* PLAYER 1 2 DEALER 7 10 4 2 A */ + + StringBuilder output = new StringBuilder(); + output.append("PLAYERS "); + for (Player player : players) { + output.append(player.getPlayerNumber() + "\t"); + } + output.append("DEALER\n"); + //Loop through two rows of cards + for (int j = 0; j < 2; j++) { + output.append("\t"); + for (Player player : players) { + output.append(player.getHand().get(j).toString()).append("\t"); + } + if(j == 0 ){ + output.append(dealer.getHand().get(j).toString()); + } + output.append("\n"); + } + System.out.print(output); } /** diff --git a/10_Blackjack/java/src/Deck.java b/10_Blackjack/java/src/Deck.java index c2363de4..7b9a9094 100644 --- a/10_Blackjack/java/src/Deck.java +++ b/10_Blackjack/java/src/Deck.java @@ -25,8 +25,8 @@ public class Deck { * @return The card that was dealt. */ public Card deal() { - // TODO implement Deck.deal() - return null; + // TODO implement Deck.deal() - new Card(10, Card.Suit.CLUBS) added temporarily + return new Card(10, Card.Suit.CLUBS); } /** @@ -37,4 +37,5 @@ public class Deck { // Probably just call Collections.shuffle(cards); } + } From 914f123bfc04b686bd24e635f19e93d81d2c73c3 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Wed, 2 Feb 2022 21:36:04 -0600 Subject: [PATCH 17/51] Implement unit tests for Blackjack Java --- 10_Blackjack/java/src/Card.java | 31 +++++++++++++++++++++ 10_Blackjack/java/src/Deck.java | 33 ++++++++++++++++++----- 10_Blackjack/java/test/DeckTest.java | 40 ++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 10_Blackjack/java/test/DeckTest.java diff --git a/10_Blackjack/java/src/Card.java b/10_Blackjack/java/src/Card.java index 8fd36ad0..3f88f9c5 100644 --- a/10_Blackjack/java/src/Card.java +++ b/10_Blackjack/java/src/Card.java @@ -23,6 +23,12 @@ public final class Card { private final Suit suit; public Card(int value, Suit suit) { + if(value < 1 || value > 13) { + throw new IllegalArgumentException("Invalid card value " + value); + } + if(suit == null) { + throw new IllegalArgumentException("Card suit must be non-null"); + } this.value = value; this.suit = suit; } @@ -51,4 +57,29 @@ public final class Card { // result.append(suit.name().charAt(0)); return result.toString(); } + + @Override + public boolean equals(Object obj) { + // Overriding 'equals' and 'hashCode' (below) make your class work correctly + // with all sorts of methods in the Java API that need to determine the uniqueness + // of an instance (like a Set). + if(obj.getClass() != Card.class) { + return false; + } + Card other = (Card) obj; + return this.getSuit() == other.getSuit() && this.getValue() == other.getValue(); + } + + @Override + public int hashCode() { + // This is a fairly standard hashCode implementation for a data object. + // The details are beyond the scope of this comment, but most IDEs can generate + // this for you. + + // Note that it's a best practice to implement hashCode whenever you implement equals and vice versa. + int hash = 7; + hash = 31 * hash + (int) value; + hash = 31 * hash + suit.hashCode(); + return hash; + } } \ No newline at end of file diff --git a/10_Blackjack/java/src/Deck.java b/10_Blackjack/java/src/Deck.java index 7b9a9094..7ed290bc 100644 --- a/10_Blackjack/java/src/Deck.java +++ b/10_Blackjack/java/src/Deck.java @@ -1,8 +1,10 @@ +import java.util.Collections; import java.util.LinkedList; +import java.util.List; public class Deck { - LinkedList cards; + private LinkedList cards; /** * Initialize the game deck with the given number of standard decks. @@ -12,12 +14,14 @@ public class Deck { * @param nDecks */ public Deck(int nDecks) { - // TODO implement Deck constructor - // See line 33 of Blackjack.java for the current version of this code - /* for each suit - * for each value 1-13 - * add new Card(value, suit) to cards - */ + cards = new LinkedList<>(); + for(int deckIndex = 0; deckIndex < nDecks; deckIndex++) { + for(Card.Suit suit : Card.Suit.values()) { + for(int value = 1; value < 14; value++) { + cards.add(new Card(value, suit)); + } + } + } } /** @@ -37,5 +41,20 @@ public class Deck { // Probably just call Collections.shuffle(cards); } + /** + * Get the number of cards in this deck. + * @return The number of cards in this deck. For example, 52 for a single deck. + */ + public int size() { + return cards.size(); + } + /** + * Returns the cards in this deck. + * @return An immutable view of the cards in this deck. + */ + public List getCards() { + // The returned list is immutable because we don't want other code messing with the deck. + return Collections.unmodifiableList(cards); + } } diff --git a/10_Blackjack/java/test/DeckTest.java b/10_Blackjack/java/test/DeckTest.java new file mode 100644 index 00000000..0ded8403 --- /dev/null +++ b/10_Blackjack/java/test/DeckTest.java @@ -0,0 +1,40 @@ +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertAll; +import org.junit.jupiter.api.Test; + +public class DeckTest { + + @Test + void testInitOne() { + // When + Deck deck = new Deck(1); + + // Then + long nCards = deck.size(); + long nSuits = deck.getCards().stream() + .map(card -> card.getSuit()) + .distinct() + .count(); + long nValues = deck.getCards().stream() + .map(card -> card.getValue()) + .distinct() + .count(); + + assertAll("deck", + () -> assertEquals(52, nCards, "Expected 52 cards in a deck, but got " + nCards), + () -> assertEquals(4, nSuits, "Expected 4 suits, but got " + nSuits), + () -> assertEquals(13, nValues, "Expected 13 values, but got " + nValues) + ); + + } + + @Test + void testInitTwo() { + // When + Deck deck = new Deck(2); + + // Then + assertEquals(104, deck.size()); + } + +} From 15c26cbe09f3b16920786553e40e7c0e4438435e Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Thu, 3 Feb 2022 08:31:02 -0600 Subject: [PATCH 18/51] Limit number of decks to 1 The subroutine to get a card shuffles 52 cards when the deck is run through: 100 REM--SUBROUTINE TO GET A CARD. RESULT IS PUT IN X. 110 IF C<51 THEN 230 120 PRINT "RESHUFFLING" 130 FOR D=D TO 1 STEP -1 140 C=C-1 150 C(C)=D(D) 160 NEXT D 170 FOR C1=52 TO C STEP -1 180 C2=INT(RND(1)*(C1-C+1))+C 190 C3=C(C2) 200 C(C2)=C(C1) 210 C(C1)=C3 220 NEXT C1 230 X=C(C) 240 C=C+1 250 RETURN --- 10_Blackjack/java/src/Blackjack.java | 2 +- 10_Blackjack/java/src/Deck.java | 10 ++++------ 10_Blackjack/java/test/DeckTest.java | 13 ++----------- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/10_Blackjack/java/src/Blackjack.java b/10_Blackjack/java/src/Blackjack.java index 47cb802b..a8699640 100644 --- a/10_Blackjack/java/src/Blackjack.java +++ b/10_Blackjack/java/src/Blackjack.java @@ -27,7 +27,7 @@ public class Blackjack { nPlayers = promptInt("NUMBER OF PLAYERS"); } - Deck deck = new Deck(4); // TODO figure out how many decks the BASIC version uses + Deck deck = new Deck(); System.out.println("RESHUFFLING"); deck.shuffle(); diff --git a/10_Blackjack/java/src/Deck.java b/10_Blackjack/java/src/Deck.java index 7ed290bc..c8b81004 100644 --- a/10_Blackjack/java/src/Deck.java +++ b/10_Blackjack/java/src/Deck.java @@ -13,13 +13,11 @@ public class Deck { * * @param nDecks */ - public Deck(int nDecks) { + public Deck() { cards = new LinkedList<>(); - for(int deckIndex = 0; deckIndex < nDecks; deckIndex++) { - for(Card.Suit suit : Card.Suit.values()) { - for(int value = 1; value < 14; value++) { - cards.add(new Card(value, suit)); - } + for(Card.Suit suit : Card.Suit.values()) { + for(int value = 1; value < 14; value++) { + cards.add(new Card(value, suit)); } } } diff --git a/10_Blackjack/java/test/DeckTest.java b/10_Blackjack/java/test/DeckTest.java index 0ded8403..58ec0a63 100644 --- a/10_Blackjack/java/test/DeckTest.java +++ b/10_Blackjack/java/test/DeckTest.java @@ -5,9 +5,9 @@ import org.junit.jupiter.api.Test; public class DeckTest { @Test - void testInitOne() { + void testInit() { // When - Deck deck = new Deck(1); + Deck deck = new Deck(); // Then long nCards = deck.size(); @@ -28,13 +28,4 @@ public class DeckTest { } - @Test - void testInitTwo() { - // When - Deck deck = new Deck(2); - - // Then - assertEquals(104, deck.size()); - } - } From 0b1f57ae4fc7ea417101ee6dc6d9b8cb36ee2466 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Mon, 7 Feb 2022 15:02:13 -0600 Subject: [PATCH 19/51] Refactor to allow testing side effects By externalizing the source of i/o and randomness for shuffling, we can inject non-interactive and deterministic behavior during unit tests. --- 10_Blackjack/java/src/Blackjack.java | 262 +++------------------------ 10_Blackjack/java/src/Deck.java | 38 ++-- 10_Blackjack/java/src/Game.java | 190 +++++++++++++++++++ 10_Blackjack/java/src/UserIo.java | 102 +++++++++++ 10_Blackjack/java/test/DeckTest.java | 3 +- 5 files changed, 345 insertions(+), 250 deletions(-) create mode 100644 10_Blackjack/java/src/Game.java create mode 100644 10_Blackjack/java/src/UserIo.java diff --git a/10_Blackjack/java/src/Blackjack.java b/10_Blackjack/java/src/Blackjack.java index a8699640..102cfd17 100644 --- a/10_Blackjack/java/src/Blackjack.java +++ b/10_Blackjack/java/src/Blackjack.java @@ -1,240 +1,34 @@ -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; +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; public class Blackjack { public static void main(String[] args) { - System.out.println("BLACK JACK"); - System.out.println("CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n\n\n"); - if(promptBoolean("DO YOU WANT INSTRUCTIONS? ")){ - System.out.println("THIS IS THE GAME OF 21. AS MANY AS 7 PLAYERS MAY PLAY THE"); - System.out.println("GAME. ON EACH DEAL, BETS WILL BE ASKED FOR, AND THE"); - System.out.println("PLAYERS' BETS SHOULD BE TYPED IN. THE CARDS WILL THEN BE"); - System.out.println("DEALT, AND EACH PLAYER IN TURN PLAYS HIS HAND. THE"); - System.out.println("FIRST RESPONSE SHOULD BE EITHER 'D', INDICATING THAT THE"); - System.out.println("PLAYER IS DOUBLING DOWN, 'S', INDICATING THAT HE IS"); - System.out.println("STANDING, 'H', INDICATING HE WANTS ANOTHER CARD, OR '/',"); - System.out.println("INDICATING THAT HE WANTS TO SPLIT HIS CARDS. AFTER THE"); - System.out.println("INITIAL RESPONSE, ALL FURTHER RESPONSES SHOULD BE 'S' OR"); - System.out.println("'H', UNLESS THE CARDS WERE SPLIT, IN WHICH CASE DOUBLING"); - System.out.println("DOWN IS AGAIN PERMITTED. IN ORDER TO COLLECT FOR"); - System.out.println("BLACKJACK, THE INITIAL RESPONSE SHOULD BE 'S'."); - } - - int nPlayers = 0; - while(nPlayers < 1 || nPlayers > 7) { - nPlayers = promptInt("NUMBER OF PLAYERS"); - } - - Deck deck = new Deck(); - System.out.println("RESHUFFLING"); - deck.shuffle(); - - List players = new ArrayList<>(); - for(int i = 0; i < nPlayers; i++) { - players.add(new Player(i + 1)); - } - - while(true) { - int[] bets = new int[nPlayers]; // empty array initialized with all '0' valuses. - while(!betsAreValid(bets)){ - System.out.println("BETS:"); - for(int i = 0; i < nPlayers; i++) { - // Note that the bet for player "1" is at index "0" in the bets - // array and take care to avoid off-by-one errors. - bets[i] = promptInt("#" + (i + 1)); //TODO: If there isn't a need for a separate Bets in the future, combine these two lines and convert to enhanced FOR loop - players.get(i).setCurrentBet(bets[i]); - } - } - - for(Player player : players){ - player.dealCard(deck.deal()); - player.dealCard(deck.deal()); //TODO: This could be in a separate loop to more acurrately follow how a game would be dealt, I couldn't figure out of the BASIC version did it - } - - - // Consider adding a Dealer class to track the dealer's hand and running total. - // Alternately, the dealer could just be a Player instance where currentBet=0 and is ignored. - LinkedList dealerHand = new LinkedList<>(); - Player dealer = new Player(0); //Dealer is Player 0 - this can be converted into a dealer class later on - dealer.dealCard(deck.deal()); - // TODO deal two cards to the dealer - - // TODO handle 'insurance' if the dealer's card is an Ace. - - printInitialDeal(players, dealer); - - for(Player player : players){ - play(player, deck); - } - - // only play the dealer if at least one player has not busted or gotten a natural blackjack (21 in the first two cards) - // otherwise, just print the dealer's concealed card - dealerHand = playDealer(dealerHand, deck); - - evaluateRound(players, dealerHand); + // Intuitively it might seem like the main program logic should be right + // here in 'main' and that we should just use System.in and System.out + // directly whenever we need them. However, by externalizing the source + // of input/output data (and the ordering of the cards via a custom + // shuffle function), we can write non-interactive and deterministic + // tests of the code. See UserIoTest as an example. + try (Reader in = new InputStreamReader(System.in)) { + Writer out = new OutputStreamWriter(System.out); + UserIo userIo = new UserIo(in, out); + Deck deck = new Deck(cards -> { + userIo.println("RESHUFFLING"); + Collections.shuffle(cards); + return cards; + }); + Game game = new Game(deck, userIo); + game.run(); + } catch (Exception e) { + // This allows us to elegantly handle CTRL+D / CTRL+Z by throwing an exception. + System.out.println(e.getMessage()); + System.exit(1); } } - - private static void printInitialDeal(List players, Player dealer) { - // Prints the initial deal in the following format: - /* - PLAYER 1 2 DEALER - 7 10 4 - 2 A - */ - - StringBuilder output = new StringBuilder(); - output.append("PLAYERS "); - for (Player player : players) { - output.append(player.getPlayerNumber() + "\t"); - } - output.append("DEALER\n"); - //Loop through two rows of cards - for (int j = 0; j < 2; j++) { - output.append("\t"); - for (Player player : players) { - output.append(player.getHand().get(j).toString()).append("\t"); - } - if(j == 0 ){ - output.append(dealer.getHand().get(j).toString()); - } - output.append("\n"); - } - System.out.print(output); - } - - /** - * Plays the players turn. Prompts the user to hit (H), stay (S), or if - * appropriate, split (/) or double down (D), and then performs those - * actions. On a hit, prints "RECEIVED A [x] HIT? " - * - * @param player - * @param deck - */ - private static 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. - } - - private static 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) - return 0; - } - - /** - * Play the dealer's hand. The dealer draws until they have >=17 or busts. Prints each draw as in the following example: - * - * DEALER HAS A 5 CONCEALED FOR A TOTAL OF 11 - * DRAWS 10 ---TOTAL IS 21 - * - * TODO find out if the dealer draws on a "soft" 17 (17 using an ace as 11) or not in the original basic code. - * - * @param dealerHand - * @return - */ - private static LinkedList playDealer(LinkedList dealerHand, Deck deck) { - // TODO implement playDealer - return null; - } - - /** - * Evaluates the result of the round, prints the results, and updates player/dealer totals. - * @param players - * @param dealerHand - */ - private static void evaluateRound(List players, LinkedList dealerHand) { - // TODO implement evaluateRound - // print something like: - /* - PLAYER 1 LOSES 100 TOTAL=-100 - PLAYER 2 WINS 150 TOTAL= 150 - DEALER'S TOTAL= 200 - */ - // this should probably take in a "Dealer" instance instead of just the dealer hand so we can update the dealer's total. - // currentBets of each player are added/subtracted from the dealer total depending on whether they win/lose (accounting for doubling down, insurance etc.) - // remember to handle a "PUSH" when the dealer ties and the bet is returned. - } - - /** - * Prompts the user for a "Yes" or "No" answer. - * @param prompt The prompt to display to the user on STDOUT. - * @return false if the user enters a value beginning with "N" or "n"; true otherwise. - */ - public static boolean promptBoolean(String prompt) { - System.out.print(prompt); - - // Other ways to read input are - // new BufferedReader(new InputStreamReader(System.in)).readLine(); - // and new Scanner(System.in) - // But those are less expressive and care must be taken to close the - // Reader or Scanner resource. - String input = System.console().readLine(); - if(input == null) { - // readLine returns null on CTRL-D or CTRL-Z - // this is how the original basic handled that. - System.out.println("!END OF INPUT"); - System.exit(0); - } - - if(input.toLowerCase().startsWith("n")) { - return false; - } else { - return true; - } - } - - /** - * Prompts the user for an integer. As in Vintage Basic, "the optional - * prompt string is followed by a question mark and a space." and if the - * input is non-numeric, "an error will be generated and the user will be - * re-prompted."" - * - * @param prompt The prompt to display to the user. - * @return the number given by the user. - */ - public static int promptInt(String prompt) { - System.out.print(prompt + "? "); - - while(true) { - String input = System.console().readLine(); - if(input == null) { - // readLine returns null on CTRL-D or CTRL-Z - // this is how the original basic handled that. - System.out.println("!END OF INPUT"); - System.exit(0); - } - try { - return Integer.parseInt(input); - } catch(NumberFormatException e) { - // Input was not numeric. - System.out.println("!NUMBER EXPECTED - RETRY INPUT LINE"); - System.out.print("? "); - continue; - } - } - } - - /** - * Validates that all bets are between 1 and 500 (inclusive). - * - * @param bets The array of bets for each player. - * @return true if all bets are valid, false otherwise. - */ - public static boolean betsAreValid(int[] bets) { - return Arrays.stream(bets) - .allMatch(bet -> bet >= 1 && bet <= 500); - } - } diff --git a/10_Blackjack/java/src/Deck.java b/10_Blackjack/java/src/Deck.java index c8b81004..79149015 100644 --- a/10_Blackjack/java/src/Deck.java +++ b/10_Blackjack/java/src/Deck.java @@ -1,42 +1,50 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.function.Function; public class Deck { private LinkedList cards; + private Function, LinkedList> shuffleAlgorithm; /** * Initialize the game deck with the given number of standard decks. * e.g. if you want to play with 2 decks, then {@code new Decks(2)} will * initialize 'cards' with 2 copies of a standard 52 card deck. * - * @param nDecks + * @param shuffleAlgorithm A function that takes the initial sorted card + * list and returns a shuffled list ready to deal. + * */ - public Deck() { - cards = new LinkedList<>(); - for(Card.Suit suit : Card.Suit.values()) { - for(int value = 1; value < 14; value++) { - cards.add(new Card(value, suit)); - } - } + public Deck(Function, LinkedList> shuffleAlgorithm) { + this.shuffleAlgorithm = shuffleAlgorithm; } /** - * Deals one card from the deck, removing it from this object's state. + * Deals one card from the deck, removing it from this object's state. If + * the deck is empty, it will be reshuffled before dealing a new card. + * * @return The card that was dealt. */ public Card deal() { - // TODO implement Deck.deal() - new Card(10, Card.Suit.CLUBS) added temporarily - return new Card(10, Card.Suit.CLUBS); + if(cards == null || cards.isEmpty()) { + reshuffle(); + } + return cards.pollFirst(); } /** - * Shuffle the cards in this deck. + * Shuffle the cards in this deck using the shuffleAlgorithm. */ - public void shuffle() { - // TODO implement Deck.shuffle() - // Probably just call Collections.shuffle(cards); + public void reshuffle() { + LinkedList newCards = new LinkedList<>(); + for(Card.Suit suit : Card.Suit.values()) { + for(int value = 1; value < 14; value++) { + newCards.add(new Card(value, suit)); + } + } + this.cards = this.shuffleAlgorithm.apply(newCards); } /** diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java new file mode 100644 index 00000000..6dcc99bd --- /dev/null +++ b/10_Blackjack/java/src/Game.java @@ -0,0 +1,190 @@ +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 { + + private Deck deck; + private UserIo userIo; + + public Game(Deck deck, UserIo userIo) { + this.deck = deck; + this.userIo = userIo; + } + + public void run() { + userIo.println("BLACK JACK", 31); + userIo.println("CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n\n\n", 15); + if(userIo.promptBoolean("DO YOU WANT INSTRUCTIONS")){ + userIo.println("THIS IS THE GAME OF 21. AS MANY AS 7 PLAYERS MAY PLAY THE"); + userIo.println("GAME. ON EACH DEAL, BETS WILL BE ASKED FOR, AND THE"); + userIo.println("PLAYERS' BETS SHOULD BE TYPED IN. THE CARDS WILL THEN BE"); + userIo.println("DEALT, AND EACH PLAYER IN TURN PLAYS HIS HAND. THE"); + userIo.println("FIRST RESPONSE SHOULD BE EITHER 'D', INDICATING THAT THE"); + userIo.println("PLAYER IS DOUBLING DOWN, 'S', INDICATING THAT HE IS"); + userIo.println("STANDING, 'H', INDICATING HE WANTS ANOTHER CARD, OR '/',"); + userIo.println("INDICATING THAT HE WANTS TO SPLIT HIS CARDS. AFTER THE"); + userIo.println("INITIAL RESPONSE, ALL FURTHER RESPONSES SHOULD BE 'S' OR"); + userIo.println("'H', UNLESS THE CARDS WERE SPLIT, IN WHICH CASE DOUBLING"); + userIo.println("DOWN IS AGAIN PERMITTED. IN ORDER TO COLLECT FOR"); + userIo.println("BLACKJACK, THE INITIAL RESPONSE SHOULD BE 'S'."); + } + + int nPlayers = 0; + while(nPlayers < 1 || nPlayers > 7) { + nPlayers = userIo.promptInt("NUMBER OF PLAYERS"); + } + + deck.reshuffle(); + + List players = new ArrayList<>(); + for(int i = 0; i < nPlayers; i++) { + players.add(new Player(i + 1)); + } + + while(true) { + int[] bets = new int[nPlayers]; // empty array initialized with all '0' valuses. + while(!betsAreValid(bets)){ + userIo.println("BETS:"); + for(int i = 0; i < nPlayers; i++) { + // Note that the bet for player "1" is at index "0" in the bets + // array and take care to avoid off-by-one errors. + bets[i] = userIo.promptInt("#" + (i + 1)); //TODO: If there isn't a need for a separate Bets in the future, combine these two lines and convert to enhanced FOR loop + players.get(i).setCurrentBet(bets[i]); + } + } + + for(Player player : players){ + player.dealCard(deck.deal()); + player.dealCard(deck.deal()); //TODO: This could be in a separate loop to more acurrately follow how a game would be dealt, I couldn't figure out of the BASIC version did it + } + + + // Consider adding a Dealer class to track the dealer's hand and running total. + // Alternately, the dealer could just be a Player instance where currentBet=0 and is ignored. + LinkedList dealerHand = new LinkedList<>(); + Player dealer = new Player(0); //Dealer is Player 0 - this can be converted into a dealer class later on + dealer.dealCard(deck.deal()); + // TODO deal two cards to the dealer + + // TODO handle 'insurance' if the dealer's card is an Ace. + + printInitialDeal(players, dealer); + + for(Player player : players){ + play(player, deck); + } + + // only play the dealer if at least one player has not busted or gotten a natural blackjack (21 in the first two cards) + // otherwise, just print the dealer's concealed card + dealerHand = playDealer(dealerHand, deck); + + evaluateRound(players, dealerHand); + } + } + + private void printInitialDeal(List players, Player dealer) { + // Prints the initial deal in the following format: + /* + PLAYER 1 2 DEALER + 7 10 4 + 2 A + */ + + StringBuilder output = new StringBuilder(); + output.append("PLAYERS "); + for (Player player : players) { + output.append(player.getPlayerNumber() + "\t"); + } + output.append("DEALER\n"); + //Loop through two rows of cards + for (int j = 0; j < 2; j++) { + output.append("\t"); + for (Player player : players) { + output.append(player.getHand().get(j).toString()).append("\t"); + } + if(j == 0 ){ + output.append(dealer.getHand().get(j).toString()); + } + output.append("\n"); + } + System.out.print(output); + } + + /** + * Plays the players turn. Prompts the user to hit (H), stay (S), or if + * appropriate, split (/) or double down (D), and then performs those + * actions. On a hit, prints "RECEIVED A [x] HIT? " + * + * @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. + } + + 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) + return 0; + } + + /** + * Play the dealer's hand. The dealer draws until they have >=17 or busts. Prints each draw as in the following example: + * + * DEALER HAS A 5 CONCEALED FOR A TOTAL OF 11 + * DRAWS 10 ---TOTAL IS 21 + * + * TODO find out if the dealer draws on a "soft" 17 (17 using an ace as 11) or not in the original basic code. + * + * @param dealerHand + * @return + */ + private LinkedList playDealer(LinkedList dealerHand, Deck deck) { + // TODO implement playDealer + return null; + } + + /** + * Evaluates the result of the round, prints the results, and updates player/dealer totals. + * @param players + * @param dealerHand + */ + private void evaluateRound(List players, LinkedList dealerHand) { + // TODO implement evaluateRound + // print something like: + /* + PLAYER 1 LOSES 100 TOTAL=-100 + PLAYER 2 WINS 150 TOTAL= 150 + DEALER'S TOTAL= 200 + */ + // this should probably take in a "Dealer" instance instead of just the dealer hand so we can update the dealer's total. + // currentBets of each player are added/subtracted from the dealer total depending on whether they win/lose (accounting for doubling down, insurance etc.) + // remember to handle a "PUSH" when the dealer ties and the bet is returned. + } + + /** + * Validates that all bets are between 1 and 500 (inclusive). + * + * @param bets The array of bets for each player. + * @return true if all bets are valid, false otherwise. + */ + public boolean betsAreValid(int[] bets) { + return Arrays.stream(bets) + .allMatch(bet -> bet >= 1 && bet <= 500); + } + +} diff --git a/10_Blackjack/java/src/UserIo.java b/10_Blackjack/java/src/UserIo.java new file mode 100644 index 00000000..bfef4cd7 --- /dev/null +++ b/10_Blackjack/java/src/UserIo.java @@ -0,0 +1,102 @@ +import java.io.BufferedReader; +import java.io.EOFException; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.util.stream.IntStream; + +/** + * This class is responsible for printing output to the screen and reading input + * from the user. It must be initialized with a reader to get input data from + * and a writer to send output to. Typically these will wrap System.in and + * System.out respectively, but can be a StringReader and StringWriter when + * running in test code. + */ +public class UserIo { + + private BufferedReader in; + private PrintWriter out; + + /** + * Initializes the UserIo with the given reader/writer. The reader will be + * wrapped in a BufferedReader and so should not be a BufferedReader + * already (to avoid double buffering). + * + * @param in Typically an InputStreamReader wrapping System.in or a StringReader + * @param out Typically an OuputStreamWriter wrapping System.out or a StringWriter + */ + public UserIo(Reader in, Writer out) { + this.in = new BufferedReader(in); + this.out = new PrintWriter(out, true); + } + + public void println(String text) { + out.println(text); + } + + public void println(String text, int leftPad) { + IntStream.range(0, leftPad).forEach((i) -> out.print(' ')); + out.println(text); + } + + public void print(String text) { + out.print(text); + out.flush(); + } + + private String readLine() { + try { + String line = in.readLine(); + if(line == null) { + throw new UncheckedIOException("!END OF INPUT", new EOFException()); + } + return line; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * Prompts the user for a "Yes" or "No" answer. + * @param prompt The prompt to display to the user on STDOUT. + * @return false if the user enters a value beginning with "N" or "n"; true otherwise. + */ + public boolean promptBoolean(String prompt) { + print(prompt + "? "); + + String input = readLine(); + + if(input.toLowerCase().startsWith("n")) { + return false; + } else { + return true; + } + } + + /** + * Prompts the user for an integer. As in Vintage Basic, "the optional + * prompt string is followed by a question mark and a space." and if the + * input is non-numeric, "an error will be generated and the user will be + * re-prompted."" + * + * @param prompt The prompt to display to the user. + * @return the number given by the user. + */ + public int promptInt(String prompt) { + print(prompt + "? "); + + while(true) { + String input = readLine(); + try { + return Integer.parseInt(input); + } catch(NumberFormatException e) { + // Input was not numeric. + println("!NUMBER EXPECTED - RETRY INPUT LINE"); + print("? "); + continue; + } + } + } +} diff --git a/10_Blackjack/java/test/DeckTest.java b/10_Blackjack/java/test/DeckTest.java index 58ec0a63..b8d42ae5 100644 --- a/10_Blackjack/java/test/DeckTest.java +++ b/10_Blackjack/java/test/DeckTest.java @@ -7,7 +7,8 @@ public class DeckTest { @Test void testInit() { // When - Deck deck = new Deck(); + Deck deck = new Deck((cards) -> cards); + deck.reshuffle(); // Then long nCards = deck.size(); From 0760f22494089569ea83c661c6f38c20df27930f Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Mon, 7 Feb 2022 21:23:41 -0600 Subject: [PATCH 20/51] Add example i/o test --- 10_Blackjack/java/test/GameTest.java | 40 ++++++++++++ 10_Blackjack/java/test/UserIoTest.java | 84 ++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 10_Blackjack/java/test/GameTest.java create mode 100644 10_Blackjack/java/test/UserIoTest.java diff --git a/10_Blackjack/java/test/GameTest.java b/10_Blackjack/java/test/GameTest.java new file mode 100644 index 00000000..c5cf158c --- /dev/null +++ b/10_Blackjack/java/test/GameTest.java @@ -0,0 +1,40 @@ +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +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; + +public class GameTest { + + private StringReader in; + 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(); + UserIo userIo = new UserIo(in, out); + Deck deck = new Deck((cards) -> cards); + game = new Game(deck, userIo); + } + + @Test + public void shouldQuitOnCtrlD() { + // Given + givenInput("\u2404"); // U+2404 is "End of Transmission" sent by CTRL+D (or CTRL+Z on Windows) + + // When + Exception e = assertThrows(UncheckedIOException.class, game::run); + + // Then + assertTrue(e.getCause() instanceof EOFException); + assertEquals("!END OF INPUT", e.getMessage()); + } +} diff --git a/10_Blackjack/java/test/UserIoTest.java b/10_Blackjack/java/test/UserIoTest.java new file mode 100644 index 00000000..2c27b609 --- /dev/null +++ b/10_Blackjack/java/test/UserIoTest.java @@ -0,0 +1,84 @@ +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +public class UserIoTest { + + @ParameterizedTest(name = "''{0}'' is accepted as ''no''") + @ValueSource(strings = {"N", "n", "No", "NO", "no"}) + public void testPromptBooleanAcceptsNo(String response) { + // Given + Reader in = new StringReader(response + "\n"); + StringWriter out = new StringWriter(); + UserIo userIo = new UserIo(in, out); + + // When + boolean result = userIo.promptBoolean("TEST"); + + // Then + assertEquals("TEST? ", out.toString()); + assertFalse(result); + } + + @ParameterizedTest(name = "''{0}'' is accepted as ''yes''") + @ValueSource(strings = {"Y", "y", "Yes", "YES", "yes", "", "foobar"}) + public void testPromptBooleanAcceptsYes(String response) { + // Given + Reader in = new StringReader(response + "\n"); + StringWriter out = new StringWriter(); + UserIo userIo = new UserIo(in, out); + + // When + boolean result = userIo.promptBoolean("TEST"); + + // Then + assertEquals("TEST? ", out.toString()); + assertTrue(result); + } + + @ParameterizedTest(name = "''{0}'' is accepted as number") + @CsvSource({ + "1,1", + "0,0", + "-1,-1", + }) + public void testPromptIntAcceptsNumbers(String response, int expected) { + // Given + Reader in = new StringReader(response + "\n"); + StringWriter out = new StringWriter(); + UserIo userIo = new UserIo(in, out); + + // When + int result = userIo.promptInt("TEST"); + + // Then + assertEquals("TEST? ", out.toString()); + assertEquals(expected, result); + } + + @Test + @DisplayName("promptInt should print an error and reprompt if given a non-numeric response") + public void testPromptIntRepromptsOnNonNumeric() { + // Given + Reader in = new StringReader("foo\n1"); // word, then number + StringWriter out = new StringWriter(); + UserIo userIo = new UserIo(in, out); + + // When + int result = userIo.promptInt("TEST"); + + // Then + assertEquals("TEST? !NUMBER EXPECTED - RETRY INPUT LINE\n? ", out.toString()); + assertEquals(1, result); + } +} From 2b2f9327f78c9c827b025ecb569c94b6d53a80a8 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Wed, 9 Feb 2022 21:19:04 -0600 Subject: [PATCH 21/51] Implement play() and scoreHand() --- 10_Blackjack/java/src/Blackjack.java | 3 - 10_Blackjack/java/src/Game.java | 85 +++++++++--- 10_Blackjack/java/src/UserIo.java | 5 + 10_Blackjack/java/test/GameTest.java | 186 ++++++++++++++++++++++++++- 4 files changed, 253 insertions(+), 26 deletions(-) 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); + } } From f65c2de0589a8eb561ca6b9bea48b6d1703180d6 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Wed, 9 Feb 2022 21:40:27 -0600 Subject: [PATCH 22/51] Add comments and tasks --- 10_Blackjack/java/src/Blackjack.java | 13 ++++++++----- 10_Blackjack/java/src/Game.java | 18 +++++++++++++++--- 10_Blackjack/java/src/UserIo.java | 28 ++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/10_Blackjack/java/src/Blackjack.java b/10_Blackjack/java/src/Blackjack.java index 7552b02d..bc8c2194 100644 --- a/10_Blackjack/java/src/Blackjack.java +++ b/10_Blackjack/java/src/Blackjack.java @@ -8,11 +8,14 @@ public class Blackjack { public static void main(String[] args) { // Intuitively it might seem like the main program logic should be right // here in 'main' and that we should just use System.in and System.out - // directly whenever we need them. However, by externalizing the source - // of input/output data (and the ordering of the cards via a custom - // shuffle function), we can write non-interactive and deterministic - // tests of the code. See UserIoTest as an example. - try (Reader in = new InputStreamReader(System.in)) { + // directly whenever we need them. However, notice that System.out and + // System.in are just an OutputStream and InputStream respectively. By + // allowing alternate streams to be specified to Game at runtime, we can + // write non-interactive tests of the code. See UserIoTest as an + // example. + // Likewise, by allowing an alternative "shuffle" algorithm, test code + // can provide a deterministic card ordering. + try (Reader in = new InputStreamReader(System.in)) { Writer out = new OutputStreamWriter(System.out); UserIo userIo = new UserIo(in, out); Deck deck = new Deck(cards -> { diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index a79e986f..9ec64105 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -13,6 +13,9 @@ public class Game { this.userIo = userIo; } + /** + * Run the game, running rounds until ended with CTRL+D/CTRL+Z or CTRL+C + */ public void run() { userIo.println("BLACK JACK", 31); userIo.println("CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n\n\n", 15); @@ -72,6 +75,10 @@ public class Game { printInitialDeal(players, dealer); + // TODO check for dealer blackjack + // if blackjack, print "DEALER HAS A [x] IN THE HOLE\nFOR BLACKJACK" and skip to evaluateRound + // if not, print "NO DEALER BLACKJACK" + // TODO if dealer has an ACE, prompt "ANY INSURANCE" and deal with insurance for(Player player : players){ @@ -86,6 +93,9 @@ public class Game { } } + /** + * Print the cards for each player and the up card for the dealer. + */ private void printInitialDeal(List players, Player dealer) { // Prints the initial deal in the following format: /* @@ -111,7 +121,7 @@ public class Game { } output.append("\n"); } - System.out.print(output); + userIo.print(output); } /** @@ -160,10 +170,11 @@ public class Game { } /** - * Calculates the value of a hand. + * Calculates the value of a hand. When the hand contains aces, it will + * count one of them as 11 if that does not result in a bust. * * @param hand the hand to evaluate - * @return The numeric value of a hand. + * @return The numeric value of a hand. A value over 21 indicates a bust. */ protected int scoreHand(LinkedList hand){ int nAces = (int) hand.stream().filter(c -> c.getValue() == 1).count(); @@ -188,6 +199,7 @@ public class Game { * @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) { + // TODO implement compareHands return 0; } diff --git a/10_Blackjack/java/src/UserIo.java b/10_Blackjack/java/src/UserIo.java index 57becd12..272614da 100644 --- a/10_Blackjack/java/src/UserIo.java +++ b/10_Blackjack/java/src/UserIo.java @@ -32,20 +32,42 @@ public class UserIo { this.out = new PrintWriter(out, true); } + /** + * Print the line of text to output including a trailing linebreak. + * + * @param text the text to print + */ public void println(String text) { out.println(text); } + /** + * Print the given text left padded with spaces. + * + * @param text The text to print + * @param leftPad The number of spaces to pad with. + */ public void println(String text, int leftPad) { IntStream.range(0, leftPad).forEach((i) -> out.print(' ')); out.println(text); } + /** + * Print the given text without a trailing linebreak. + * + * @param text The text to print. + */ public void print(String text) { out.print(text); out.flush(); } + /** + * Reads a line of text from input. + * + * @return The line entered into input. + * @throws UncheckedIOException if the line is null (CTRL+D or CTRL+Z was pressed) + */ private String readLine() { try { String line = in.readLine(); @@ -58,6 +80,12 @@ public class UserIo { } } + /** + * Prompt the user via input. + * + * @param prompt The text to display as a prompt. A question mark and space will be added to the end, so if prompt = "EXAMPLE" then the user will see "EXAMPLE? ". + * @return The line read from input. + */ public String prompt(String prompt) { print(prompt + "? "); return readLine(); From a6cf574479f5387e75f2fdeaed6ed099541ecf51 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Wed, 9 Feb 2022 21:51:31 -0600 Subject: [PATCH 23/51] Add notes about insurance --- 10_Blackjack/java/src/Game.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index 9ec64105..be8b987e 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -75,11 +75,15 @@ public class Game { printInitialDeal(players, dealer); + // TODO if dealer has an ACE, prompt "ANY INSURANCE" + // if yes, print "INSURANCE BETS" and prompt each player with "# [x] ? " where X is player number + // insurance bets must be equal or less than half the player's regular bet + // TODO check for dealer blackjack // if blackjack, print "DEALER HAS A [x] IN THE HOLE\nFOR BLACKJACK" and skip to evaluateRound + // pay 2x insurance bets (insurance bet of 5 pays 10) if applicable // if not, print "NO DEALER BLACKJACK" - - // TODO if dealer has an ACE, prompt "ANY INSURANCE" and deal with insurance + // collect insurance bets if applicable for(Player player : players){ play(player); From 5c998f3bb5fe40c9bfe8e652e3f4b30490003aa1 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Fri, 11 Feb 2022 12:37:09 -0600 Subject: [PATCH 24/51] Add TODO to use fractions for bets --- 10_Blackjack/java/src/Game.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index be8b987e..afb4e20e 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -53,6 +53,8 @@ public class Game { for(int i = 0; i < nPlayers; i++) { // Note that the bet for player "1" is at index "0" in the bets // array and take care to avoid off-by-one errors. + // TODO the BASIC version allows fractional bets (e.g. '1.5') of arbitrary precision + // We can use a float if we want--the original basic has floating point errors. Or we could use BigDecimal if we really want to fix that. bets[i] = userIo.promptInt("#" + (i + 1)); //TODO: If there isn't a need for a separate Bets in the future, combine these two lines and convert to enhanced FOR loop players.get(i).setCurrentBet(bets[i]); } From df86d49bb711c5b517376c33f33ad85611950724 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Fri, 11 Feb 2022 12:37:15 -0600 Subject: [PATCH 25/51] Fix print call syntax --- 10_Blackjack/java/src/Game.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index afb4e20e..9dbe4688 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -127,7 +127,7 @@ public class Game { } output.append("\n"); } - userIo.print(output); + userIo.print(output.toString()); } /** From db1e32a314475e18f731ec19d052fc4a4db227c8 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Fri, 11 Feb 2022 13:02:22 -0600 Subject: [PATCH 26/51] Use double to store bets The original basic allowed fractional bets. --- 10_Blackjack/java/src/Game.java | 12 ++++++------ 10_Blackjack/java/src/Player.java | 10 +++++----- 10_Blackjack/java/src/UserIo.java | 25 +++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index 9dbe4688..e81c8f56 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -47,15 +47,15 @@ public class Game { } while(true) { - int[] bets = new int[nPlayers]; // empty array initialized with all '0' valuses. + double[] bets = new double[nPlayers]; // empty array initialized with all '0' valuses. while(!betsAreValid(bets)){ userIo.println("BETS:"); for(int i = 0; i < nPlayers; i++) { // Note that the bet for player "1" is at index "0" in the bets // array and take care to avoid off-by-one errors. // TODO the BASIC version allows fractional bets (e.g. '1.5') of arbitrary precision - // We can use a float if we want--the original basic has floating point errors. Or we could use BigDecimal if we really want to fix that. - bets[i] = userIo.promptInt("#" + (i + 1)); //TODO: If there isn't a need for a separate Bets in the future, combine these two lines and convert to enhanced FOR loop + // We can use a double if we want--the original basic has doubleing point errors. Or we could use BigDecimal if we really want to fix that. + bets[i] = userIo.promptDouble("#" + (i + 1)); //TODO: If there isn't a need for a separate Bets in the future, combine these two lines and convert to enhanced FOR loop players.get(i).setCurrentBet(bets[i]); } } @@ -244,14 +244,14 @@ public class Game { } /** - * Validates that all bets are between 1 and 500 (inclusive). + * Validates that all bets are between 0 (exclusive) and 500 (inclusive). Fractional bets are valid. * * @param bets The array of bets for each player. * @return true if all bets are valid, false otherwise. */ - public boolean betsAreValid(int[] bets) { + public boolean betsAreValid(double[] bets) { return Arrays.stream(bets) - .allMatch(bet -> bet >= 1 && bet <= 500); + .allMatch(bet -> bet > 0 && bet <= 500); } } diff --git a/10_Blackjack/java/src/Player.java b/10_Blackjack/java/src/Player.java index f41fec83..e664780c 100644 --- a/10_Blackjack/java/src/Player.java +++ b/10_Blackjack/java/src/Player.java @@ -3,8 +3,8 @@ import java.util.LinkedList; public class Player { private int playerNumber; // e.g. playerNumber = 1 means "this is Player 1" - private int currentBet; - private int total; + private double currentBet; + private double total; private LinkedList hand; // TODO we'll need to decide how to deal with a split hand or doubled down bet. @@ -26,11 +26,11 @@ public class Player { return this.playerNumber; } - public void setCurrentBet(int currentBet) { + public void setCurrentBet(double currentBet) { this.currentBet = currentBet; } - public int getCurrentBet() { + public double getCurrentBet() { return this.currentBet; } @@ -52,7 +52,7 @@ public class Player { * Returns the total of all bets won. * @return Total value */ - public int getTotal() { + public double getTotal() { return this.total; } diff --git a/10_Blackjack/java/src/UserIo.java b/10_Blackjack/java/src/UserIo.java index 272614da..fcaf826f 100644 --- a/10_Blackjack/java/src/UserIo.java +++ b/10_Blackjack/java/src/UserIo.java @@ -132,4 +132,29 @@ public class UserIo { } } } + + /** + * Prompts the user for a double. As in Vintage Basic, "the optional + * prompt string is followed by a question mark and a space." and if the + * input is non-numeric, "an error will be generated and the user will be + * re-prompted."" + * + * @param prompt The prompt to display to the user. + * @return the number given by the user. + */ + public double promptDouble(String prompt) { + print(prompt + "? "); + + while(true) { + String input = readLine(); + try { + return Double.parseDouble(input); + } catch(NumberFormatException e) { + // Input was not numeric. + println("!NUMBER EXPECTED - RETRY INPUT LINE"); + print("? "); + continue; + } + } + } } From 5f28cd03feae6dad196c2c253592e481a75c6d5b Mon Sep 17 00:00:00 2001 From: Mitch Peck Date: Fri, 18 Feb 2022 19:35:54 -0600 Subject: [PATCH 27/51] Implement compareHands method, add compareHands tests and fix UserIO Test --- 10_Blackjack/java/src/Game.java | 21 ++++- 10_Blackjack/java/test/GameTest.java | 109 +++++++++++++++++++++++++ 10_Blackjack/java/test/UserIoTest.java | 4 +- 3 files changed, 128 insertions(+), 6 deletions(-) diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index e81c8f56..01e34d39 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -198,15 +198,28 @@ public class Game { } /** - * Compares two hands accounting for natural blackjacks + * Compares two hands accounting for natural blackjacks using the + * java.lang.Comparable convention of returning positive or negative integers * * @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) { - // TODO implement compareHands - return 0; + protected int compareHands(LinkedList handA, LinkedList handB) { + int scoreA = this.scoreHand(handA); + int scoreB = this.scoreHand(handB); + + if(scoreA == 21 && scoreB == 21){ + if(handA.size() == 2 && handB.size() != 2){ + return 1; //Hand A wins + } else if (handA.size() != 2 && handB.size() == 2) { + return -1; //Hand B wins + } else { + return 0; //Tie + } + } else { + return Integer.compare(scoreA, scoreB); + } } /** diff --git a/10_Blackjack/java/test/GameTest.java b/10_Blackjack/java/test/GameTest.java index ad0d625b..00ced688 100644 --- a/10_Blackjack/java/test/GameTest.java +++ b/10_Blackjack/java/test/GameTest.java @@ -213,4 +213,113 @@ public class GameTest { // Then assertEquals(13, result); } + + @Test + @DisplayName("compareHands should return 1 meaning A beat B, 20 to 12") + public void compareHandsAWins() { + + givenStubGame(); + LinkedList handA = new LinkedList<>(); + handA.add(new Card(10, Card.Suit.SPADES)); + handA.add(new Card(10, Card.Suit.CLUBS)); + + LinkedList handB = new LinkedList<>(); + handB.add(new Card(1, Card.Suit.SPADES)); + handB.add(new Card(1, Card.Suit.CLUBS)); + + int result = game.compareHands(handA,handB); + + assertEquals(1, result); + } + + @Test + @DisplayName("compareHands should return -1 meaning B beat A, 18 to 4") + public void compareHandsBwins() { + givenStubGame(); + LinkedList handA = new LinkedList<>(); + handA.add(new Card(2, Card.Suit.SPADES)); + handA.add(new Card(2, Card.Suit.CLUBS)); + + LinkedList handB = new LinkedList<>(); + handB.add(new Card(5, Card.Suit.SPADES)); + handB.add(new Card(6, Card.Suit.HEARTS)); + handB.add(new Card(7, Card.Suit.CLUBS)); + + int result = game.compareHands(handA,handB); + + assertEquals(-1, result); + } + + @Test + @DisplayName("compareHands should return 1 meaning A beat B, natural Blackjack to Blackjack") + public void compareHandsAWinsWithNaturalBlackJack() { + //Hand A wins with natural BlackJack, B with Blackjack + givenStubGame(); + LinkedList handA = new LinkedList<>(); + handA.add(new Card(10, Card.Suit.SPADES)); + handA.add(new Card(1, Card.Suit.CLUBS)); + + LinkedList handB = new LinkedList<>(); + handB.add(new Card(6, Card.Suit.SPADES)); + handB.add(new Card(7, Card.Suit.HEARTS)); + handB.add(new Card(8, Card.Suit.CLUBS)); + + int result = game.compareHands(handA,handB); + + assertEquals(1, result); + } + + @Test + @DisplayName("compareHands should return -1 meaning B beat A, natural Blackjack to Blackjack") + public void compareHandsBWinsWithNaturalBlackJack() { + givenStubGame(); + LinkedList handA = new LinkedList<>(); + handA.add(new Card(6, Card.Suit.SPADES)); + handA.add(new Card(7, Card.Suit.HEARTS)); + handA.add(new Card(8, Card.Suit.CLUBS)); + + LinkedList handB = new LinkedList<>(); + handB.add(new Card(10, Card.Suit.SPADES)); + handB.add(new Card(1, Card.Suit.CLUBS)); + + int result = game.compareHands(handA,handB); + + assertEquals(-1, result); + } + + @Test + @DisplayName("compareHands should return 0, hand A and B tied with a Blackjack") + public void compareHandsTieBothBlackJack() { + givenStubGame(); + LinkedList handA = new LinkedList<>(); + handA.add(new Card(11, Card.Suit.SPADES)); + handA.add(new Card(10, Card.Suit.CLUBS)); + + LinkedList handB = new LinkedList<>(); + handB.add(new Card(10, Card.Suit.SPADES)); + handB.add(new Card(11, Card.Suit.CLUBS)); + + int result = game.compareHands(handA,handB); + + assertEquals(0, result); + } + + @Test + @DisplayName("compareHands should return 0, hand A and B tie without a Blackjack") + public void compareHandsTieNoBlackJack() { + givenStubGame(); + LinkedList handA = new LinkedList<>(); + handA.add(new Card(10, Card.Suit.DIAMONDS)); + handA.add(new Card(10, Card.Suit.HEARTS)); + + LinkedList handB = new LinkedList<>(); + handB.add(new Card(10, Card.Suit.SPADES)); + handB.add(new Card(10, Card.Suit.CLUBS)); + + int result = game.compareHands(handA,handB); + + assertEquals(0, result); + } + + } diff --git a/10_Blackjack/java/test/UserIoTest.java b/10_Blackjack/java/test/UserIoTest.java index 2c27b609..a2fccc26 100644 --- a/10_Blackjack/java/test/UserIoTest.java +++ b/10_Blackjack/java/test/UserIoTest.java @@ -70,7 +70,7 @@ public class UserIoTest { @DisplayName("promptInt should print an error and reprompt if given a non-numeric response") public void testPromptIntRepromptsOnNonNumeric() { // Given - Reader in = new StringReader("foo\n1"); // word, then number + Reader in = new StringReader("foo" + System.lineSeparator() +"1"); // word, then number StringWriter out = new StringWriter(); UserIo userIo = new UserIo(in, out); @@ -78,7 +78,7 @@ public class UserIoTest { int result = userIo.promptInt("TEST"); // Then - assertEquals("TEST? !NUMBER EXPECTED - RETRY INPUT LINE\n? ", out.toString()); + assertEquals("TEST? !NUMBER EXPECTED - RETRY INPUT LINE" + System.lineSeparator() +"? ", out.toString()); assertEquals(1, result); } } From 15922632586987c0a080be16e9cb15bef22053d7 Mon Sep 17 00:00:00 2001 From: Mitch Peck Date: Sat, 19 Feb 2022 18:06:20 -0600 Subject: [PATCH 28/51] Update player with split fields, stub out playSplit, and playSplit tests --- 10_Blackjack/java/src/Game.java | 19 +++++-- 10_Blackjack/java/src/Player.java | 42 +++++++++++++- 10_Blackjack/java/test/GameTest.java | 85 +++++++++++++++++++++++++++- 3 files changed, 137 insertions(+), 9 deletions(-) diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index 01e34d39..f9b7cd6e 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -136,7 +136,6 @@ public class Game { * actions. On a hit, prints "RECEIVED A [x] HIT? " * * @param player - * @param deck */ protected void play(Player player) { String action = userIo.prompt("PLAYER " + player.getPlayerNumber() + " "); @@ -156,10 +155,8 @@ public class Game { 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 + if(player.getHand().get(0).equals(player.getHand().get(1))){ + playSplit(player); } else { userIo.println("SPLITTING NOT ALLOWED"); action = userIo.prompt("PLAYER " + player.getPlayerNumber() + " "); @@ -174,6 +171,18 @@ public class Game { } } + /** + * 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) { + player.split(); + //TODO: Deal cards, and prompt user action + //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 e664780c..4a277128 100644 --- a/10_Blackjack/java/src/Player.java +++ b/10_Blackjack/java/src/Player.java @@ -4,9 +4,11 @@ public class Player { private int playerNumber; // e.g. playerNumber = 1 means "this is Player 1" private double currentBet; + private double insuranceBet; + private double splitBet; private double total; private LinkedList hand; - // TODO we'll need to decide how to deal with a split hand or doubled down bet. + private LinkedList splitHand; /** * Represents a player in the game with cards, bets, total and a playerNumber. @@ -14,11 +16,14 @@ public class Player { public Player(int playerNumber) { this.playerNumber = playerNumber; currentBet = 0; + insuranceBet = 0; + splitBet = 0; total = 0; hand = new LinkedList<>(); + splitHand = new LinkedList<>(); } - public void setPlayerNumber(int playerNumber) { //TODO: Is this needed if set in constructor? + public void setPlayerNumber(int playerNumber) { this.playerNumber = playerNumber; } @@ -34,6 +39,22 @@ public class Player { return this.currentBet; } + public double getSplitBet() { + return splitBet; + } + + public void setSplitBet(double splitBet) { + this.splitBet = splitBet; + } + + public double getInsuranceBet() { + return insuranceBet; + } + + public void setInsuranceBet(double insuranceBet) { + this.insuranceBet = insuranceBet; + } + /** * RecordWin adds 'currentBet' to 'total' and then sets 'currentBet' to zero */ @@ -60,13 +81,28 @@ public class Player { public void dealCard(Card card) { hand.add(card); } + + public void dealSplitHandCard(Card card) { + splitHand.add(card); + } + /** + * Removes first card from hand to adds it to split hand + */ + public void split() { + splitHand.add(hand.pop()); + } - // resetHand resets 'hand' to an empty list + // resetHand resets 'hand' & 'splitHand' to empty lists public void resetHand() { this.hand = new LinkedList<>(); + this.splitHand = new LinkedList<>(); } public LinkedList getHand() { return this.hand; } + + public LinkedList getSplitHand() { + return this.splitHand; + } } \ No newline at end of file diff --git a/10_Blackjack/java/test/GameTest.java b/10_Blackjack/java/test/GameTest.java index 00ced688..210ecb68 100644 --- a/10_Blackjack/java/test/GameTest.java +++ b/10_Blackjack/java/test/GameTest.java @@ -79,7 +79,7 @@ public class GameTest { // Given Player player = new Player(1); player.dealCard(new Card(10, Card.Suit.HEARTS)); - player.dealCard(new Card(9, Card.Suit.SPADES)); + player.dealCard(new Card(10, Card.Suit.SPADES)); givenInput("H\nH\nH\n", new Card(1, Card.Suit.SPADES), // 20 @@ -321,5 +321,88 @@ public class GameTest { assertEquals(0, result); } + //@Test + @DisplayName("playSplit() should end on STAY") + 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"); + + // When + game.playSplit(player); + + // Then + assertTrue(out.toString().contains("FIRST HAND RECEIVES")); + assertTrue(out.toString().contains("SECOND HAND RECEIVES")); + assertEquals("PLAYER 1 ? ", out.toString()); + } + + //@Test + @DisplayName("playSplit() should allow HIT until BUST") + 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 + + // When + game.playSplit(player); + + // Then + assertTrue(out.toString().contains("BUSTED")); + } + + //@Test + @DisplayName("playSplit should allow double down") + public void playSplitDoubleDown(){ + // Given + Player player = new Player(1); + player.setCurrentBet(100); + player.dealCard(new Card(9, Card.Suit.HEARTS)); + player.dealCard(new Card(9, Card.Suit.SPADES)); + + givenInput("D\nD\n", + 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); + + // Then + assertTrue(player.getCurrentBet() == 200); + assertTrue(player.getSplitBet() == 200); + assertTrue(player.getHand().size() == 3); + assertTrue(player.getSplitHand().size() == 3); + } + + //@Test + @DisplayName("playSplit should NOT allow re-splitting") + public void playSplitDoubleDownLate(){ + // 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), + new Card(13, Card.Suit.SPADES)); + + // When + game.playSplit(player); + + // Then + assertTrue(out.toString().contains("TYPE H, S OR D, PLEASE")); + } } From 5aee1ecd093f998299dfff455d83c261027646b4 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Tue, 22 Feb 2022 13:03:35 -0600 Subject: [PATCH 29/51] Initial playSplitHand implementation --- 10_Blackjack/java/src/Game.java | 70 +++++++++++++++++++++++++++- 10_Blackjack/java/src/Player.java | 1 + 10_Blackjack/java/test/GameTest.java | 11 ++--- 3 files changed, 75 insertions(+), 7 deletions(-) diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index f9b7cd6e..a46f00db 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -157,6 +157,7 @@ public class Game { } else if(player.getHand().size() == 2 && action.equalsIgnoreCase("/")) { // SPLIT if(player.getHand().get(0).equals(player.getHand().get(1))){ playSplit(player); + return; } else { userIo.println("SPLITTING NOT ALLOWED"); action = userIo.prompt("PLAYER " + player.getPlayerNumber() + " "); @@ -178,8 +179,75 @@ public class Game { * @param player */ protected void playSplit(Player player) { + // TODO refactor to avoid so much logic duplication + player.split(); - //TODO: Deal cards, and prompt user action + // 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 } diff --git a/10_Blackjack/java/src/Player.java b/10_Blackjack/java/src/Player.java index 4a277128..e130a9e6 100644 --- a/10_Blackjack/java/src/Player.java +++ b/10_Blackjack/java/src/Player.java @@ -89,6 +89,7 @@ public class Player { * Removes first card from hand to adds it to split hand */ public void split() { + this.splitBet = this.currentBet; splitHand.add(hand.pop()); } diff --git a/10_Blackjack/java/test/GameTest.java b/10_Blackjack/java/test/GameTest.java index 210ecb68..3bd35f31 100644 --- a/10_Blackjack/java/test/GameTest.java +++ b/10_Blackjack/java/test/GameTest.java @@ -321,7 +321,7 @@ public class GameTest { assertEquals(0, result); } - //@Test + @Test @DisplayName("playSplit() should end on STAY") public void playSplitEndOnStay(){ // Given @@ -336,10 +336,9 @@ public class GameTest { // Then assertTrue(out.toString().contains("FIRST HAND RECEIVES")); assertTrue(out.toString().contains("SECOND HAND RECEIVES")); - assertEquals("PLAYER 1 ? ", out.toString()); } - //@Test + @Test @DisplayName("playSplit() should allow HIT until BUST") public void playSplitHitUntilBust() { // Given @@ -360,7 +359,7 @@ public class GameTest { assertTrue(out.toString().contains("BUSTED")); } - //@Test + @Test @DisplayName("playSplit should allow double down") public void playSplitDoubleDown(){ // Given @@ -385,7 +384,7 @@ public class GameTest { assertTrue(player.getSplitHand().size() == 3); } - //@Test + @Test @DisplayName("playSplit should NOT allow re-splitting") public void playSplitDoubleDownLate(){ // Given @@ -394,7 +393,7 @@ public class GameTest { player.dealCard(new Card(1, Card.Suit.HEARTS)); player.dealCard(new Card(1, Card.Suit.SPADES)); - givenInput("\\\nS\nS\n", + givenInput("/\nS\nS\n", new Card(13, Card.Suit.HEARTS), new Card(13, Card.Suit.SPADES)); From ccbed873e52e4cc06735be76338d9b310b12bee8 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Wed, 2 Mar 2022 22:14:56 -0600 Subject: [PATCH 30/51] Add TODO to make Card a Record --- 10_Blackjack/java/src/Card.java | 1 + 1 file changed, 1 insertion(+) diff --git a/10_Blackjack/java/src/Card.java b/10_Blackjack/java/src/Card.java index 3f88f9c5..53f7f6dd 100644 --- a/10_Blackjack/java/src/Card.java +++ b/10_Blackjack/java/src/Card.java @@ -10,6 +10,7 @@ * this is a good candidate for immutability. * */ +// TODO consider making this a Record public final class Card { public enum Suit { From 5e950275fa4f990d09a3b8dc92786c44e2a9be55 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Wed, 2 Mar 2022 22:15:44 -0600 Subject: [PATCH 31/51] 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")); + } } From 58fd32823ea0c2557b16ecb9eeec304bd44aa792 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Wed, 2 Mar 2022 22:16:03 -0600 Subject: [PATCH 32/51] Use List instead of LinkedList where possible --- 10_Blackjack/java/src/Game.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index 6ec5f101..3cb49eee 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -212,7 +212,7 @@ public class Game { * @param hand the hand to evaluate * @return The numeric value of a hand. A value over 21 indicates a bust. */ - protected int scoreHand(LinkedList hand){ + protected int scoreHand(List hand){ int nAces = (int) hand.stream().filter(c -> c.getValue() == 1).count(); int value = hand.stream() .mapToInt(Card::getValue) @@ -235,15 +235,15 @@ public class Game { * @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. */ - protected int compareHands(LinkedList handA, LinkedList handB) { + protected int compareHands(List handA, List handB) { int scoreA = this.scoreHand(handA); int scoreB = this.scoreHand(handB); if(scoreA == 21 && scoreB == 21){ if(handA.size() == 2 && handB.size() != 2){ - return 1; //Hand A wins + return 1; //Hand A wins with a natural blackjack } else if (handA.size() != 2 && handB.size() == 2) { - return -1; //Hand B wins + return -1; //Hand B wins with a natural blackjack } else { return 0; //Tie } From a8d2e1259744763c51df5a2257541f1327e0bfa4 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Wed, 2 Mar 2022 22:20:41 -0600 Subject: [PATCH 33/51] Add a couple TODOs --- 10_Blackjack/java/src/Player.java | 1 + 10_Blackjack/java/test/GameTest.java | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/10_Blackjack/java/src/Player.java b/10_Blackjack/java/src/Player.java index 747125e0..2553873d 100644 --- a/10_Blackjack/java/src/Player.java +++ b/10_Blackjack/java/src/Player.java @@ -2,6 +2,7 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; +// TODO fill out the javadoc for this class public class Player { private int playerNumber; // e.g. playerNumber = 1 means "this is Player 1" diff --git a/10_Blackjack/java/test/GameTest.java b/10_Blackjack/java/test/GameTest.java index 61e17e58..706c3bb3 100644 --- a/10_Blackjack/java/test/GameTest.java +++ b/10_Blackjack/java/test/GameTest.java @@ -20,6 +20,15 @@ public class GameTest { private StringWriter out; private Game game; + // TODO See if it's possible to initialize test data in a more readable way. + // e.g. + // playerGets(Card) + // playerGets(Card) + // playerDoes(String) + // playerGets(Card) + // playerDoes(String) + // initGame(g) // Creates the deck and initializes 'in' based on values accumulated from prior calls to playerGets and playerDoes. + private void givenStubGame() { in = new StringReader(""); out = new StringWriter(); From 193edaa4284cfa7113c1c3d1c7cb6e02a5b28a40 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Thu, 3 Mar 2022 21:43:29 -0600 Subject: [PATCH 34/51] Fix toString for Ace --- 10_Blackjack/java/src/Card.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/10_Blackjack/java/src/Card.java b/10_Blackjack/java/src/Card.java index 53f7f6dd..a29f0283 100644 --- a/10_Blackjack/java/src/Card.java +++ b/10_Blackjack/java/src/Card.java @@ -44,7 +44,9 @@ public final class Card { public String toString() { StringBuilder result = new StringBuilder(2); - if(value < 11) { + if(value == 1) { + result.append("A"); + } else if(value < 11) { result.append(value); } else if(value == 11) { result.append('J'); From 7e09a58f32793c68899e32e881232220fb823e1b Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Thu, 3 Mar 2022 21:44:42 -0600 Subject: [PATCH 35/51] Fix output for Double Down --- 10_Blackjack/java/src/Game.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index 3cb49eee..a53fb80c 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -142,8 +142,6 @@ public class Game { } private void play(Player player, int handNumber) { - List hand = player.getHand(handNumber); - String action; if(player.isSplit()){ action = userIo.prompt("HAND #" + handNumber); @@ -154,16 +152,22 @@ public class Game { if(action.equalsIgnoreCase("H")){ // HIT Card c = deck.deal(); player.dealCard(c, handNumber); - if(scoreHand(hand) > 21){ + if(scoreHand(player.getHand(handNumber)) > 21){ userIo.println("...BUSTED"); - return; + break; } action = userIo.prompt("RECEIVED A " + c.toString() + " HIT"); } else if(action.equalsIgnoreCase("S")){ // STAY return; } else if(action.equalsIgnoreCase("D") && player.canDoubleDown(handNumber)) { // DOUBLE DOWN - player.doubleDown(deck.deal(), handNumber); - return; + Card c = deck.deal(); + player.doubleDown(c, handNumber); + if(scoreHand(player.getHand(handNumber)) > 21){ + userIo.println("...BUSTED"); + break; + } + userIo.println("RECEIVED A " + c.toString()); + break; } else if(action.equalsIgnoreCase("/")) { // SPLIT if(player.isSplit()) { // The original basic code printed different output @@ -189,7 +193,7 @@ public class Game { userIo.println("SECOND HAND RECEIVES A " + card.toString()); } play(player, 2); - return; + break; } else { userIo.println("SPLITTING NOT ALLOWED"); action = userIo.prompt("PLAYER " + player.getPlayerNumber() + " "); From 9fac5530e4bfdd23ff2a6d4806ac4806c2154323 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Thu, 3 Mar 2022 21:46:24 -0600 Subject: [PATCH 36/51] Print total of each hand --- 10_Blackjack/java/src/Game.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index a53fb80c..a254aa4a 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -193,7 +193,7 @@ public class Game { userIo.println("SECOND HAND RECEIVES A " + card.toString()); } play(player, 2); - break; + return; // Don't fall out of the while loop and print another total } else { userIo.println("SPLITTING NOT ALLOWED"); action = userIo.prompt("PLAYER " + player.getPlayerNumber() + " "); @@ -206,7 +206,7 @@ public class Game { } } } - + userIo.println("TOTAL IS " + scoreHand(player.getHand(handNumber))); } /** From 1f24d98a72c57abeebd8b2bdad6fb0986ef9b396 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Thu, 3 Mar 2022 21:49:47 -0600 Subject: [PATCH 37/51] Make play() tests more readable --- 10_Blackjack/java/test/GameTest.java | 171 +++++++++++++++++---------- 1 file changed, 107 insertions(+), 64 deletions(-) diff --git a/10_Blackjack/java/test/GameTest.java b/10_Blackjack/java/test/GameTest.java index 706c3bb3..84cd7d58 100644 --- a/10_Blackjack/java/test/GameTest.java +++ b/10_Blackjack/java/test/GameTest.java @@ -1,5 +1,7 @@ import org.junit.jupiter.api.Test; + import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import static org.junit.jupiter.api.Assertions.assertAll; @@ -20,38 +22,32 @@ public class GameTest { private StringWriter out; private Game game; - // TODO See if it's possible to initialize test data in a more readable way. - // e.g. - // playerGets(Card) - // playerGets(Card) - // playerDoes(String) - // playerGets(Card) - // playerDoes(String) - // initGame(g) // Creates the deck and initializes 'in' based on values accumulated from prior calls to playerGets and playerDoes. + private StringBuilder playerActions; + private LinkedList cards; - 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); + @BeforeEach + public void resetIo() { + in = null; + out = null; + game = null; + playerActions = new StringBuilder(); + cards = new LinkedList<>(); } - 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 playerGets(int value, Card.Suit suit) { + cards.add(new Card(value, suit)); } - private void givenInput(String input, Card... customCards) { - in = new StringReader(input); + private void playerSays(String action) { + playerActions.append(action).append(System.lineSeparator()); + } + + private void initGame() { + System.out.printf("Running game with input: %s\tand cards: %s\n",playerActions.toString(), cards); + in = new StringReader(playerActions.toString()); out = new StringWriter(); UserIo userIo = new UserIo(in, out); - LinkedList cardList = new LinkedList<>(); - cardList.addAll(Arrays.asList(customCards)); - Deck deck = new Deck((cards) -> cardList); + Deck deck = new Deck((c) -> cards); game = new Game(deck, userIo); } @@ -63,7 +59,8 @@ public class GameTest { @Test public void shouldQuitOnCtrlD() { // Given - givenInput("\u2404"); // U+2404 is "End of Transmission" sent by CTRL+D (or CTRL+Z on Windows) + playerSays("\u2404"); // U+2404 is "End of Transmission" sent by CTRL+D (or CTRL+Z on Windows) + initGame(); // When Exception e = assertThrows(UncheckedIOException.class, game::run); @@ -80,7 +77,8 @@ public class GameTest { 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." + playerSays("S"); // "I also like to live dangerously." + initGame(); // When game.play(player); @@ -97,10 +95,13 @@ public class GameTest { player.dealCard(new Card(10, Card.Suit.HEARTS)); player.dealCard(new Card(10, 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! + playerSays("H"); + playerGets(1, Card.Suit.SPADES); // 20 + playerSays("H"); + playerGets(1, Card.Suit.HEARTS); // 21 + playerSays("H"); + playerGets(1, Card.Suit.CLUBS); // 22 - D'oh! + initGame(); // When game.play(player); @@ -112,13 +113,16 @@ public class GameTest { @Test @DisplayName("Should allow double down on initial turn") public void playDoubleDown(){ + System.out.println("Here"); // 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)); + playerSays("D"); + playerGets(7, Card.Suit.SPADES); + initGame(); // When game.play(player); @@ -137,7 +141,11 @@ public class GameTest { 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)); + playerSays("H"); + playerGets(7, Card.Suit.SPADES); + playerSays("D"); + playerSays("S"); + initGame(); // When game.play(player); @@ -150,12 +158,13 @@ public class GameTest { @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)); + initGame(); + // When int result = game.scoreHand(hand); @@ -167,12 +176,13 @@ public class GameTest { @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)); + initGame(); + // When int result = game.scoreHand(hand); @@ -184,11 +194,12 @@ public class GameTest { @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)); + initGame(); + // When int result = game.scoreHand(hand); @@ -200,12 +211,13 @@ public class GameTest { @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)); + initGame(); + // When int result = game.scoreHand(hand); @@ -217,12 +229,13 @@ public class GameTest { @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)); + initGame(); + // When int result = game.scoreHand(hand); @@ -233,8 +246,6 @@ public class GameTest { @Test @DisplayName("compareHands should return 1 meaning A beat B, 20 to 12") public void compareHandsAWins() { - - givenStubGame(); LinkedList handA = new LinkedList<>(); handA.add(new Card(10, Card.Suit.SPADES)); handA.add(new Card(10, Card.Suit.CLUBS)); @@ -243,6 +254,8 @@ public class GameTest { handB.add(new Card(1, Card.Suit.SPADES)); handB.add(new Card(1, Card.Suit.CLUBS)); + initGame(); + int result = game.compareHands(handA,handB); assertEquals(1, result); @@ -251,7 +264,6 @@ public class GameTest { @Test @DisplayName("compareHands should return -1 meaning B beat A, 18 to 4") public void compareHandsBwins() { - givenStubGame(); LinkedList handA = new LinkedList<>(); handA.add(new Card(2, Card.Suit.SPADES)); handA.add(new Card(2, Card.Suit.CLUBS)); @@ -261,6 +273,8 @@ public class GameTest { handB.add(new Card(6, Card.Suit.HEARTS)); handB.add(new Card(7, Card.Suit.CLUBS)); + initGame(); + int result = game.compareHands(handA,handB); assertEquals(-1, result); @@ -270,7 +284,6 @@ public class GameTest { @DisplayName("compareHands should return 1 meaning A beat B, natural Blackjack to Blackjack") public void compareHandsAWinsWithNaturalBlackJack() { //Hand A wins with natural BlackJack, B with Blackjack - givenStubGame(); LinkedList handA = new LinkedList<>(); handA.add(new Card(10, Card.Suit.SPADES)); handA.add(new Card(1, Card.Suit.CLUBS)); @@ -280,6 +293,8 @@ public class GameTest { handB.add(new Card(7, Card.Suit.HEARTS)); handB.add(new Card(8, Card.Suit.CLUBS)); + initGame(); + int result = game.compareHands(handA,handB); assertEquals(1, result); @@ -288,7 +303,6 @@ public class GameTest { @Test @DisplayName("compareHands should return -1 meaning B beat A, natural Blackjack to Blackjack") public void compareHandsBWinsWithNaturalBlackJack() { - givenStubGame(); LinkedList handA = new LinkedList<>(); handA.add(new Card(6, Card.Suit.SPADES)); handA.add(new Card(7, Card.Suit.HEARTS)); @@ -298,6 +312,8 @@ public class GameTest { handB.add(new Card(10, Card.Suit.SPADES)); handB.add(new Card(1, Card.Suit.CLUBS)); + initGame(); + int result = game.compareHands(handA,handB); assertEquals(-1, result); @@ -306,7 +322,6 @@ public class GameTest { @Test @DisplayName("compareHands should return 0, hand A and B tied with a Blackjack") public void compareHandsTieBothBlackJack() { - givenStubGame(); LinkedList handA = new LinkedList<>(); handA.add(new Card(11, Card.Suit.SPADES)); handA.add(new Card(10, Card.Suit.CLUBS)); @@ -315,6 +330,8 @@ public class GameTest { handB.add(new Card(10, Card.Suit.SPADES)); handB.add(new Card(11, Card.Suit.CLUBS)); + initGame(); + int result = game.compareHands(handA,handB); assertEquals(0, result); @@ -323,7 +340,6 @@ public class GameTest { @Test @DisplayName("compareHands should return 0, hand A and B tie without a Blackjack") public void compareHandsTieNoBlackJack() { - givenStubGame(); LinkedList handA = new LinkedList<>(); handA.add(new Card(10, Card.Suit.DIAMONDS)); handA.add(new Card(10, Card.Suit.HEARTS)); @@ -332,6 +348,8 @@ public class GameTest { handB.add(new Card(10, Card.Suit.SPADES)); handB.add(new Card(10, Card.Suit.CLUBS)); + initGame(); + int result = game.compareHands(handA,handB); assertEquals(0, result); @@ -344,7 +362,13 @@ public class GameTest { Player player = new Player(1); player.dealCard(new Card(1, Card.Suit.CLUBS)); player.dealCard(new Card(1, Card.Suit.SPADES)); - givenInput("/\nS\nS\n"); + + playerSays("/"); + playerGets(2, Card.Suit.SPADES); // First hand + playerSays("S"); + playerGets(2, Card.Suit.SPADES); // Second hand + playerSays("S"); + initGame(); // When game.play(player); @@ -362,10 +386,13 @@ public class GameTest { player.dealCard(new Card(10, Card.Suit.HEARTS)); player.dealCard(new Card(10, Card.Suit.SPADES)); - 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. + playerSays("/"); + playerGets(12, Card.Suit.SPADES); // First hand has 20 + playerSays("H"); + playerGets(12, Card.Suit.HEARTS); // First hand busted + playerGets(10, Card.Suit.HEARTS); // Second hand gets a 10 + playerSays("S"); + initGame(); // When game.play(player); @@ -382,10 +409,14 @@ public class GameTest { 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 + playerSays("/"); + playerGets(1, Card.Suit.CLUBS); // First hand is 21 + playerSays("S"); + playerGets(12, Card.Suit.SPADES); // Second hand is 20 + playerSays("H"); + playerGets(12, Card.Suit.HEARTS); // Busted + playerSays("H"); + initGame(); // When game.play(player); @@ -403,10 +434,14 @@ public class GameTest { player.dealCard(new Card(9, Card.Suit.HEARTS)); player.dealCard(new Card(9, Card.Suit.SPADES)); - givenInput("/\nD\nD\n", - new Card(5, Card.Suit.DIAMONDS), - new Card(6, Card.Suit.HEARTS), - new Card(7, Card.Suit.CLUBS)); + playerSays("/"); + playerGets(5, Card.Suit.DIAMONDS); // First hand is 14 + playerSays("D"); + playerGets(6, Card.Suit.HEARTS); // First hand is 20 + playerGets(7, Card.Suit.CLUBS); // Second hand is 16 + playerSays("D"); + playerGets(4, Card.Suit.CLUBS); // Second hand is 20 + initGame(); // When game.play(player); @@ -429,9 +464,13 @@ public class GameTest { player.dealCard(new Card(1, Card.Suit.HEARTS)); player.dealCard(new Card(1, Card.Suit.SPADES)); - givenInput("/\n/\nS\nS\n", - new Card(13, Card.Suit.CLUBS), - new Card(13, Card.Suit.SPADES)); + playerSays("/"); + playerGets(13, Card.Suit.CLUBS); // First hand + playerSays("/"); // Not allowed + playerSays("S"); + playerGets(13, Card.Suit.SPADES); // Second hand + playerSays("S"); + initGame(); // When game.play(player); @@ -449,9 +488,13 @@ public class GameTest { 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)); + playerSays("/"); + playerGets(13, Card.Suit.CLUBS); // First hand + playerSays("S"); + playerGets(13, Card.Suit.SPADES); // Second hand + playerSays("/"); // Not allowed + playerSays("S"); + initGame(); // When game.play(player); From f7a257e6aeb3500e3cc6537fbbd51532e3acf6b6 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Thu, 3 Mar 2022 21:55:30 -0600 Subject: [PATCH 38/51] Refactor scoreHand and compareHands into ScoringUtils --- 10_Blackjack/java/src/Game.java | 53 +---- 10_Blackjack/java/src/ScoringUtils.java | 52 +++++ 10_Blackjack/java/test/GameTest.java | 201 ------------------- 10_Blackjack/java/test/ScoringUtilsTest.java | 143 +++++++++++++ 4 files changed, 198 insertions(+), 251 deletions(-) create mode 100644 10_Blackjack/java/src/ScoringUtils.java create mode 100644 10_Blackjack/java/test/ScoringUtilsTest.java diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index a254aa4a..5a1736e3 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -152,7 +152,7 @@ public class Game { if(action.equalsIgnoreCase("H")){ // HIT Card c = deck.deal(); player.dealCard(c, handNumber); - if(scoreHand(player.getHand(handNumber)) > 21){ + if(ScoringUtils.scoreHand(player.getHand(handNumber)) > 21){ userIo.println("...BUSTED"); break; } @@ -162,7 +162,7 @@ public class Game { } else if(action.equalsIgnoreCase("D") && player.canDoubleDown(handNumber)) { // DOUBLE DOWN Card c = deck.deal(); player.doubleDown(c, handNumber); - if(scoreHand(player.getHand(handNumber)) > 21){ + if(ScoringUtils.scoreHand(player.getHand(handNumber)) > 21){ userIo.println("...BUSTED"); break; } @@ -206,54 +206,7 @@ public class Game { } } } - userIo.println("TOTAL IS " + scoreHand(player.getHand(handNumber))); - } - - /** - * Calculates the value of a hand. When the hand contains aces, it will - * count one of them as 11 if that does not result in a bust. - * - * @param hand the hand to evaluate - * @return The numeric value of a hand. A value over 21 indicates a bust. - */ - protected int scoreHand(List 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 using the - * java.lang.Comparable convention of returning positive or negative integers - * - * @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. - */ - protected int compareHands(List handA, List handB) { - int scoreA = this.scoreHand(handA); - int scoreB = this.scoreHand(handB); - - if(scoreA == 21 && scoreB == 21){ - if(handA.size() == 2 && handB.size() != 2){ - return 1; //Hand A wins with a natural blackjack - } else if (handA.size() != 2 && handB.size() == 2) { - return -1; //Hand B wins with a natural blackjack - } else { - return 0; //Tie - } - } else { - return Integer.compare(scoreA, scoreB); - } + userIo.println("TOTAL IS " + ScoringUtils.scoreHand(player.getHand(handNumber))); } /** diff --git a/10_Blackjack/java/src/ScoringUtils.java b/10_Blackjack/java/src/ScoringUtils.java new file mode 100644 index 00000000..df149e27 --- /dev/null +++ b/10_Blackjack/java/src/ScoringUtils.java @@ -0,0 +1,52 @@ +import java.util.List; + +public final class ScoringUtils { + + /** + * Calculates the value of a hand. When the hand contains aces, it will + * count one of them as 11 if that does not result in a bust. + * + * @param hand the hand to evaluate + * @return The numeric value of a hand. A value over 21 indicates a bust. + */ + public static final int scoreHand(List 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 using the + * java.lang.Comparable convention of returning positive or negative integers + * + * @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. + */ + public static final int compareHands(List handA, List handB) { + int scoreA = scoreHand(handA); + int scoreB = scoreHand(handB); + + if(scoreA == 21 && scoreB == 21){ + if(handA.size() == 2 && handB.size() != 2){ + return 1; //Hand A wins with a natural blackjack + } else if (handA.size() != 2 && handB.size() == 2) { + return -1; //Hand B wins with a natural blackjack + } else { + return 0; //Tie + } + } else { + return Integer.compare(scoreA, scoreB); + } + } + +} diff --git a/10_Blackjack/java/test/GameTest.java b/10_Blackjack/java/test/GameTest.java index 84cd7d58..e6807219 100644 --- a/10_Blackjack/java/test/GameTest.java +++ b/10_Blackjack/java/test/GameTest.java @@ -154,207 +154,6 @@ public class GameTest { assertTrue(out.toString().contains("TYPE H, OR S, PLEASE")); } - @Test - @DisplayName("scoreHand should sum non-ace values normally") - public void scoreHandNormally() { - // Given - 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)); - - initGame(); - - // When - int result = game.scoreHand(hand); - - // Then - assertEquals(20, result); - } - - @Test - @DisplayName("scoreHand should treat face cards as 10") - public void scoreHandFaceCards() { - // Given - 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)); - - initGame(); - - // When - int result = game.scoreHand(hand); - - // Then - assertEquals(30, result); - } - - @Test - @DisplayName("scoreHand should score aces as 11 when possible") - public void scoreHandSoftAce() { - // Given - LinkedList hand = new LinkedList<>(); - hand.add(new Card(10, Card.Suit.SPADES)); - hand.add(new Card(1, Card.Suit.SPADES)); - - initGame(); - - // 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 - 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)); - - initGame(); - - // When - int result = game.scoreHand(hand); - - // Then - assertEquals(20, result); - } - - @Test - @DisplayName("scoreHand should score 3 aces as 13") - public void scoreHandMultipleAces() { - // Given - 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)); - - initGame(); - - // When - int result = game.scoreHand(hand); - - // Then - assertEquals(13, result); - } - - @Test - @DisplayName("compareHands should return 1 meaning A beat B, 20 to 12") - public void compareHandsAWins() { - LinkedList handA = new LinkedList<>(); - handA.add(new Card(10, Card.Suit.SPADES)); - handA.add(new Card(10, Card.Suit.CLUBS)); - - LinkedList handB = new LinkedList<>(); - handB.add(new Card(1, Card.Suit.SPADES)); - handB.add(new Card(1, Card.Suit.CLUBS)); - - initGame(); - - int result = game.compareHands(handA,handB); - - assertEquals(1, result); - } - - @Test - @DisplayName("compareHands should return -1 meaning B beat A, 18 to 4") - public void compareHandsBwins() { - LinkedList handA = new LinkedList<>(); - handA.add(new Card(2, Card.Suit.SPADES)); - handA.add(new Card(2, Card.Suit.CLUBS)); - - LinkedList handB = new LinkedList<>(); - handB.add(new Card(5, Card.Suit.SPADES)); - handB.add(new Card(6, Card.Suit.HEARTS)); - handB.add(new Card(7, Card.Suit.CLUBS)); - - initGame(); - - int result = game.compareHands(handA,handB); - - assertEquals(-1, result); - } - - @Test - @DisplayName("compareHands should return 1 meaning A beat B, natural Blackjack to Blackjack") - public void compareHandsAWinsWithNaturalBlackJack() { - //Hand A wins with natural BlackJack, B with Blackjack - LinkedList handA = new LinkedList<>(); - handA.add(new Card(10, Card.Suit.SPADES)); - handA.add(new Card(1, Card.Suit.CLUBS)); - - LinkedList handB = new LinkedList<>(); - handB.add(new Card(6, Card.Suit.SPADES)); - handB.add(new Card(7, Card.Suit.HEARTS)); - handB.add(new Card(8, Card.Suit.CLUBS)); - - initGame(); - - int result = game.compareHands(handA,handB); - - assertEquals(1, result); - } - - @Test - @DisplayName("compareHands should return -1 meaning B beat A, natural Blackjack to Blackjack") - public void compareHandsBWinsWithNaturalBlackJack() { - LinkedList handA = new LinkedList<>(); - handA.add(new Card(6, Card.Suit.SPADES)); - handA.add(new Card(7, Card.Suit.HEARTS)); - handA.add(new Card(8, Card.Suit.CLUBS)); - - LinkedList handB = new LinkedList<>(); - handB.add(new Card(10, Card.Suit.SPADES)); - handB.add(new Card(1, Card.Suit.CLUBS)); - - initGame(); - - int result = game.compareHands(handA,handB); - - assertEquals(-1, result); - } - - @Test - @DisplayName("compareHands should return 0, hand A and B tied with a Blackjack") - public void compareHandsTieBothBlackJack() { - LinkedList handA = new LinkedList<>(); - handA.add(new Card(11, Card.Suit.SPADES)); - handA.add(new Card(10, Card.Suit.CLUBS)); - - LinkedList handB = new LinkedList<>(); - handB.add(new Card(10, Card.Suit.SPADES)); - handB.add(new Card(11, Card.Suit.CLUBS)); - - initGame(); - - int result = game.compareHands(handA,handB); - - assertEquals(0, result); - } - - @Test - @DisplayName("compareHands should return 0, hand A and B tie without a Blackjack") - public void compareHandsTieNoBlackJack() { - LinkedList handA = new LinkedList<>(); - handA.add(new Card(10, Card.Suit.DIAMONDS)); - handA.add(new Card(10, Card.Suit.HEARTS)); - - LinkedList handB = new LinkedList<>(); - handB.add(new Card(10, Card.Suit.SPADES)); - handB.add(new Card(10, Card.Suit.CLUBS)); - - initGame(); - - int result = game.compareHands(handA,handB); - - assertEquals(0, result); - } - @Test @DisplayName("play() should end on STAY after split") public void playSplitEndOnStay(){ diff --git a/10_Blackjack/java/test/ScoringUtilsTest.java b/10_Blackjack/java/test/ScoringUtilsTest.java new file mode 100644 index 00000000..a6c3877f --- /dev/null +++ b/10_Blackjack/java/test/ScoringUtilsTest.java @@ -0,0 +1,143 @@ +import org.junit.jupiter.api.Test; + +import org.junit.jupiter.api.DisplayName; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.LinkedList; + +public class ScoringUtilsTest { + + @Test + @DisplayName("scoreHand should score aces as 1 when using 11 would bust") + public void scoreHandHardAce() { + // Given + 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 = ScoringUtils.scoreHand(hand); + + // Then + assertEquals(20, result); + } + + @Test + @DisplayName("scoreHand should score 3 aces as 13") + public void scoreHandMultipleAces() { + // Given + 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 = ScoringUtils.scoreHand(hand); + + // Then + assertEquals(13, result); + } + + @Test + @DisplayName("compareHands should return 1 meaning A beat B, 20 to 12") + public void compareHandsAWins() { + LinkedList handA = new LinkedList<>(); + handA.add(new Card(10, Card.Suit.SPADES)); + handA.add(new Card(10, Card.Suit.CLUBS)); + + LinkedList handB = new LinkedList<>(); + handB.add(new Card(1, Card.Suit.SPADES)); + handB.add(new Card(1, Card.Suit.CLUBS)); + + int result = ScoringUtils.compareHands(handA,handB); + + assertEquals(1, result); + } + + @Test + @DisplayName("compareHands should return -1 meaning B beat A, 18 to 4") + public void compareHandsBwins() { + LinkedList handA = new LinkedList<>(); + handA.add(new Card(2, Card.Suit.SPADES)); + handA.add(new Card(2, Card.Suit.CLUBS)); + + LinkedList handB = new LinkedList<>(); + handB.add(new Card(5, Card.Suit.SPADES)); + handB.add(new Card(6, Card.Suit.HEARTS)); + handB.add(new Card(7, Card.Suit.CLUBS)); + + int result = ScoringUtils.compareHands(handA,handB); + + assertEquals(-1, result); + } + + @Test + @DisplayName("compareHands should return 1 meaning A beat B, natural Blackjack to Blackjack") + public void compareHandsAWinsWithNaturalBlackJack() { + //Hand A wins with natural BlackJack, B with Blackjack + LinkedList handA = new LinkedList<>(); + handA.add(new Card(10, Card.Suit.SPADES)); + handA.add(new Card(1, Card.Suit.CLUBS)); + + LinkedList handB = new LinkedList<>(); + handB.add(new Card(6, Card.Suit.SPADES)); + handB.add(new Card(7, Card.Suit.HEARTS)); + handB.add(new Card(8, Card.Suit.CLUBS)); + + int result = ScoringUtils.compareHands(handA,handB); + + assertEquals(1, result); + } + + @Test + @DisplayName("compareHands should return -1 meaning B beat A, natural Blackjack to Blackjack") + public void compareHandsBWinsWithNaturalBlackJack() { + LinkedList handA = new LinkedList<>(); + handA.add(new Card(6, Card.Suit.SPADES)); + handA.add(new Card(7, Card.Suit.HEARTS)); + handA.add(new Card(8, Card.Suit.CLUBS)); + + LinkedList handB = new LinkedList<>(); + handB.add(new Card(10, Card.Suit.SPADES)); + handB.add(new Card(1, Card.Suit.CLUBS)); + + int result = ScoringUtils.compareHands(handA,handB); + + assertEquals(-1, result); + } + + @Test + @DisplayName("compareHands should return 0, hand A and B tied with a Blackjack") + public void compareHandsTieBothBlackJack() { + LinkedList handA = new LinkedList<>(); + handA.add(new Card(11, Card.Suit.SPADES)); + handA.add(new Card(10, Card.Suit.CLUBS)); + + LinkedList handB = new LinkedList<>(); + handB.add(new Card(10, Card.Suit.SPADES)); + handB.add(new Card(11, Card.Suit.CLUBS)); + + int result = ScoringUtils.compareHands(handA,handB); + + assertEquals(0, result); + } + + @Test + @DisplayName("compareHands should return 0, hand A and B tie without a Blackjack") + public void compareHandsTieNoBlackJack() { + LinkedList handA = new LinkedList<>(); + handA.add(new Card(10, Card.Suit.DIAMONDS)); + handA.add(new Card(10, Card.Suit.HEARTS)); + + LinkedList handB = new LinkedList<>(); + handB.add(new Card(10, Card.Suit.SPADES)); + handB.add(new Card(10, Card.Suit.CLUBS)); + + int result = ScoringUtils.compareHands(handA,handB); + + assertEquals(0, result); + } + +} From c7e684a99af350bab7694332b85fd52ca93e8cb3 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Thu, 3 Mar 2022 22:00:10 -0600 Subject: [PATCH 39/51] Simplify bet validation --- 10_Blackjack/java/src/Game.java | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index 5a1736e3..cb545663 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -1,5 +1,6 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.LinkedList; import java.util.List; @@ -47,16 +48,11 @@ public class Game { } while(true) { - double[] bets = new double[nPlayers]; // empty array initialized with all '0' valuses. - while(!betsAreValid(bets)){ + while(!betsAreValid(players)){ userIo.println("BETS:"); for(int i = 0; i < nPlayers; i++) { - // Note that the bet for player "1" is at index "0" in the bets - // array and take care to avoid off-by-one errors. - // TODO the BASIC version allows fractional bets (e.g. '1.5') of arbitrary precision - // We can use a double if we want--the original basic has doubleing point errors. Or we could use BigDecimal if we really want to fix that. - bets[i] = userIo.promptDouble("#" + (i + 1)); //TODO: If there isn't a need for a separate Bets in the future, combine these two lines and convert to enhanced FOR loop - players.get(i).setCurrentBet(bets[i]); + double bet = userIo.promptDouble("#" + (i + 1)); // 1st player is "Player 1" not "Player 0" + players.get(i).setCurrentBet(bet); } } @@ -246,11 +242,12 @@ public class Game { /** * Validates that all bets are between 0 (exclusive) and 500 (inclusive). Fractional bets are valid. * - * @param bets The array of bets for each player. + * @param players The players with their current bet set. * @return true if all bets are valid, false otherwise. */ - public boolean betsAreValid(double[] bets) { - return Arrays.stream(bets) + public boolean betsAreValid(Collection players) { + return players.stream() + .map(Player::getCurrentBet) .allMatch(bet -> bet > 0 && bet <= 500); } From 213f77678fc745a22690d9daf8bfff880ebfc236 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Thu, 3 Mar 2022 22:10:19 -0600 Subject: [PATCH 40/51] Mimic a real deal Just because it's fun. --- 10_Blackjack/java/src/Game.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index cb545663..de3d0e44 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -56,12 +56,15 @@ public class Game { } } - for(Player player : players){ - player.dealCard(deck.deal()); - player.dealCard(deck.deal()); //TODO: This could be in a separate loop to more acurrately follow how a game would be dealt, I couldn't figure out of the BASIC version did it + // It doesn't *really* matter whether we deal two cards at once to each player + // or one card to each and then a second card to each, but this technically + // mimics the way a deal works in real life. + for(int i = 0; i < 2; i++){ + for(Player player : players){ + player.dealCard(deck.deal()); + } } - // Consider adding a Dealer class to track the dealer's hand and running total. // Alternately, the dealer could just be a Player instance where currentBet=0 and is ignored. LinkedList dealerHand = new LinkedList<>(); From 375f16c4a835b2ac9bcb7c47a0784affb2ec2fea Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Thu, 3 Mar 2022 22:10:39 -0600 Subject: [PATCH 41/51] Print total after STAY --- 10_Blackjack/java/src/Game.java | 2 +- 10_Blackjack/java/test/GameTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index de3d0e44..dc7852fb 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -157,7 +157,7 @@ public class Game { } action = userIo.prompt("RECEIVED A " + c.toString() + " HIT"); } else if(action.equalsIgnoreCase("S")){ // STAY - return; + break; } else if(action.equalsIgnoreCase("D") && player.canDoubleDown(handNumber)) { // DOUBLE DOWN Card c = deck.deal(); player.doubleDown(c, handNumber); diff --git a/10_Blackjack/java/test/GameTest.java b/10_Blackjack/java/test/GameTest.java index e6807219..0dd004db 100644 --- a/10_Blackjack/java/test/GameTest.java +++ b/10_Blackjack/java/test/GameTest.java @@ -84,7 +84,7 @@ public class GameTest { game.play(player); // Then - assertEquals("PLAYER 1 ? ", out.toString()); + assertTrue(out.toString().startsWith("PLAYER 1 ? TOTAL IS 5")); } @Test From 84a70ad1ff01f6df350d9db155a1b99fa4fb6510 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Fri, 4 Mar 2022 13:02:26 -0600 Subject: [PATCH 42/51] Handle insurance --- 10_Blackjack/java/src/Game.java | 53 ++++++++++++++++++++----------- 10_Blackjack/java/src/Player.java | 11 +++++++ 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index dc7852fb..433a94f6 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -70,29 +70,44 @@ public class Game { LinkedList dealerHand = new LinkedList<>(); Player dealer = new Player(0); //Dealer is Player 0 - this can be converted into a dealer class later on dealer.dealCard(deck.deal()); - // TODO deal two cards to the dealer - - // TODO handle 'insurance' if the dealer's card is an Ace. + dealer.dealCard(deck.deal()); printInitialDeal(players, dealer); - // TODO if dealer has an ACE, prompt "ANY INSURANCE" - // if yes, print "INSURANCE BETS" and prompt each player with "# [x] ? " where X is player number - // insurance bets must be equal or less than half the player's regular bet - - // TODO check for dealer blackjack - // if blackjack, print "DEALER HAS A [x] IN THE HOLE\nFOR BLACKJACK" and skip to evaluateRound - // pay 2x insurance bets (insurance bet of 5 pays 10) if applicable - // if not, print "NO DEALER BLACKJACK" - // collect insurance bets if applicable - - for(Player player : players){ - play(player); + if(dealer.getHand().get(0).getValue() == 1) { + boolean isInsurance = userIo.promptBoolean("ANY INSURANCE"); + if(isInsurance) { + userIo.println("INSURANCE BETS"); + for(Player player : players) { + while(true) { + double insuranceBet = userIo.promptDouble("# " + player.getPlayerNumber() + " "); + // 0 indicates no insurance for that player. + if(insuranceBet >= 0 && insuranceBet <= player.getCurrentBet()) { + player.setInsuranceBet(insuranceBet); + break; + } + } + } + } } - // only play the dealer if at least one player has not busted or gotten a natural blackjack (21 in the first two cards) - // otherwise, just print the dealer's concealed card - dealerHand = playDealer(dealerHand, deck); + if(ScoringUtils.scoreHand(dealer.getHand()) == 21) { + userIo.println("DEALER HAS A " + dealer.getHand().get(1).toString() + " IN THE HOLE"); + userIo.println("FOR BLACKJACK"); + } else { + Card dealerFirstCard = dealer.getHand().get(0); + if(dealerFirstCard.getValue() == 1 || dealerFirstCard.getValue() > 9) { + userIo.println(""); + userIo.println("NO DEALER BLACKJACK."); + } // else dealer blackjack is imposible + for(Player player : players){ + play(player); + } + + // TODO only play the dealer if at least one player has not busted or gotten a natural blackjack (21 in the first two cards) + // otherwise, just print the dealer's concealed card + dealerHand = playDealer(dealerHand, deck); + } evaluateRound(players, dealerHand); } @@ -236,6 +251,8 @@ public class Game { PLAYER 1 LOSES 100 TOTAL=-100 PLAYER 2 WINS 150 TOTAL= 150 DEALER'S TOTAL= 200 + // In "WINS X TOTAL= Y" the value of 'X' combines normal and insurance bets. e.g. if you bet 100 and get 5 insurance, + // then win the hand but lose insurance, it will say "WINS 95" */ // this should probably take in a "Dealer" instance instead of just the dealer hand so we can update the dealer's total. // currentBets of each player are added/subtracted from the dealer total depending on whether they win/lose (accounting for doubling down, insurance etc.) diff --git a/10_Blackjack/java/src/Player.java b/10_Blackjack/java/src/Player.java index 2553873d..699a4f69 100644 --- a/10_Blackjack/java/src/Player.java +++ b/10_Blackjack/java/src/Player.java @@ -64,6 +64,17 @@ public class Player { total = total - currentBet; currentBet = 0; } + + public void recordInsuranceWin() { + total = total + (insuranceBet * 2); + insuranceBet = 0; + } + + public void recordInsuranceLoss() { + total = total - insuranceBet; + insuranceBet = 0; + } + /** * Returns the total of all bets won. * @return Total value From 926612b736b6b932b52447a8932a6a33f354bb86 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Sat, 5 Mar 2022 09:28:27 -0600 Subject: [PATCH 43/51] Test insurance collection --- 10_Blackjack/java/src/Game.java | 32 ++++---- 10_Blackjack/java/test/GameTest.java | 112 ++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 15 deletions(-) diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index 433a94f6..1733ae96 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -75,20 +75,7 @@ public class Game { printInitialDeal(players, dealer); if(dealer.getHand().get(0).getValue() == 1) { - boolean isInsurance = userIo.promptBoolean("ANY INSURANCE"); - if(isInsurance) { - userIo.println("INSURANCE BETS"); - for(Player player : players) { - while(true) { - double insuranceBet = userIo.promptDouble("# " + player.getPlayerNumber() + " "); - // 0 indicates no insurance for that player. - if(insuranceBet >= 0 && insuranceBet <= player.getCurrentBet()) { - player.setInsuranceBet(insuranceBet); - break; - } - } - } - } + collectInsurance(players); } if(ScoringUtils.scoreHand(dealer.getHand()) == 21) { @@ -113,6 +100,23 @@ public class Game { } } + protected void collectInsurance(Iterable players) { + boolean isInsurance = userIo.promptBoolean("ANY INSURANCE"); + if(isInsurance) { + userIo.println("INSURANCE BETS"); + for(Player player : players) { + while(true) { + double insuranceBet = userIo.promptDouble("# " + player.getPlayerNumber() + " "); + // 0 indicates no insurance for that player. + if(insuranceBet >= 0 && insuranceBet <= (player.getCurrentBet() / 2)) { + player.setInsuranceBet(insuranceBet); + break; + } + } + } + } + } + /** * Print the cards for each player and the up card for the dealer. */ diff --git a/10_Blackjack/java/test/GameTest.java b/10_Blackjack/java/test/GameTest.java index 0dd004db..360c897f 100644 --- a/10_Blackjack/java/test/GameTest.java +++ b/10_Blackjack/java/test/GameTest.java @@ -6,6 +6,7 @@ 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.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -14,7 +15,9 @@ import java.io.StringReader; import java.io.StringWriter; import java.io.UncheckedIOException; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedList; +import java.util.List; public class GameTest { @@ -70,6 +73,114 @@ public class GameTest { assertEquals("!END OF INPUT", e.getMessage()); } + @Test + @DisplayName("collectInsurance() should not prompt on N") + public void collectInsuranceNo(){ + // Given + List players = Collections.singletonList(new Player(1)); + playerSays("N"); + initGame(); + + // When + game.collectInsurance(players); + + // Then + assertAll( + () -> assertTrue(out.toString().contains("ANY INSURANCE")), + () -> assertFalse(out.toString().contains("INSURANCE BETS")) + ); + } + + @Test + @DisplayName("collectInsurance() should collect on Y") + public void collectInsuranceYes(){ + // Given + List players = Collections.singletonList(new Player(1)); + players.get(0).setCurrentBet(100); + playerSays("Y"); + playerSays("50"); + initGame(); + + // When + game.collectInsurance(players); + + // Then + assertAll( + () -> assertTrue(out.toString().contains("ANY INSURANCE")), + () -> assertTrue(out.toString().contains("INSURANCE BETS")), + () -> assertEquals(50, players.get(0).getInsuranceBet()) + ); + } + + @Test + @DisplayName("collectInsurance() should not allow more than 50% of current bet") + public void collectInsuranceYesTooMuch(){ + // Given + List players = Collections.singletonList(new Player(1)); + players.get(0).setCurrentBet(100); + playerSays("Y"); + playerSays("51"); + playerSays("50"); + initGame(); + + // When + game.collectInsurance(players); + + // Then + assertAll( + () -> assertEquals(50, players.get(0).getInsuranceBet()), + () -> assertTrue(out.toString().contains("# 1 ? # 1 ?")) + ); + } + + @Test + @DisplayName("collectInsurance() should not allow negative bets") + public void collectInsuranceYesNegative(){ + // Given + List players = Collections.singletonList(new Player(1)); + players.get(0).setCurrentBet(100); + playerSays("Y"); + playerSays("-1"); + playerSays("1"); + initGame(); + + // When + game.collectInsurance(players); + + // Then + assertAll( + () -> assertEquals(1, players.get(0).getInsuranceBet()), + () -> assertTrue(out.toString().contains("# 1 ? # 1 ?")) + ); + } + + @Test + @DisplayName("collectInsurance() should prompt all players") + public void collectInsuranceYesTwoPlayers(){ + // Given + List players = Arrays.asList( + new Player(1), + new Player(2) + ); + players.get(0).setCurrentBet(100); + players.get(1).setCurrentBet(100); + + playerSays("Y"); + playerSays("50"); + playerSays("25"); + initGame(); + + // When + game.collectInsurance(players); + + // Then + assertAll( + () -> assertEquals(50, players.get(0).getInsuranceBet()), + () -> assertEquals(25, players.get(1).getInsuranceBet()), + () -> assertTrue(out.toString().contains("# 1 ? # 2 ?")) + ); + } + @Test @DisplayName("play() should end on STAY") public void playEndOnStay(){ @@ -113,7 +224,6 @@ public class GameTest { @Test @DisplayName("Should allow double down on initial turn") public void playDoubleDown(){ - System.out.println("Here"); // Given Player player = new Player(1); player.setCurrentBet(100); From d6e0edb75f29e58cc3775735db860afc7c4d9780 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Sat, 5 Mar 2022 09:47:15 -0600 Subject: [PATCH 44/51] Fill out javadoc --- 10_Blackjack/java/src/Blackjack.java | 23 +++++++++++ 10_Blackjack/java/src/Game.java | 4 +- 10_Blackjack/java/src/Player.java | 60 ++++++++++++++++++++++++---- 3 files changed, 79 insertions(+), 8 deletions(-) diff --git a/10_Blackjack/java/src/Blackjack.java b/10_Blackjack/java/src/Blackjack.java index bc8c2194..2809431e 100644 --- a/10_Blackjack/java/src/Blackjack.java +++ b/10_Blackjack/java/src/Blackjack.java @@ -4,6 +4,29 @@ import java.io.Reader; import java.io.Writer; import java.util.Collections; +/** + * Plays a game of blackjack on the terminal. Looking at the code, the reader + * might conclude that this implementation is "over engineered." We use many + * techniques and patterns developed for much larger code bases to create more + * maintainable code, which may not be as relevant for a simple game of + * Blackjack. To wit, the rules and requirements are not likely to ever change + * so there is not so much value making the code flexible. + * + * Nevertheless, this is meant to be an example that the reader can learn good + * Java coding techniques from. Furthermore, many of the "over-engineering" + * tactics are as much about testability as they are about maintainability. + * Imagine trying to manually test infrequent scenarios like Blackjack, + * insurance, or splitting without any ability to automate a specific scenario + * and the value of unit testing becomes immediately apparent. + * + * Another "unnecessary" aspect of this codebase is good Javadoc. Again, this is + * meant to be educational, but another often overlooked benefit is that most + * IDEs will display Javadoc in "autocomplete" suggestions. This is remarkably + * helpful when using a class as a quick reminder of what you coded earlier. + * This is true even if no one ever publishes or reads the HTML output of the + * javadoc. + * + */ public class Blackjack { public static void main(String[] args) { // Intuitively it might seem like the main program logic should be right diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index 1733ae96..4c90d9c5 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -1,9 +1,11 @@ import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.LinkedList; import java.util.List; +/** + * This is the primary class that runs the game itself. + */ public class Game { private Deck deck; diff --git a/10_Blackjack/java/src/Player.java b/10_Blackjack/java/src/Player.java index 699a4f69..0416ae8e 100644 --- a/10_Blackjack/java/src/Player.java +++ b/10_Blackjack/java/src/Player.java @@ -2,16 +2,18 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; -// TODO fill out the javadoc for this class +/** + * Represents a player and data related to them (number, bets, cards). + */ public class Player { private int playerNumber; // e.g. playerNumber = 1 means "this is Player 1" private double currentBet; - private double insuranceBet; - private double splitBet; + private double insuranceBet; // 0 when the player has not made an insurance bet (either it does not apply or they chose not to) + private double splitBet; // 0 whenever the hand is not split private double total; private LinkedList hand; - private LinkedList splitHand; + private LinkedList splitHand; // null whenever the hand is not split /** * Represents a player in the game with cards, bets, total and a playerNumber. @@ -57,6 +59,7 @@ public class Player { this.total = this.total + this.currentBet; this.currentBet = 0; } + /** * RecordLoss subtracts 'currentBet' to 'total' and then sets 'currentBet' to zero */ @@ -65,29 +68,45 @@ public class Player { currentBet = 0; } + /** + * Adds 2x the insurance bet to the players total and resets the insurance bet to zero. + */ public void recordInsuranceWin() { total = total + (insuranceBet * 2); insuranceBet = 0; } + /** + * Subtracts the insurance bet from the players total and resets the insurance bet to zero. + */ public void recordInsuranceLoss() { total = total - insuranceBet; insuranceBet = 0; } /** - * Returns the total of all bets won. + * Returns the total of all bets won/lost. * @return Total value */ public double getTotal() { return this.total; } - // dealCard adds the given card to the player's hand + /** + * Add the given card to the players main hand. + * + * @param card The card to add. + */ public void dealCard(Card card) { dealCard(card, 1); } + /** + * Adds the given card to the players hand or split hand depending on the handNumber. + * + * @param card The card to add + * @param handNumber 1 for the "first" hand and 2 for the "second" hand in a split hand scenario. + */ public void dealCard(Card card, int handNumber) { if(handNumber == 1) { hand.add(card); @@ -98,6 +117,10 @@ public class Player { } } + /** + * Determines whether the player is eligible to split. + * @return True if the player has not already split, and their hand is a pair. False otherwise. + */ public boolean canSplit() { if(isSplit()) { // Can't split twice @@ -108,6 +131,10 @@ public class Player { } } + /** + * Determines whether the player has already split their hand. + * @return false if splitHand is null, true otherwise. + */ public boolean isSplit() { return this.splitHand != null; } @@ -121,6 +148,12 @@ public class Player { splitHand.add(hand.pop()); } + /** + * Determines whether the player can double down. + * + * @param handNumber + * @return + */ public boolean canDoubleDown(int handNumber) { if(handNumber == 1){ return this.hand.size() == 2; @@ -131,6 +164,12 @@ public class Player { } } + /** + * Doubles down on the given hand. Specifically, this method doubles the bet for the given hand and deals the given card. + * + * @param card The card to deal + * @param handNumber The hand to deal to and double the bet for + */ public void doubleDown(Card card, int handNumber) { if(handNumber == 1){ this.currentBet = this.currentBet * 2; @@ -142,7 +181,9 @@ public class Player { this.dealCard(card, handNumber); } - // resetHand resets 'hand' & 'splitHand' to empty lists + /** + * Resets the hand to an empty list and the splitHand to null. + */ public void resetHand() { this.hand = new LinkedList<>(); this.splitHand = null; @@ -152,6 +193,11 @@ public class Player { return getHand(1); } + /** + * Returns the given hand + * @param handNumber 1 for the "first" of a split hand (or the main hand when there is no split) or 2 for the "second" hand of a split hand. + * @return The hand specified by handNumber + */ public List getHand(int handNumber) { if(handNumber == 1){ return Collections.unmodifiableList(this.hand); From ca0906cbfc2c8f735fa1aafb0c798be4ebf3e472 Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Sat, 5 Mar 2022 09:53:41 -0600 Subject: [PATCH 45/51] Handle 'an' vs 'a' grammar --- 10_Blackjack/java/src/Card.java | 13 +++++++++++++ 10_Blackjack/java/src/Game.java | 19 +++++-------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/10_Blackjack/java/src/Card.java b/10_Blackjack/java/src/Card.java index a29f0283..b72448dc 100644 --- a/10_Blackjack/java/src/Card.java +++ b/10_Blackjack/java/src/Card.java @@ -61,6 +61,19 @@ public final class Card { return result.toString(); } + /** + * Returns the value of {@link #toString()} preceded by either "AN " or "A " depending on which is gramatically correct. + * + * @return "AN [x]" when [x] is "an" ace or "an" 8, and "A [X]" otherwise. + */ + public String toProseString() { + if(value == 1 || value == 8) { + return "AN " + toString(); + } else { + return "A " + toString(); + } + } + @Override public boolean equals(Object obj) { // Overriding 'equals' and 'hashCode' (below) make your class work correctly diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index 4c90d9c5..24334fe0 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -81,7 +81,7 @@ public class Game { } if(ScoringUtils.scoreHand(dealer.getHand()) == 21) { - userIo.println("DEALER HAS A " + dealer.getHand().get(1).toString() + " IN THE HOLE"); + userIo.println("DEALER HAS " + dealer.getHand().get(1).toProseString() + " IN THE HOLE"); userIo.println("FOR BLACKJACK"); } else { Card dealerFirstCard = dealer.getHand().get(0); @@ -176,7 +176,7 @@ public class Game { userIo.println("...BUSTED"); break; } - action = userIo.prompt("RECEIVED A " + c.toString() + " HIT"); + action = userIo.prompt("RECEIVED " + c.toProseString() + " HIT"); } else if(action.equalsIgnoreCase("S")){ // STAY break; } else if(action.equalsIgnoreCase("D") && player.canDoubleDown(handNumber)) { // DOUBLE DOWN @@ -186,7 +186,7 @@ public class Game { userIo.println("...BUSTED"); break; } - userIo.println("RECEIVED A " + c.toString()); + userIo.println("RECEIVED " + c.toProseString()); break; } else if(action.equalsIgnoreCase("/")) { // SPLIT if(player.isSplit()) { @@ -198,20 +198,11 @@ public class Game { 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()); - } + userIo.println("FIRST HAND RECEIVES " + card.toProseString()); 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()); - } + userIo.println("SECOND HAND RECEIVES " + card.toProseString()); play(player, 2); return; // Don't fall out of the while loop and print another total } else { From 5bfb8a40885b60f8c1b30a3055f607f1ccd6dbae Mon Sep 17 00:00:00 2001 From: Mitch Peck Date: Wed, 9 Mar 2022 20:25:10 -0600 Subject: [PATCH 46/51] Implement evaluateRound with tests --- 10_Blackjack/java/src/Game.java | 53 +++++++++++++++++--- 10_Blackjack/java/src/Player.java | 33 +++---------- 10_Blackjack/java/test/GameTest.java | 73 ++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 33 deletions(-) diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index 24334fe0..e0a415b7 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.LinkedList; import java.util.List; +import java.text.DecimalFormat; /** * This is the primary class that runs the game itself. @@ -95,11 +96,11 @@ public class Game { // TODO only play the dealer if at least one player has not busted or gotten a natural blackjack (21 in the first two cards) // otherwise, just print the dealer's concealed card - dealerHand = playDealer(dealerHand, deck); + dealerHand = playDealer(dealerHand, deck); } - evaluateRound(players, dealerHand); - } + evaluateRound(players, dealer.getHand());//TODO: User dealerHand once playHeader implemented + } } protected void collectInsurance(Iterable players) { @@ -173,7 +174,7 @@ public class Game { Card c = deck.deal(); player.dealCard(c, handNumber); if(ScoringUtils.scoreHand(player.getHand(handNumber)) > 21){ - userIo.println("...BUSTED"); + userIo.println("RECEIVED " + c.toProseString() + " ...BUSTED"); break; } action = userIo.prompt("RECEIVED " + c.toProseString() + " HIT"); @@ -183,7 +184,7 @@ public class Game { Card c = deck.deal(); player.doubleDown(c, handNumber); if(ScoringUtils.scoreHand(player.getHand(handNumber)) > 21){ - userIo.println("...BUSTED"); + userIo.println("RECEIVED " + c.toProseString() + " ...BUSTED"); break; } userIo.println("RECEIVED " + c.toProseString()); @@ -241,7 +242,7 @@ public class Game { * @param players * @param dealerHand */ - private void evaluateRound(List players, LinkedList dealerHand) { + protected void evaluateRound(List players, List dealerHand) { // TODO implement evaluateRound // print something like: /* @@ -254,6 +255,46 @@ public class Game { // this should probably take in a "Dealer" instance instead of just the dealer hand so we can update the dealer's total. // currentBets of each player are added/subtracted from the dealer total depending on whether they win/lose (accounting for doubling down, insurance etc.) // remember to handle a "PUSH" when the dealer ties and the bet is returned. + + for(Player player : players){ + int result = ScoringUtils.compareHands(player.getHand(), dealerHand); + double totalBet = 0; + if(result > 0){ + totalBet += player.getCurrentBet(); + } else if(result < 0){ + totalBet -= player.getCurrentBet(); + } + if(player.isSplit()) { + int splitResult = ScoringUtils.compareHands(player.getHand(2), dealerHand); + if(splitResult > 0){ + totalBet += player.getSplitBet(); + } else if(splitResult < 0){ + totalBet -= player.getSplitBet(); + } + } + if(player.getInsuranceBet() != 0){ + int dealerResult = ScoringUtils.scoreHand(dealerHand); + if(dealerResult == 21 && dealerHand.size() == 2){ + totalBet += (player.getInsuranceBet() * 2); + } else { + totalBet -= player.getInsuranceBet(); + } + } + + userIo.print("PLAYER " + player.getPlayerNumber()); + if(totalBet < 0) { + userIo.print(" LOSES "); + } else if(totalBet > 0) { + userIo.print(" WINS "); + } else { + userIo.print(" PUSHES"); + } + player.recordRound(totalBet); + DecimalFormat formatter = new DecimalFormat("0.#"); //Removes trailing zeros + userIo.println(String.format("%6s", formatter.format(Math.abs(totalBet))) + " TOTAL= " + formatter.format(player.getTotal())); + player.resetHand(); + } + } /** diff --git a/10_Blackjack/java/src/Player.java b/10_Blackjack/java/src/Player.java index 0416ae8e..731cb37f 100644 --- a/10_Blackjack/java/src/Player.java +++ b/10_Blackjack/java/src/Player.java @@ -53,35 +53,14 @@ public class Player { } /** - * RecordWin adds 'currentBet' to 'total' and then sets 'currentBet' to zero + * RecordRound adds input paramater 'totalBet' to 'total' and then + * sets 'currentBet', 'splitBet', and 'insuranceBet' to zero */ - public void recordWin() { - this.total = this.total + this.currentBet; + public void recordRound(double totalBet) { + this.total = this.total + totalBet; this.currentBet = 0; - } - - /** - * RecordLoss subtracts 'currentBet' to 'total' and then sets 'currentBet' to zero - */ - public void recordLoss() { - total = total - currentBet; - currentBet = 0; - } - - /** - * Adds 2x the insurance bet to the players total and resets the insurance bet to zero. - */ - public void recordInsuranceWin() { - total = total + (insuranceBet * 2); - insuranceBet = 0; - } - - /** - * Subtracts the insurance bet from the players total and resets the insurance bet to zero. - */ - public void recordInsuranceLoss() { - total = total - insuranceBet; - insuranceBet = 0; + this.splitBet = 0; + this.insuranceBet = 0; } /** diff --git a/10_Blackjack/java/test/GameTest.java b/10_Blackjack/java/test/GameTest.java index 360c897f..d1445b6e 100644 --- a/10_Blackjack/java/test/GameTest.java +++ b/10_Blackjack/java/test/GameTest.java @@ -411,4 +411,77 @@ public class GameTest { // Then assertTrue(out.toString().contains("TYPE H, S OR D, PLEASE")); } + + @Test + @DisplayName("evaluateRound() should total both hands when split") + public void evaluateRoundWithSplitHands(){ + // Given + Player dealer = new Player(0); //Dealer + dealer.dealCard(new Card(1, Card.Suit.HEARTS)); + dealer.dealCard(new Card(1, Card.Suit.SPADES)); + + Player player = new Player(1); + player.recordRound(200);//Set starting total + player.setCurrentBet(50); + player.dealCard(new Card(1, Card.Suit.HEARTS)); + player.dealCard(new Card(1, Card.Suit.SPADES)); + + playerSays("/"); + playerGets(13, Card.Suit.CLUBS); // First hand + playerSays("S"); + playerGets(13, Card.Suit.SPADES); // Second hand + playerSays("S"); + initGame(); + + // When + game.play(player); + game.evaluateRound(Arrays.asList(player), dealer.getHand()); + + // Then + assertTrue(out.toString().contains("PLAYER 1 WINS 100 TOTAL= 300")); + } + + @Test + @DisplayName("evaluateRound() should total add twice insurance bet") + public void evaluateRoundWithInsurance(){ + // Given + Player dealer = new Player(0); //Dealer + dealer.dealCard(new Card(10, Card.Suit.HEARTS)); + dealer.dealCard(new Card(1, Card.Suit.SPADES)); + + Player player = new Player(1); + player.setCurrentBet(50); + player.setInsuranceBet(10); + player.dealCard(new Card(2, Card.Suit.HEARTS)); + player.dealCard(new Card(1, Card.Suit.SPADES)); + initGame(); + + // When + game.evaluateRound(Arrays.asList(player), dealer.getHand()); + + // Then + // Loses current bet (50) and wins 2*10 for total -30 + assertTrue(out.toString().contains("PLAYER 1 LOSES 30 TOTAL= -30")); + } + + @Test + @DisplayName("evaluateRound() should push with no total change") + public void evaluateRoundWithPush(){ + // Given + Player dealer = new Player(0); + dealer.dealCard(new Card(10, Card.Suit.HEARTS)); + dealer.dealCard(new Card(8, Card.Suit.SPADES)); + + Player player = new Player(1); + player.setCurrentBet(10); + player.dealCard(new Card(9, Card.Suit.HEARTS)); + player.dealCard(new Card(9, Card.Suit.SPADES)); + initGame(); + + // When (Dealer and Player both have 19) + game.evaluateRound(Arrays.asList(player), dealer.getHand()); + + // Then + assertTrue(out.toString().contains("PLAYER 1 PUSHES 0 TOTAL= 0")); + } } From 34574d60cb4f6316a96dadd02b9f9e9e5e13f013 Mon Sep 17 00:00:00 2001 From: Mitch Peck Date: Wed, 9 Mar 2022 20:43:56 -0600 Subject: [PATCH 47/51] Handle dealer's total in evaluateRound --- 10_Blackjack/java/src/Game.java | 21 +++++++++------------ 10_Blackjack/java/test/GameTest.java | 23 ++++++++++++++++------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index e0a415b7..7a14e2c2 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -99,7 +99,7 @@ public class Game { dealerHand = playDealer(dealerHand, deck); } - evaluateRound(players, dealer.getHand());//TODO: User dealerHand once playHeader implemented + evaluateRound(players, dealer);//TODO: User dealerHand once playHeader implemented } } @@ -242,22 +242,19 @@ public class Game { * @param players * @param dealerHand */ - protected void evaluateRound(List players, List dealerHand) { - // TODO implement evaluateRound - // print something like: + protected void evaluateRound(List players, Player dealer) { /* PLAYER 1 LOSES 100 TOTAL=-100 PLAYER 2 WINS 150 TOTAL= 150 DEALER'S TOTAL= 200 - // In "WINS X TOTAL= Y" the value of 'X' combines normal and insurance bets. e.g. if you bet 100 and get 5 insurance, - // then win the hand but lose insurance, it will say "WINS 95" */ // this should probably take in a "Dealer" instance instead of just the dealer hand so we can update the dealer's total. // currentBets of each player are added/subtracted from the dealer total depending on whether they win/lose (accounting for doubling down, insurance etc.) // remember to handle a "PUSH" when the dealer ties and the bet is returned. + DecimalFormat formatter = new DecimalFormat("0.#"); //Removes trailing zeros for(Player player : players){ - int result = ScoringUtils.compareHands(player.getHand(), dealerHand); + int result = ScoringUtils.compareHands(player.getHand(), dealer.getHand()); double totalBet = 0; if(result > 0){ totalBet += player.getCurrentBet(); @@ -265,7 +262,7 @@ public class Game { totalBet -= player.getCurrentBet(); } if(player.isSplit()) { - int splitResult = ScoringUtils.compareHands(player.getHand(2), dealerHand); + int splitResult = ScoringUtils.compareHands(player.getHand(2), dealer.getHand()); if(splitResult > 0){ totalBet += player.getSplitBet(); } else if(splitResult < 0){ @@ -273,8 +270,8 @@ public class Game { } } if(player.getInsuranceBet() != 0){ - int dealerResult = ScoringUtils.scoreHand(dealerHand); - if(dealerResult == 21 && dealerHand.size() == 2){ + int dealerResult = ScoringUtils.scoreHand(dealer.getHand()); + if(dealerResult == 21 && dealer.getHand().size() == 2){ totalBet += (player.getInsuranceBet() * 2); } else { totalBet -= player.getInsuranceBet(); @@ -290,11 +287,11 @@ public class Game { userIo.print(" PUSHES"); } player.recordRound(totalBet); - DecimalFormat formatter = new DecimalFormat("0.#"); //Removes trailing zeros + dealer.recordRound(totalBet*-1); userIo.println(String.format("%6s", formatter.format(Math.abs(totalBet))) + " TOTAL= " + formatter.format(player.getTotal())); player.resetHand(); } - + userIo.println("DEALER'S TOTAL= " + formatter.format(dealer.getTotal())); } /** diff --git a/10_Blackjack/java/test/GameTest.java b/10_Blackjack/java/test/GameTest.java index d1445b6e..5eb3553c 100644 --- a/10_Blackjack/java/test/GameTest.java +++ b/10_Blackjack/java/test/GameTest.java @@ -435,10 +435,13 @@ public class GameTest { // When game.play(player); - game.evaluateRound(Arrays.asList(player), dealer.getHand()); + game.evaluateRound(Arrays.asList(player), dealer); // Then - assertTrue(out.toString().contains("PLAYER 1 WINS 100 TOTAL= 300")); + assertAll( + () -> assertTrue(out.toString().contains("PLAYER 1 WINS 100 TOTAL= 300")), + () -> assertTrue(out.toString().contains("DEALER'S TOTAL= -100")) + ); } @Test @@ -457,11 +460,14 @@ public class GameTest { initGame(); // When - game.evaluateRound(Arrays.asList(player), dealer.getHand()); + game.evaluateRound(Arrays.asList(player), dealer); // Then // Loses current bet (50) and wins 2*10 for total -30 - assertTrue(out.toString().contains("PLAYER 1 LOSES 30 TOTAL= -30")); + assertAll( + () -> assertTrue(out.toString().contains("PLAYER 1 LOSES 30 TOTAL= -30")), + () -> assertTrue(out.toString().contains("DEALER'S TOTAL= 30")) + ); } @Test @@ -479,9 +485,12 @@ public class GameTest { initGame(); // When (Dealer and Player both have 19) - game.evaluateRound(Arrays.asList(player), dealer.getHand()); + game.evaluateRound(Arrays.asList(player), dealer); - // Then - assertTrue(out.toString().contains("PLAYER 1 PUSHES 0 TOTAL= 0")); + // Then + assertAll( + () -> assertTrue(out.toString().contains("PLAYER 1 PUSHES 0 TOTAL= 0")), + () -> assertTrue(out.toString().contains("DEALER'S TOTAL= 0")) + ); } } From 14123c9a4fa9b167319403c55c85def2ab47f2c7 Mon Sep 17 00:00:00 2001 From: Mitch Peck Date: Fri, 11 Mar 2022 21:12:59 -0600 Subject: [PATCH 48/51] shouldPlayDealer & playDealer implementations, formatting to mimic original code --- 10_Blackjack/java/src/Game.java | 83 ++++++++--- 10_Blackjack/java/src/ScoringUtils.java | 43 +++--- 10_Blackjack/java/test/GameTest.java | 140 ++++++++++++++++++- 10_Blackjack/java/test/ScoringUtilsTest.java | 50 +++++++ 4 files changed, 275 insertions(+), 41 deletions(-) diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index 7a14e2c2..009b0335 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -94,12 +94,14 @@ public class Game { play(player); } - // TODO only play the dealer if at least one player has not busted or gotten a natural blackjack (21 in the first two cards) - // otherwise, just print the dealer's concealed card - dealerHand = playDealer(dealerHand, deck); + if(shouldPlayDealer(players)){ + playDealer(dealer); + } else { + userIo.println("DEALER HAD " + dealer.getHand().get(1).toProseString() + " CONCEALED."); + } } - evaluateRound(players, dealer);//TODO: User dealerHand once playHeader implemented + evaluateRound(players, dealer); } } @@ -165,7 +167,7 @@ public class Game { private void play(Player player, int handNumber) { String action; if(player.isSplit()){ - action = userIo.prompt("HAND #" + handNumber); + action = userIo.prompt("HAND " + handNumber); } else { action = userIo.prompt("PLAYER " + player.getPlayerNumber() + " "); } @@ -200,11 +202,13 @@ public class Game { Card card = deck.deal(); player.dealCard(card, 1); userIo.println("FIRST HAND RECEIVES " + card.toProseString()); - play(player, 1); card = deck.deal(); player.dealCard(card, 2); - userIo.println("SECOND HAND RECEIVES " + card.toProseString()); - play(player, 2); + userIo.println("SECOND HAND RECEIVES " + card.toProseString()); + if(player.getHand().get(0).getValue() > 1){ //Can't play after splitting aces + play(player, 1); + play(player, 2); + } return; // Don't fall out of the while loop and print another total } else { userIo.println("SPLITTING NOT ALLOWED"); @@ -218,9 +222,38 @@ public class Game { } } } - userIo.println("TOTAL IS " + ScoringUtils.scoreHand(player.getHand(handNumber))); + int total = ScoringUtils.scoreHand(player.getHand(handNumber)); + if(total == 21) { + userIo.println("BLACKJACK"); + } else { + userIo.println("TOTAL IS " + total); + } } + /** + * Check the Dealer's hand should be played out. If every player has either busted or won with natural Blackjack, + * the Dealer doesn't need to play. + * + * @param players + * @return boolean whether the dealer should play + */ + + protected boolean shouldPlayDealer(List players){ + for(Player player : players){ + int score = ScoringUtils.scoreHand(player.getHand()); + if(score < 21 || (score == 21 && player.getHand().size() > 2)){ + return true; + } + if(player.isSplit()){ + int splitScore = ScoringUtils.scoreHand(player.getHand(2)); + if(splitScore < 21 || (splitScore == 21 && player.getHand(2).size() > 2)){ + return true; + } + } + } + return false; + } + /** * Play the dealer's hand. The dealer draws until they have >=17 or busts. Prints each draw as in the following example: * @@ -232,9 +265,25 @@ public class Game { * @param dealerHand * @return */ - private LinkedList playDealer(LinkedList dealerHand, Deck deck) { - // TODO implement playDealer - return null; + protected void playDealer(Player dealer) { + int score = ScoringUtils.scoreHand(dealer.getHand()); + userIo.println("DEALER HAS " + dealer.getHand().get(1).toProseString() + " CONCEALED FOR A TOTAL OF " + score); + + if(score < 17){ + userIo.print("DRAWS "); + } + while(score < 17) { + Card dealtCard = deck.deal(); + dealer.dealCard(dealtCard); + score = ScoringUtils.scoreHand(dealer.getHand()); + userIo.print(dealtCard.toString() + " "); + } + + if(score > 21) { + userIo.println("...BUSTED\n"); + } else { + userIo.println("---TOTAL IS " + score + "\n"); + } } /** @@ -280,18 +329,18 @@ public class Game { userIo.print("PLAYER " + player.getPlayerNumber()); if(totalBet < 0) { - userIo.print(" LOSES "); + userIo.print(" LOSES " + String.format("%6s", formatter.format(Math.abs(totalBet)))); } else if(totalBet > 0) { - userIo.print(" WINS "); + userIo.print(" WINS " + String.format("%6s", formatter.format(totalBet))); } else { - userIo.print(" PUSHES"); + userIo.print(" PUSHES "); } player.recordRound(totalBet); dealer.recordRound(totalBet*-1); - userIo.println(String.format("%6s", formatter.format(Math.abs(totalBet))) + " TOTAL= " + formatter.format(player.getTotal())); + userIo.println(" TOTAL= " + formatter.format(player.getTotal())); player.resetHand(); } - userIo.println("DEALER'S TOTAL= " + formatter.format(dealer.getTotal())); + userIo.println("DEALER'S TOTAL= " + formatter.format(dealer.getTotal()) + "\n"); } /** diff --git a/10_Blackjack/java/src/ScoringUtils.java b/10_Blackjack/java/src/ScoringUtils.java index df149e27..b7401102 100644 --- a/10_Blackjack/java/src/ScoringUtils.java +++ b/10_Blackjack/java/src/ScoringUtils.java @@ -1,7 +1,7 @@ import java.util.List; public final class ScoringUtils { - + /** * Calculates the value of a hand. When the hand contains aces, it will * count one of them as 11 if that does not result in a bust. @@ -9,15 +9,16 @@ public final class ScoringUtils { * @param hand the hand to evaluate * @return The numeric value of a hand. A value over 21 indicates a bust. */ - public static final int scoreHand(List hand){ + public static final int scoreHand(List 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(); + .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) { + 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. } @@ -25,27 +26,35 @@ public final class ScoringUtils { } /** - * Compares two hands accounting for natural blackjacks using the + * Compares two hands accounting for natural blackjacks and busting using the * java.lang.Comparable convention of returning positive or negative integers * * @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. + * @return a negative integer, zero, or a positive integer as handA is less + * than, equal to, or greater than handB. */ public static final int compareHands(List handA, List handB) { int scoreA = scoreHand(handA); int scoreB = scoreHand(handB); - - if(scoreA == 21 && scoreB == 21){ - if(handA.size() == 2 && handB.size() != 2){ - return 1; //Hand A wins with a natural blackjack - } else if (handA.size() != 2 && handB.size() == 2) { - return -1; //Hand B wins with a natural blackjack + if (scoreA == 21 && scoreB == 21) { + if (handA.size() == 2 && handB.size() != 2) { + return 1; // Hand A wins with a natural blackjack + } else if (handA.size() != 2 && handB.size() == 2) { + return -1; // Hand B wins with a natural blackjack } else { - return 0; //Tie + return 0; // Tie + } + } else if (scoreA > 21 || scoreB > 21) { + if (scoreA > 21 && scoreB > 21) { + return 0; // Tie, both bust + } else if (scoreB > 21) { + return 1; // A wins, B busted + } else { + return -1; // B wins, A busted } } else { - return Integer.compare(scoreA, scoreB); + return Integer.compare(scoreA, scoreB); } } diff --git a/10_Blackjack/java/test/GameTest.java b/10_Blackjack/java/test/GameTest.java index 5eb3553c..ec56874e 100644 --- a/10_Blackjack/java/test/GameTest.java +++ b/10_Blackjack/java/test/GameTest.java @@ -370,8 +370,8 @@ public class GameTest { // 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)); + player.dealCard(new Card(2, Card.Suit.HEARTS)); + player.dealCard(new Card(2, Card.Suit.SPADES)); playerSays("/"); playerGets(13, Card.Suit.CLUBS); // First hand @@ -394,8 +394,8 @@ public class GameTest { // 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)); + player.dealCard(new Card(10, Card.Suit.HEARTS)); + player.dealCard(new Card(10, Card.Suit.SPADES)); playerSays("/"); playerGets(13, Card.Suit.CLUBS); // First hand @@ -425,7 +425,7 @@ public class GameTest { player.setCurrentBet(50); player.dealCard(new Card(1, Card.Suit.HEARTS)); player.dealCard(new Card(1, Card.Suit.SPADES)); - + playerSays("/"); playerGets(13, Card.Suit.CLUBS); // First hand playerSays("S"); @@ -439,7 +439,7 @@ public class GameTest { // Then assertAll( - () -> assertTrue(out.toString().contains("PLAYER 1 WINS 100 TOTAL= 300")), + () -> assertTrue(out.toString().contains("PLAYER 1 WINS 100 TOTAL= 300")), () -> assertTrue(out.toString().contains("DEALER'S TOTAL= -100")) ); } @@ -489,8 +489,134 @@ public class GameTest { // Then assertAll( - () -> assertTrue(out.toString().contains("PLAYER 1 PUSHES 0 TOTAL= 0")), + () -> assertTrue(out.toString().contains("PLAYER 1 PUSHES TOTAL= 0")), () -> assertTrue(out.toString().contains("DEALER'S TOTAL= 0")) ); } + + @Test + @DisplayName("shouldPlayDealer() return false when players bust") + public void shouldPlayDealerBust(){ + // Given + Player player = new Player(1); + player.dealCard(new Card(10, Card.Suit.SPADES)); + player.dealCard(new Card(10, Card.Suit.SPADES)); + player.split(); + player.dealCard(new Card(5, Card.Suit.SPADES)); + player.dealCard(new Card(8, Card.Suit.SPADES));//First hand Busted + + player.dealCard(new Card(5, Card.Suit.SPADES),2); + player.dealCard(new Card(8, Card.Suit.SPADES),2);//Second hand Busted + + Player playerTwo = new Player(2); + playerTwo.dealCard(new Card(7, Card.Suit.HEARTS)); + playerTwo.dealCard(new Card(8, Card.Suit.HEARTS)); + playerTwo.dealCard(new Card(9, Card.Suit.HEARTS)); + initGame(); + + // When + boolean result = game.shouldPlayDealer(Arrays.asList(player,playerTwo)); + + // Then + assertFalse(result); + } + + @Test + @DisplayName("shouldPlayDealer() return false when players bust") + public void ShouldPlayer(){ + // Given + Player player = new Player(1); + player.dealCard(new Card(10, Card.Suit.SPADES)); + player.dealCard(new Card(10, Card.Suit.SPADES)); + player.split(); + player.dealCard(new Card(5, Card.Suit.SPADES)); + player.dealCard(new Card(8, Card.Suit.SPADES));//First hand Busted + + player.dealCard(new Card(5, Card.Suit.SPADES),2); + player.dealCard(new Card(8, Card.Suit.SPADES),2);//Second hand Busted + + Player playerTwo = new Player(2); + playerTwo.dealCard(new Card(7, Card.Suit.HEARTS)); + playerTwo.dealCard(new Card(8, Card.Suit.HEARTS)); + playerTwo.dealCard(new Card(9, Card.Suit.HEARTS)); + initGame(); + + // When + boolean result = game.shouldPlayDealer(Arrays.asList(player,playerTwo)); + + // Then + assertFalse(result); + } + + @Test + @DisplayName("shouldPlayDealer() return true when player has non-natural blackjack") + public void shouldPlayDealerNonNaturalBlackjack(){ + // Given + Player player = new Player(1); + player.dealCard(new Card(5, Card.Suit.SPADES)); + player.dealCard(new Card(6, Card.Suit.DIAMONDS)); + player.dealCard(new Card(10, Card.Suit.SPADES)); + + initGame(); + + // When + boolean result = game.shouldPlayDealer(Arrays.asList(player)); + + // Then + assertTrue(result); + } + + @Test + @DisplayName("shouldPlayDealer() return true when player doesn't have blackjack") + public void shouldPlayDealerNonBlackjack(){ + // Given + Player player = new Player(1); + player.dealCard(new Card(10, Card.Suit.SPADES)); + player.dealCard(new Card(6, Card.Suit.DIAMONDS)); + initGame(); + + // When + boolean result = game.shouldPlayDealer(Arrays.asList(player)); + + // Then + assertTrue(result); + } + + + @Test + @DisplayName("playDealer() should DRAW on less than 17 intial deal") + public void playDealerLessThanSeventeen(){ + // Given + Player dealer = new Player(0); + dealer.dealCard(new Card(10, Card.Suit.SPADES)); + dealer.dealCard(new Card(6, Card.Suit.DIAMONDS)); + playerGets(11, Card.Suit.DIAMONDS); + initGame(); + + // When + game.playDealer(dealer); + + // Then + assertTrue(out.toString().contains("DRAWS")); + assertTrue(out.toString().contains("BUSTED")); + } + + @Test + @DisplayName("playDealer() should stay on more than 17 intial deal") + public void playDealerMoreThanSeventeen(){ + // Given + Player dealer = new Player(0); + dealer.dealCard(new Card(10, Card.Suit.SPADES)); + dealer.dealCard(new Card(8, Card.Suit.DIAMONDS)); + initGame(); + + // When + game.playDealer(dealer); + + // Then + assertFalse(out.toString().contains("DRAWS")); + assertFalse(out.toString().contains("BUSTED")); + assertTrue(out.toString().contains("---TOTAL IS")); + } + } diff --git a/10_Blackjack/java/test/ScoringUtilsTest.java b/10_Blackjack/java/test/ScoringUtilsTest.java index a6c3877f..01a1738c 100644 --- a/10_Blackjack/java/test/ScoringUtilsTest.java +++ b/10_Blackjack/java/test/ScoringUtilsTest.java @@ -140,4 +140,54 @@ public class ScoringUtilsTest { assertEquals(0, result); } + @Test + @DisplayName("compareHands should return 0, hand A and B tie when both bust") + public void compareHandsTieBust() { + LinkedList handA = new LinkedList<>(); + handA.add(new Card(10, Card.Suit.DIAMONDS)); + handA.add(new Card(10, Card.Suit.HEARTS)); + handA.add(new Card(3, Card.Suit.HEARTS)); + + LinkedList handB = new LinkedList<>(); + handB.add(new Card(10, Card.Suit.SPADES)); + handB.add(new Card(11, Card.Suit.SPADES)); + handB.add(new Card(4, Card.Suit.SPADES)); + + int result = ScoringUtils.compareHands(handA,handB); + + assertEquals(0, result); + } + @Test + @DisplayName("compareHands should return -1, meaning B beat A, A busted") + public void compareHandsABusted() { + LinkedList handA = new LinkedList<>(); + handA.add(new Card(10, Card.Suit.DIAMONDS)); + handA.add(new Card(10, Card.Suit.HEARTS)); + handA.add(new Card(3, Card.Suit.HEARTS)); + + LinkedList handB = new LinkedList<>(); + handB.add(new Card(10, Card.Suit.SPADES)); + handB.add(new Card(10, Card.Suit.SPADES)); + + int result = ScoringUtils.compareHands(handA,handB); + + assertEquals(-1, result); + } + + @Test + @DisplayName("compareHands should return 1, meaning A beat B, B busted") + public void compareHandsBBusted() { + LinkedList handA = new LinkedList<>(); + handA.add(new Card(10, Card.Suit.DIAMONDS)); + handA.add(new Card(3, Card.Suit.HEARTS)); + + LinkedList handB = new LinkedList<>(); + handB.add(new Card(10, Card.Suit.SPADES)); + handB.add(new Card(10, Card.Suit.SPADES)); + handB.add(new Card(5, Card.Suit.SPADES)); + + int result = ScoringUtils.compareHands(handA,handB); + + assertEquals(1, result); + } } From 88202ec9bee5f54ce2c0338332fd4c31e4ab759d Mon Sep 17 00:00:00 2001 From: Mitch Peck Date: Wed, 16 Mar 2022 21:27:37 -0500 Subject: [PATCH 49/51] Fix dealer initialization --- 10_Blackjack/java/src/Game.java | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index 009b0335..d88b600e 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -45,6 +45,9 @@ public class Game { deck.reshuffle(); + Player dealer = new Player(0); //Dealer is Player 0 - this can be converted into a dealer class later on + + List players = new ArrayList<>(); for(int i = 0; i < nPlayers; i++) { players.add(new Player(i + 1)); @@ -66,15 +69,9 @@ public class Game { for(Player player : players){ player.dealCard(deck.deal()); } + dealer.dealCard(deck.deal()); } - // Consider adding a Dealer class to track the dealer's hand and running total. - // Alternately, the dealer could just be a Player instance where currentBet=0 and is ignored. - LinkedList dealerHand = new LinkedList<>(); - Player dealer = new Player(0); //Dealer is Player 0 - this can be converted into a dealer class later on - dealer.dealCard(deck.deal()); - dealer.dealCard(deck.deal()); - printInitialDeal(players, dealer); if(dealer.getHand().get(0).getValue() == 1) { @@ -305,7 +302,7 @@ public class Game { for(Player player : players){ int result = ScoringUtils.compareHands(player.getHand(), dealer.getHand()); double totalBet = 0; - if(result > 0){ + if(result > 0) { totalBet += player.getCurrentBet(); } else if(result < 0){ totalBet -= player.getCurrentBet(); @@ -336,11 +333,12 @@ public class Game { userIo.print(" PUSHES "); } player.recordRound(totalBet); - dealer.recordRound(totalBet*-1); + dealer.recordRound(totalBet * (-1)); userIo.println(" TOTAL= " + formatter.format(player.getTotal())); player.resetHand(); } - userIo.println("DEALER'S TOTAL= " + formatter.format(dealer.getTotal()) + "\n"); + userIo.println("DEALER'S TOTAL= " + formatter.format(dealer.getTotal()) + "\n"); + dealer.resetHand(); } /** From 0b1f8097077b0cfab5b0e0d70b815fdef8ecae56 Mon Sep 17 00:00:00 2001 From: Mitch Peck Date: Fri, 18 Mar 2022 12:52:14 -0500 Subject: [PATCH 50/51] Clean up comments and convert Card to a record --- 10_Blackjack/java/src/Card.java | 18 +++--------- 10_Blackjack/java/src/Game.java | 44 ++++++++++------------------ 10_Blackjack/java/test/GameTest.java | 2 +- 3 files changed, 21 insertions(+), 43 deletions(-) diff --git a/10_Blackjack/java/src/Card.java b/10_Blackjack/java/src/Card.java index b72448dc..1ef00d65 100644 --- a/10_Blackjack/java/src/Card.java +++ b/10_Blackjack/java/src/Card.java @@ -1,37 +1,27 @@ /** - * This is an example of an "immutable" class in Java. That's just a fancy way + * This is an example of an "record" class in Java. That's just a fancy way * of saying the properties (value and suit) can't change after the object has - * been created (it has no 'setter' methods and the properties are 'final'). + * been created (it has no 'setter' methods and the properties are implicitly 'final'). * * Immutability often makes it easier to reason about code logic and avoid * certain classes of bugs. * * Since it would never make sense for a card to change in the middle of a game, * this is a good candidate for immutability. - * */ -// TODO consider making this a Record -public final class Card { +record Card(int value, Suit suit) { public enum Suit { HEARTS, DIAMONDS, SPADES, CLUBS; } - // Since this class is immutable, there's no reason these couldn't be - // 'public', but the pattern of using 'getters' is more consistent with - // typical Java coding patterns. - private final int value; - private final Suit suit; - - public Card(int value, Suit suit) { + public Card { if(value < 1 || value > 13) { throw new IllegalArgumentException("Invalid card value " + value); } if(suit == null) { throw new IllegalArgumentException("Card suit must be non-null"); } - this.value = value; - this.suit = suit; } public int getValue() { diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index d88b600e..0c92c687 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -1,6 +1,5 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedList; import java.util.List; import java.text.DecimalFormat; @@ -45,8 +44,7 @@ public class Game { deck.reshuffle(); - Player dealer = new Player(0); //Dealer is Player 0 - this can be converted into a dealer class later on - + Player dealer = new Player(0); //Dealer is Player 0 List players = new ArrayList<>(); for(int i = 0; i < nPlayers; i++) { @@ -121,15 +119,14 @@ public class Game { /** * Print the cards for each player and the up card for the dealer. + * Prints the initial deal in the following format: + * + * PLAYER 1 2 DEALER + * 7 10 4 + * 2 A */ private void printInitialDeal(List players, Player dealer) { - // Prints the initial deal in the following format: - /* - PLAYER 1 2 DEALER - 7 10 4 - 2 A - */ - + StringBuilder output = new StringBuilder(); output.append("PLAYERS "); for (Player player : players) { @@ -234,7 +231,6 @@ public class Game { * @param players * @return boolean whether the dealer should play */ - protected boolean shouldPlayDealer(List players){ for(Player player : players){ int score = ScoringUtils.scoreHand(player.getHand()); @@ -256,24 +252,21 @@ public class Game { * * DEALER HAS A 5 CONCEALED FOR A TOTAL OF 11 * DRAWS 10 ---TOTAL IS 21 - * - * TODO find out if the dealer draws on a "soft" 17 (17 using an ace as 11) or not in the original basic code. - * + * * @param dealerHand - * @return */ protected void playDealer(Player dealer) { int score = ScoringUtils.scoreHand(dealer.getHand()); userIo.println("DEALER HAS " + dealer.getHand().get(1).toProseString() + " CONCEALED FOR A TOTAL OF " + score); if(score < 17){ - userIo.print("DRAWS "); + userIo.print("DRAWS"); } while(score < 17) { Card dealtCard = deck.deal(); dealer.dealCard(dealtCard); score = ScoringUtils.scoreHand(dealer.getHand()); - userIo.print(dealtCard.toString() + " "); + userIo.print(" " + String.format("%-4s", dealtCard.toString())); } if(score > 21) { @@ -285,19 +278,15 @@ public class Game { /** * Evaluates the result of the round, prints the results, and updates player/dealer totals. + * + * PLAYER 1 LOSES 100 TOTAL=-100 + * PLAYER 2 WINS 150 TOTAL= 150 + * DEALER'S TOTAL= 200 + * * @param players * @param dealerHand */ protected void evaluateRound(List players, Player dealer) { - /* - PLAYER 1 LOSES 100 TOTAL=-100 - PLAYER 2 WINS 150 TOTAL= 150 - DEALER'S TOTAL= 200 - */ - // this should probably take in a "Dealer" instance instead of just the dealer hand so we can update the dealer's total. - // currentBets of each player are added/subtracted from the dealer total depending on whether they win/lose (accounting for doubling down, insurance etc.) - // remember to handle a "PUSH" when the dealer ties and the bet is returned. - DecimalFormat formatter = new DecimalFormat("0.#"); //Removes trailing zeros for(Player player : players){ int result = ScoringUtils.compareHands(player.getHand(), dealer.getHand()); @@ -328,7 +317,7 @@ public class Game { if(totalBet < 0) { userIo.print(" LOSES " + String.format("%6s", formatter.format(Math.abs(totalBet)))); } else if(totalBet > 0) { - userIo.print(" WINS " + String.format("%6s", formatter.format(totalBet))); + userIo.print(" WINS " + String.format("%6s", formatter.format(totalBet))); } else { userIo.print(" PUSHES "); } @@ -352,5 +341,4 @@ public class Game { .map(Player::getCurrentBet) .allMatch(bet -> bet > 0 && bet <= 500); } - } diff --git a/10_Blackjack/java/test/GameTest.java b/10_Blackjack/java/test/GameTest.java index ec56874e..e79ad635 100644 --- a/10_Blackjack/java/test/GameTest.java +++ b/10_Blackjack/java/test/GameTest.java @@ -439,7 +439,7 @@ public class GameTest { // Then assertAll( - () -> assertTrue(out.toString().contains("PLAYER 1 WINS 100 TOTAL= 300")), + () -> assertTrue(out.toString().contains("PLAYER 1 WINS 100 TOTAL= 300")), () -> assertTrue(out.toString().contains("DEALER'S TOTAL= -100")) ); } From cdf194f77009cddeed36c2667af30b710ade28bc Mon Sep 17 00:00:00 2001 From: Dave Burke Date: Fri, 18 Mar 2022 17:01:06 -0500 Subject: [PATCH 51/51] Remove redundant Card record code Java Records automatically acquire an implementation of equals and hashCode that accounts for their components. They also have read accessors for their components (card.suit() to get the suit). --- 10_Blackjack/java/src/Card.java | 32 ------------------------- 10_Blackjack/java/src/Game.java | 6 ++--- 10_Blackjack/java/src/Player.java | 2 +- 10_Blackjack/java/src/ScoringUtils.java | 4 ++-- 10_Blackjack/java/test/DeckTest.java | 4 ++-- 5 files changed, 8 insertions(+), 40 deletions(-) diff --git a/10_Blackjack/java/src/Card.java b/10_Blackjack/java/src/Card.java index 1ef00d65..90daafaf 100644 --- a/10_Blackjack/java/src/Card.java +++ b/10_Blackjack/java/src/Card.java @@ -24,14 +24,6 @@ record Card(int value, Suit suit) { } } - public int getValue() { - return this.value; - } - - public Suit getSuit() { - return this.suit; - } - public String toString() { StringBuilder result = new StringBuilder(2); if(value == 1) { @@ -64,28 +56,4 @@ record Card(int value, Suit suit) { } } - @Override - public boolean equals(Object obj) { - // Overriding 'equals' and 'hashCode' (below) make your class work correctly - // with all sorts of methods in the Java API that need to determine the uniqueness - // of an instance (like a Set). - if(obj.getClass() != Card.class) { - return false; - } - Card other = (Card) obj; - return this.getSuit() == other.getSuit() && this.getValue() == other.getValue(); - } - - @Override - public int hashCode() { - // This is a fairly standard hashCode implementation for a data object. - // The details are beyond the scope of this comment, but most IDEs can generate - // this for you. - - // Note that it's a best practice to implement hashCode whenever you implement equals and vice versa. - int hash = 7; - hash = 31 * hash + (int) value; - hash = 31 * hash + suit.hashCode(); - return hash; - } } \ No newline at end of file diff --git a/10_Blackjack/java/src/Game.java b/10_Blackjack/java/src/Game.java index 0c92c687..09afd5f8 100644 --- a/10_Blackjack/java/src/Game.java +++ b/10_Blackjack/java/src/Game.java @@ -72,7 +72,7 @@ public class Game { printInitialDeal(players, dealer); - if(dealer.getHand().get(0).getValue() == 1) { + if(dealer.getHand().get(0).value() == 1) { collectInsurance(players); } @@ -81,7 +81,7 @@ public class Game { userIo.println("FOR BLACKJACK"); } else { Card dealerFirstCard = dealer.getHand().get(0); - if(dealerFirstCard.getValue() == 1 || dealerFirstCard.getValue() > 9) { + if(dealerFirstCard.value() == 1 || dealerFirstCard.value() > 9) { userIo.println(""); userIo.println("NO DEALER BLACKJACK."); } // else dealer blackjack is imposible @@ -199,7 +199,7 @@ public class Game { card = deck.deal(); player.dealCard(card, 2); userIo.println("SECOND HAND RECEIVES " + card.toProseString()); - if(player.getHand().get(0).getValue() > 1){ //Can't play after splitting aces + if(player.getHand().get(0).value() > 1){ //Can't play after splitting aces play(player, 1); play(player, 2); } diff --git a/10_Blackjack/java/src/Player.java b/10_Blackjack/java/src/Player.java index 731cb37f..85b8b1dd 100644 --- a/10_Blackjack/java/src/Player.java +++ b/10_Blackjack/java/src/Player.java @@ -105,7 +105,7 @@ public class Player { // Can't split twice return false; } else { - boolean isPair = this.hand.get(0).getValue() == this.hand.get(1).getValue(); + boolean isPair = this.hand.get(0).value() == this.hand.get(1).value(); return isPair; } } diff --git a/10_Blackjack/java/src/ScoringUtils.java b/10_Blackjack/java/src/ScoringUtils.java index b7401102..573abb62 100644 --- a/10_Blackjack/java/src/ScoringUtils.java +++ b/10_Blackjack/java/src/ScoringUtils.java @@ -10,9 +10,9 @@ public final class ScoringUtils { * @return The numeric value of a hand. A value over 21 indicates a bust. */ public static final int scoreHand(List hand) { - int nAces = (int) hand.stream().filter(c -> c.getValue() == 1).count(); + int nAces = (int) hand.stream().filter(c -> c.value() == 1).count(); int value = hand.stream() - .mapToInt(Card::getValue) + .mapToInt(Card::value) .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' diff --git a/10_Blackjack/java/test/DeckTest.java b/10_Blackjack/java/test/DeckTest.java index b8d42ae5..2da7cdc6 100644 --- a/10_Blackjack/java/test/DeckTest.java +++ b/10_Blackjack/java/test/DeckTest.java @@ -13,11 +13,11 @@ public class DeckTest { // Then long nCards = deck.size(); long nSuits = deck.getCards().stream() - .map(card -> card.getSuit()) + .map(card -> card.suit()) .distinct() .count(); long nValues = deck.getCards().stream() - .map(card -> card.getValue()) + .map(card -> card.value()) .distinct() .count();