Game.java

package com.example;

import java.io.*;
import java.util.ArrayList;

public class Game {
  public static final int MAX_PLAYER_HANDS = 7;
  private static final String SAVE_FILE = "blackjack.txt";
  private static final int MIN_BET = 500;
  private final BufferedReader reader;
  private final Shoe shoe;
  private final ArrayList<PlayerHand> playerHands;
  private int numDecks;
  private int deckType;
  private int faceType;
  private int money;
  private int currentBet;
  private DealerHand dealerHand;
  private int currentHand;
  private boolean quitting;

  public Game() {
    this.reader = new BufferedReader(new InputStreamReader(System.in));
    this.shoe = new Shoe(this);
    this.numDecks = 1;
    this.deckType = 1;
    this.faceType = 1;
    this.money = 10000;
    this.currentBet = 500;
    this.playerHands = new ArrayList<>();
    this.quitting = false;
    loadGame();
  }

  public static void run() {
    (new Game()).loop();
  }

  protected BufferedReader getReader() {
    return reader;
  }

  public int getNumDecks() {
    return numDecks;
  }

  public int getFaceType() {
    return faceType;
  }

  public String cardFace(int value, int suit) {
    return (getFaceType() == 2 ? Card.FACES2 : Card.FACES)[value][suit];
  }

  public int getCurrentHand() {
    return currentHand;
  }

  public boolean moreHandsToPlay() {
    return currentHand < playerHands.size() - 1;
  }

  public void playMoreHands() {
    currentHand++;

    PlayerHand playerHand = playerHands.get(currentHand);
    playerHand.dealCard();

    if (playerHand.isDone()) {
      playerHand.process();
      return;
    }

    drawHands();
    playerHand.getAction();
  }

  public ArrayList<PlayerHand> getPlayerHands() {
    return playerHands;
  }

  public void splitCurrentHand() {
    int handCount = getPlayerHands().size();
    PlayerHand newHand = new PlayerHand(this);
    getPlayerHands().add(newHand);

    while (handCount > currentHand) {
      PlayerHand playerHand = getPlayerHands().get(handCount - 1).clone();
      getPlayerHands().set(handCount, playerHand);
      handCount--;
    }

    PlayerHand currentPlayerHand = getPlayerHands().get(currentHand);
    PlayerHand splitHand = getPlayerHands().get(currentHand + 1);

    Card splitCard1 = currentPlayerHand.cards.get(1).clone();
    Card splitCard0 = currentPlayerHand.cards.get(0).clone();

    splitHand.cards = new ArrayList<>();
    splitHand.cards.add(splitCard1);
    currentPlayerHand.cards = new ArrayList<>();
    currentPlayerHand.cards.add(splitCard0);
    currentPlayerHand.dealCard();

    if (currentPlayerHand.isDone()) {
      currentPlayerHand.process();
      return;
    }

    drawHands();
    currentPlayerHand.getAction();
  }

  public int allBets() {
    return playerHands.stream()
        .mapToInt(PlayerHand::getBet)
        .sum();
  }

  public int getMoney() {
    return money;
  }

  public void setMoney(int money) {
    this.money = money;
  }

  public int getCurrentBet() {
    return currentBet;
  }

  public Shoe getShoe() {
    return shoe;
  }

  private void normalizeBet() {
    if (this.currentBet > money) {
      this.currentBet = money;
    }
  }

  public void getNewBet() {
    drawHands();
    System.out.print(" (1) $5  (2) $10  (3) $25  (4) $100");

    switch (getChar()) {
      case '1':
        currentBet = 500;
        break;
      case '2':
        currentBet = 1000;
        break;
      case '3':
        currentBet = 2500;
        break;
      case '4':
        currentBet = 10000;
        break;
      default:
        getNewBet();
        return;
    }

    normalizeBet();
    dealNewHand();
  }

  public void getNewNumDecks() {
    drawHands();
    System.out.printf(" Number of Decks: %d  Enter New Number of Decks (1-8): ", numDecks);

    int newNumDecks = getChar() - '0';

    if (newNumDecks < 1) {
      newNumDecks = 1;
    } else if (newNumDecks > 8) {
      newNumDecks = 8;
    }

    this.numDecks = newNumDecks;
    gameOptions();
  }

  public void getNewDeckType() {
    drawHands();
    System.out.println(" (1) Regular  (2) Aces  (3) Jacks  (4) Aces & Jacks  (5) Sevens  (6) Eights");

    int newDeckType = getChar() - '0';

    if (newDeckType > 0 && newDeckType < 7) {
      deckType = newDeckType;

      if (newDeckType > 1) {
        this.numDecks = 8;
      }

      shoe.buildNewShoe(deckType);

      saveGame();
      return;
    }

    getNewDeckType();
  }

  public void getNewFaceType() {
    drawHands();
    System.out.println(" (1) A♠  (2) 🂡");

    int newFaceType = getChar() - '0';

    if (newFaceType == 1 || newFaceType == 2) {
      faceType = newFaceType;
      saveGame();
      return;
    }

    drawHands();
    getNewFaceType();
  }

  public void gameOptions() {
    drawHands();
    System.out.println(" (N) Number of Decks  (T) Deck Type  (F) Face Type  (B) Back");

    switch (getChar()) {
      case 'n':
        getNewNumDecks();
        return;
      case 't':
        getNewDeckType();
        return;
      case 'f':
        getNewFaceType();
        return;
      case 'b':
        drawHands();
        betOptions();
        return;
    }

    drawHands();
    gameOptions();
  }

