As part of a recent interview, I was assigned to write a small BlackJack program. After submitting the solution, I received an answer that "the solution was functionally OK, but was way below their standards of quality code, especially coming from a person with significant experience" (I have 15 years experience).
I have posted my code in order to receive an honest critical review on the quality of the code. It is true that I wrote it while on vacation, but does it really suck so much? If so, how could I have written it differently?
public class Deck {
public enum CARD {
S2(2),S3(3),S4(4),S5(5),S6(6),S7(7),S8(8),S9(9),S10(10),SJ(10),SQ(10),SK(10),SA(11),
D2(2),D3(3),D4(4),D5(5),D6(6),D7(7),D8(8),D9(9),D10(10),DJ(10),DQ(10),DK(10),DA(11),
C2(2),C3(3),C4(4),C5(5),C6(6),C7(7),C8(8),C9(9),C10(10),CJ(10),CQ(10),CK(10),CA(11),
H2(2),H3(3),H4(4),H5(5),H6(6),H7(7),H8(8),H9(9),H10(10),HJ(10),HQ(10),HK(10),HA(11);
private int points;
private CARD(int points) {
this.points = points;
}
public int getPoints() {
return points;
}
}
private Stack<CARD> deck = new Stack<Deck.CARD>();
private Deck(Stack<CARD> deck) {
this.deck = deck;
}
private Deck(){
shuffleDeck();
}
private void shuffleDeck() {
// we need it mutable and the list provided by Arrays is Immutable
List<CARD> allCards = new ArrayList(Arrays.asList(CARD.values()));
int max = 52;
Random rand = new Random();
while (allCards.size()>0) {
CARD removedCard = allCards.remove(rand.nextInt(max)) ;
this.deck.add(removedCard);
--max;
}
}
public CARD draw() {
return deck.pop();
}
public static class Builder {
Stack<CARD> deck=null;
public static Builder createNew(){
return new Deck.Builder();
}
public Builder fromFile(File file) throws IOException {
Scanner scanner=null;
try {
scanner = new Scanner(file);
deck = new Stack<>();
for (String card : scanner.nextLine().split(", ")) {
deck.add(Enum.valueOf(CARD.class, card));
}
} finally {
if (scanner!=null) {
scanner.close();
}
}
return this;
}
public Builder fromStack(Stack stack) {
this.deck = stack;
return this;
}
public Builder validate() {
if (deck==null) {
return this;
}
if (deck.size()>52) {
throw new RuntimeException("Critical error, deck is not initialized correctly.You need 52 unique cards.");
}
HashSet<CARD> allUniqueCards = new HashSet<>(deck==null?new HashSet<>():deck);
// verify uniqueness of cards in case of 52 card condition is met but some are duplicates
if (deck==null || allUniqueCards.size()==52) {
return this;
}
// this is error that is not recoverable therefore we define it as RuntimeException
throw new RuntimeException("Critical error, deck is not initialized correctly.You need 52 unique cards.");
}
public Deck build() {
return deck==null? new Deck():new Deck(deck);
}
}
public String toString() {
String toString = deck.stream().map(t->{return t.toString()+", ";}).reduce("", (t,q)->{return t+q;});
if (!toString.isEmpty()) {
toString = toString.substring(0,toString.length()-2);
}
return toString;
}
}
Hand class
public class Hand {
// in general it is one idea more correct to be Set,
//but I am on holiday right now and I need to write the tests fast.
private List<CARD> hand = new ArrayList<>();
public boolean hasBlackJack() {
return score()==21;
}
public boolean has22(){
return score()==22;
}
public boolean lessThan17(){
return score()<17;
}
public boolean greaterThan(Hand hand) {
return this.score()>hand.score();
}
public boolean greaterThan21(){
return score()>21;
}
public void addCard(CARD card) {
hand.add(card);
}
private int score(){
return hand.stream().mapToInt(t->{return t.getPoints();}).sum();
}
public boolean empty() {
return hand.size()==0;
}
public String toString() {
// the sequential stream is not nessesary realy, but it makes it one idea easier to test later.
String toString = hand.stream().sequential().map(t->{return t.name();}).reduce("", (t,q)->{return t.isEmpty()?q: t+", " + q;});
return toString;
}
}
Player class
public class Player {
private String playerName =null;
private Hand hand = new Hand();
public Player(String playerName) {
this.playerName=playerName;
}
public boolean hasBlackJack(){
return hand.hasBlackJack();
}
public boolean has22() {
return hand.has22();
}
public boolean lessThan17(){
return hand.lessThan17();
}
public boolean greaterThan21() {
return hand.greaterThan21();
}
public boolean hasEmptyHand() {
return hand.empty();
}
public boolean handGreaterThan(Player player) {
return this.hand.greaterThan(player.hand);
}
public void draw(Deck deck) {
hand.addCard(deck.draw());
}
public String getPlayerName(){
return playerName;
}
public String toString() {
return playerName + ":" + " " + hand.toString();
}
}
//Assumption in case of a tie SAM wins.
public class Play21 {
Player dealer = null;
Player sam = null;
Deck deck = null;
public Play21(Player sam,Player dealer,Deck deck) {
this.sam=sam;
this.dealer=dealer;
this.deck = deck;
}
public static void printWinner(Player winner,Player looser) {
System.out.println(winner.getPlayerName());
System.out.println(winner.getPlayerName().equals("sam")?winner:looser);
System.out.println(winner.getPlayerName().equals("sam")?looser:winner);
}
private Player round1() {
sam.draw(deck);
dealer.draw(deck);
sam.draw(deck);
dealer.draw(deck);
if (dealer.hasBlackJack()&&sam.hasBlackJack()) {
return sam;
}
else if (sam.hasBlackJack()) {
return sam;
}
else if(dealer.hasBlackJack()) {
return dealer;
}
if (dealer.has22()&&sam.has22()) {
return dealer;
}
else if (sam.has22()) {
return dealer;
}
else if (dealer.has22()) {
return sam;
}
return null;
}
private Player round2() {
if (sam.hasEmptyHand()||dealer.hasEmptyHand()) {
throw new IllegalStateException("You need to invoke round1 first");
}
while(sam.lessThan17()) {
sam.draw(deck);
}
if (sam.greaterThan21()) {
return dealer;
}
while (sam.handGreaterThan(dealer)) {
dealer.draw(deck);
}
if (dealer.greaterThan21()) {
return sam;
}
if (dealer.handGreaterThan(sam)) {
return dealer;
}
else {
return sam;
}
}
public Player play(Deck deck){
Player winner = round1();
if (winner!=null) {
return winner;
}
return round2();
}
public static Deck createDeck(String args[]) throws IOException {
Deck deck =null;
if (args.length==0) {
deck=Deck.Builder.createNew()
.validate()
.build();
}
else if (args.length==1) {
File file = new File(args[0]);
if (file.isFile()) {
deck=Deck.Builder.createNew()
.fromFile(file)
.validate()
.build();
}
else {
// Unrecoverable exception therefore declared as RuntimeException
throw new RuntimeException("Path is not a file, or file does not exist,or file is unreadable(check permissions).");
}
}
else {
throw new RuntimeException("Illegal number of arguments. ");
}
return deck;
}
public static void main(String[] args) throws IOException{
Deck deck = createDeck(args);
Player dealer = new Player("dealer");
Player sam = new Player("sam");
Play21 play = new Play21(sam,dealer,deck);
Player winner = play.play(deck);
printWinner(winner, winner==sam?dealer:sam);
}
}