Last time I got a lot of great advice, so this is take two with all that in mind:
My main concern with this implementation is the use of Inheritance (A base Player) vs having that as a interface - as that would lead to duplicate code in both Player and Dealer classes.
Everything works - the last thing for me to implement is having the dealer try to get to 21 after all the players.
But I would still really appreciate a review of this new version.
Deck.java
package com.tn.deck;
public interface Deck<T> {
T dealCard();
void shuffle();
}
AbstractPlayer.java
package com.tn.blackjack;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class AbstractPlayer {
private static final int WINNING_NUMBER = 21;
List<Card> hand = new ArrayList<>();
private boolean containsAce() {
return hand.stream().anyMatch(card -> card.getRank() == Rank.ACE);
}
final void drawCards(Card... cards) {
hand.addAll(Arrays.asList(cards));
}
final boolean isBust() {
return calculateScore() > WINNING_NUMBER;
}
final boolean hasBlackjack() {
return calculateScore() == WINNING_NUMBER;
}
final int calculateScore() {
int score = hand.stream().mapToInt(card -> card.getRank().getValue()).sum();
return score > WINNING_NUMBER && containsAce() ?
score - 10 : //Takes care of ace being either 1 or 11
score;
}
}
Card.java
package com.tn.blackjack;
public class Card {
private final Suit suit;
private final Rank rank;
Card(Suit suit, Rank rank) {
this.suit = suit;
this.rank = rank;
}
public Suit getSuit() {
return suit;
}
public Rank getRank() {
return rank;
}
public void print() {
System.out.printf("%s%s ", suit.getIcon(), rank.getName());
}
}
CardDeck.java
package com.tn.blackjack;
import com.tn.deck.Deck;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class CardDeck implements Deck<Card> {
private List<Card> deck;
CardDeck() {
this.deck = initializeDeck(Suit.values(), Rank.values());
shuffle();
}
CardDeck(int numberOfDecks) {
this.deck = initializeMultipleDecks(numberOfDecks, Suit.values(), Rank.values());
shuffle();
}
private List<Card> initializeDeck(Suit[] suits, Rank[] ranks) {
return Arrays.stream(suits)
.flatMap(suit -> Arrays.stream(ranks).map(rank -> new Card(suit, rank)))
.collect(Collectors.toList());
}
private List<Card> initializeMultipleDecks(int numberOfDecks, Suit[] suits, Rank[] ranks) {
return IntStream.range(0, numberOfDecks)
.mapToObj(i -> initializeDeck(suits, ranks))
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
@Override
public Card dealCard() {
if(deck.size() < 1) {
throw new IllegalStateException("Deck is empty");
}
Card card = deck.get(0);
deck.remove(0);
return card;
}
@Override
public void shuffle() {
Collections.shuffle(deck);
}
}
Dealer.java
package com.tn.blackjack;
import java.util.Arrays;
public class Dealer extends AbstractPlayer {
private Prompter prompter = new Prompter();
private CardDeck deck;
Dealer() {
initializeDeck();
}
public void dealInitialTwoCards(Player[] players) {
Arrays.stream(players).forEach(player -> player.drawCards(deck.dealCard(), deck.dealCard()));
drawCards(deck.dealCard(), deck.dealCard());
}
public void startPlayerLoop(Player[] players) {
Arrays.stream(players).forEach(player -> player.performAction(deck.dealCard()));
}
private void initializeDeck() {
int numberOfDecks = prompter.ask("How many decks should be used? ");
if(numberOfDecks < 1) {
throw new IllegalArgumentException("Deck size must be at least 1");
}
this.deck = numberOfDecks > 1 ?
new CardDeck(numberOfDecks) :
new CardDeck();
}
}
Player.java
package com.tn.blackjack;
public class Player extends AbstractPlayer {
private Prompter prompter = new Prompter();
private int id;
Player(int id) {
this.id = id;
}
public int getId() {
return id;
}
public void performAction(Card card) {
System.out.printf("%n%n==== Player %d ====%n%n", getId());
State state;
do {
prompter.printStatus(this);
state = prompter.getState();
switch (state) {
case HIT: drawCards(card);
break;
default:
break;
}
if(isBust() || hasBlackjack()) {
prompter.printStatus(this);
break;
}
} while(state == State.HIT);
}
}
Prompter.java
package com.tn.blackjack;
import java.util.Scanner;
public class Prompter {
private Scanner scanner = new Scanner(System.in);
public void printStatus(AbstractPlayer player) {
player.hand.forEach(Card::print);
System.out.printf("( score of %d )", player.calculateScore());
System.out.printf(player.hasBlackjack() ?
"\tBLACKJACK" : player.isBust() ?
"\tBUST" : "");
}
public State getState() {
String answer;
do {
System.out.printf("%n%nDo you want to (H)it or (S)tand? ");
answer = scanner.nextLine().trim().toUpperCase();
} while (!answer.equals("H") && !answer.equals("S"));
State state;
switch (answer) {
case "H": state = State.HIT;
break;
case "S": state = State.STAND;
break;
default: state = null;
}
return state;
}
public int ask(String question) {
String answer;
do {
System.out.printf("%s", question);
answer = scanner.nextLine().trim();
} while (!isInt(answer));
return Integer.parseInt(answer);
}
private static boolean isInt(String s) {
try {
Integer.parseInt(s);
} catch (NumberFormatException ex) {
return false;
}
return true;
}
}
Rank.java
package com.tn.blackjack;
public enum Rank {
TWO("2", 2), THREE("3", 3), FOUR("4", 4), FIVE("5", 5),
SIX("6", 6), SEVEN("7", 7), EIGHT("8", 8), NINE("9", 9), TEN("10", 10),
JACK("J", 10), QUEEN("Q", 10), KING("K", 10), ACE("A", 11);
private final String name;
private final int value;
Rank(String name, int value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public int getValue() {
return value;
}
}
State.java
package com.tn.blackjack;
public enum State {
HIT, STAND
}
Suit.java
package com.tn.blackjack;
public enum Suit {
SPADE("\u2660"),
HEART("\u2665"),
DIAMOND("\u2666"),
CLUB("\u2663");
private final String icon;
Suit(String icon) {
this.icon = icon;
}
public String getIcon() {
return icon;
}
}
Game.java
package com.tn.blackjack;
import java.util.stream.IntStream;
public class Game {
private Prompter prompter = new Prompter();
private Dealer dealer;
private Player[] players;
public Game() {
initializePlayers();
}
public void start() {
dealer.dealInitialTwoCards(players);
dealer.startPlayerLoop(players);
}
private void initializePlayers() {
int numberOfPlayers = prompter.ask("Not including the dealer - How many players? ");
this.dealer = new Dealer();
this.players = IntStream.rangeClosed(1, numberOfPlayers)
.mapToObj(Player::new)
.toArray(Player[]::new);
}
}