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. */ public class Game { private Deck deck; private UserIo userIo; public Game(Deck deck, UserIo userIo) { this.deck = deck; 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); 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(); 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)); } while(true) { while(!betsAreValid(players)){ userIo.println("BETS:"); for(int i = 0; i < nPlayers; i++) { double bet = userIo.promptDouble("#" + (i + 1)); // 1st player is "Player 1" not "Player 0" players.get(i).setCurrentBet(bet); } } // 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()); } dealer.dealCard(deck.deal()); } printInitialDeal(players, dealer); if(dealer.getHand().get(0).getValue() == 1) { collectInsurance(players); } if(ScoringUtils.scoreHand(dealer.getHand()) == 21) { userIo.println("DEALER HAS " + dealer.getHand().get(1).toProseString() + " 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); } if(shouldPlayDealer(players)){ playDealer(dealer); } else { userIo.println("DEALER HAD " + dealer.getHand().get(1).toProseString() + " CONCEALED."); } } evaluateRound(players, dealer); } } 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. */ 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"); } userIo.print(output.toString()); } /** * 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 */ protected void play(Player player) { play(player, 1); } private void play(Player player, int 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, handNumber); if(ScoringUtils.scoreHand(player.getHand(handNumber)) > 21){ userIo.println("RECEIVED " + c.toProseString() + " ...BUSTED"); break; } action = userIo.prompt("RECEIVED " + c.toProseString() + " HIT"); } else if(action.equalsIgnoreCase("S")){ // STAY break; } else if(action.equalsIgnoreCase("D") && player.canDoubleDown(handNumber)) { // DOUBLE DOWN Card c = deck.deal(); player.doubleDown(c, handNumber); if(ScoringUtils.scoreHand(player.getHand(handNumber)) > 21){ userIo.println("RECEIVED " + c.toProseString() + " ...BUSTED"); break; } userIo.println("RECEIVED " + c.toProseString()); break; } 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); userIo.println("FIRST HAND RECEIVES " + card.toProseString()); 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 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"); action = userIo.prompt("PLAYER " + player.getPlayerNumber() + " "); } } 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"); } } } 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: * * 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 "); } 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"); } } /** * Evaluates the result of the round, prints the results, and updates player/dealer totals. * @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()); 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), dealer.getHand()); if(splitResult > 0){ totalBet += player.getSplitBet(); } else if(splitResult < 0){ totalBet -= player.getSplitBet(); } } if(player.getInsuranceBet() != 0){ int dealerResult = ScoringUtils.scoreHand(dealer.getHand()); if(dealerResult == 21 && dealer.getHand().size() == 2){ totalBet += (player.getInsuranceBet() * 2); } else { totalBet -= player.getInsuranceBet(); } } userIo.print("PLAYER " + player.getPlayerNumber()); 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))); } else { userIo.print(" PUSHES "); } player.recordRound(totalBet); dealer.recordRound(totalBet * (-1)); userIo.println(" TOTAL= " + formatter.format(player.getTotal())); player.resetHand(); } userIo.println("DEALER'S TOTAL= " + formatter.format(dealer.getTotal()) + "\n"); dealer.resetHand(); } /** * Validates that all bets are between 0 (exclusive) and 500 (inclusive). Fractional bets are valid. * * @param players The players with their current bet set. * @return true if all bets are valid, false otherwise. */ public boolean betsAreValid(Collection players) { return players.stream() .map(Player::getCurrentBet) .allMatch(bet -> bet > 0 && bet <= 500); } }