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();