  public void betOptions() {
    System.out.println(" (D) Deal Hand  (B) Change Bet  (O) Options  (Q) Quit");

    switch (getChar()) {
      case 'd':
        return;
      case 'b':
        getNewBet();
        return;
      case 'o':
        gameOptions();
        return;
      case 'q':
        this.quitting = true;
        clear();
        return;
    }

    drawHands();
    betOptions();
  }

  public void insureHand() {
    PlayerHand playerHand = getPlayerHands().get(currentHand);
    playerHand.setBet(playerHand.getBet() / 2);
    playerHand.setPlayed(true);
    playerHand.setPaid(true);
    playerHand.setStatus(HandStatus.LOST);
    money -= playerHand.getBet();

    drawHands();
    betOptions();
  }

  public void payHands() {
    int dealerHandValue = dealerHand.getValue(CountMethod.SOFT);
    boolean dealerHandBusted = dealerHand.isBusted();

    for (PlayerHand playerHand : playerHands) {
      if (playerHand.isPaid()) {
        continue;
      }

      playerHand.setPaid(true);
      int playerHandValue = playerHand.getValue(CountMethod.SOFT);

      if (dealerHandBusted || playerHandValue > dealerHandValue) {
        if (playerHand.isBlackjack()) {
          playerHand.setBet((int) (playerHand.getBet() * 1.5));
        }

        money += playerHand.getBet();
        playerHand.setStatus(HandStatus.WON);

      } else if (playerHandValue < dealerHandValue) {
        money -= playerHand.getBet();
        playerHand.setStatus(HandStatus.LOST);

      } else {
        playerHand.setStatus(HandStatus.PUSH);
      }
    }

    normalizeBet();
    saveGame();
  }

  public boolean needToPlayDealerHand() {
    return playerHands.stream()
        .anyMatch(playerHand -> !(playerHand.isBusted() || playerHand.isBlackjack()));
  }

  public void playDealerHand() {
    if (dealerHand.isBlackjack()) {
      dealerHand.setHideDownCard(false);
    }

    if (!needToPlayDealerHand()) {
      dealerHand.setPlayed(true);
      payHands();
      return;
    }

    dealerHand.setHideDownCard(false);

    int softCount = dealerHand.getValue(CountMethod.SOFT);
    int hardCount = dealerHand.getValue(CountMethod.HARD);

    while (softCount < 18 && hardCount < 17) {
      dealerHand.dealCard();
      softCount = dealerHand.getValue(CountMethod.SOFT);
      hardCount = dealerHand.getValue(CountMethod.HARD);
    }

    dealerHand.setPlayed(true);

    payHands();
  }

  public void noInsurance() {
    if (dealerHand.isBlackjack()) {
      dealerHand.setHideDownCard(false);
      dealerHand.setPlayed(true);

      payHands();
      drawHands();
      betOptions();

      return;
    }

    PlayerHand playerHand = playerHands.get(0);
    if (playerHand.isDone()) {
      playDealerHand();
      drawHands();
      betOptions();

      return;
    }

    drawHands();
    playerHand.getAction();
  }

  public void askInsurance() {
    System.out.println(" Insurance?  (Y) Yes (N) No");

    switch (getChar()) {
      case 'y':
        insureHand();
        return;
      case 'n':
        noInsurance();
        return;
    }

    drawHands();
    askInsurance();
  }

  public void dealNewHand() {
    if (getShoe().needToShuffle()) {
      getShoe().buildNewShoe(deckType);
    }

    playerHands.clear();
    playerHands.add(new PlayerHand(this));
    currentHand = 0;

    dealerHand = new DealerHand(this);

    for (int i = 0; i < 2; i++) {
      playerHands.get(0).dealCard();
      dealerHand.dealCard();
    }

    if (dealerHand.upcardIsAce()) {
      drawHands();
      askInsurance();

      return;
    }

    if (playerHands.get(0).isDone()) {
      dealerHand.setHideDownCard(false);

      payHands();
      drawHands();
      betOptions();

      return;
    }

    drawHands();
    playerHands.get(0).getAction();

    saveGame();
  }

  public void drawHands() {
    clear();

    StringBuilder output = new StringBuilder();

    output.append("\n Dealer:\n").append(dealerHand);
    output.append(String.format("\n Player $%.2f:\n", money / 100.0));

    for (PlayerHand playerHand : playerHands) {
      output.append(playerHand);
    }

    System.out.print(output);
  }

  public void saveGame() {
    try {
      BufferedWriter writer = new BufferedWriter(new FileWriter(SAVE_FILE));
      writer.write(String.format("%d|%d|%d|%d|%d", numDecks, money, currentBet, deckType, faceType));
      writer.close();
    } catch (IOException ignored) {
    }
  }

  public void loadGame() {
    try {
      BufferedReader lineReader = new BufferedReader(new FileReader(SAVE_FILE));
      String line = lineReader.readLine();
      String[] data = line.split("\\|");
      lineReader.close();

      if (data.length == 5) {
        this.numDecks = Integer.parseInt(data[0]);
        this.money = Integer.parseInt(data[1]);
        this.currentBet = Integer.parseInt(data[2]);
        this.deckType = Integer.parseInt(data[3]);
        this.faceType = Integer.parseInt(data[4]);
      }
    } catch (IOException | NullPointerException ignored) {
    }

    if (money < MIN_BET) {
      money = 10000;
      currentBet = MIN_BET;
    }
  }

  public char getChar() {
    try {
      return (char) getReader().read();
    } catch (IOException e) {
      throw new RuntimeException("Error reading input: " + e.getMessage(), e);
    }
  }

  public void clear() {
    System.out.print("\033[H\033[2J");
    System.out.flush();
  }

  public void loop() {
    while (!quitting) {
      dealNewHand();
    }
  }
}