4
\$\begingroup\$

I have done some code-refactoring of my console-based BlackJack in C#. Finally found a better solution to solve Ace problems (double Aces should be value 22 and third Ace should be value 1).

I still find it hard to split the UI and the program logic although I have created a static Screen class for that. Secondly, I still can't figure out the purpose or need of having Hand class as suggested by some. Appreciate some code review in regards to design pattern or further code-refactoring of this version of my BlackJack.

Links

Card class

using System;
using System.Collections.Generic;

public enum Suit
{
    Diamonds, Clubs, Hearts, Spades
}

public enum Face
{
    Ace, Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten,
    Jack, Queen, King
}



public class Card
{
public Suit Suit { get; }
public Face Face { get; }
public string FaceName { get; }

// set value is for Ace because Ace can have value 1 or 11.
public int Value { get; set; }

public char Symbol { get; }

public ConsoleColor CardColor { get; set; }

/// Initilize Value and Suit Symbol
public Card(Suit suit, Face face)
{
    Suit = suit;
    Face = face;

    switch (Suit)
    {
        case Suit.Clubs:
            Symbol = '♣';
            CardColor = ConsoleColor.White;
            break;
        case Suit.Spades:
            Symbol = '♠';
            CardColor = ConsoleColor.White;
            break;
        case Suit.Diamonds:
            Symbol = '♦';
            CardColor = ConsoleColor.Red;
            break;
        case Suit.Hearts:
            Symbol = '♥';
            CardColor = ConsoleColor.Red;
            break;
    }

    switch (Face)
    {
        case Face.Ten:
            Value = 10;
            FaceName = "10";
            break;
        case Face.Jack:
            Value = 10;
            FaceName = "J";
            break;
        case Face.Queen:
            Value = 10;
            FaceName = "Q";
            break;
        case Face.King:
            Value = 10;
            FaceName = "K";
            break;
        case Face.Ace:
            Value = 11;
            FaceName = "A";
            break;
        default:
            Value = (int)face + 1;
            FaceName = Value.ToString();
            break;
    }
}

public void PrintCardColor()
{
    Utility.WriteLineInColor($"{this.Symbol}{this.FaceName}", this.CardColor);
}

public void PrintCard(Card _card)
{
    Console.Write($"Drawn card is ");
    Utility.WriteLineInColor($"{_card.Symbol}{_card.FaceName}", _card.CardColor);
}
}

Deck class

using System;
using System.Collections.Generic;
using System.Linq;

public class Deck
{
// Field
private List<Card> deck;


public Deck()
{
    /// Contruct a deck of 52 cards.
    deck = new List<Card>(52);
    InitializeColdDeck();
    Shuffle();
    Screen.PrintShufflingDeck();
}

private void InitializeColdDeck()
{
    var suitAsList = Enum.GetValues(typeof(Suit)).Cast<Suit>().ToArray();

    // Alternate way is below by LINQ. Functional programming.
    // This way more concise and less prone to have error.
    deck = suitAsList
    .SelectMany(
        suit => Enumerable.Range(0, 12),
        (suit, rank) => new Card((Suit)suit, (Face)rank))
    .ToList();

    // Alternate way is below by using 2 for loops:
    // This way seems more readable but prone to have error.
    // for (int j = 0; j < 4; j++)
    //     for (int i = 0; i < 13; i++)
    //         deck.Add(new Card((Suit)j, (Face)i));
}

// Pick top card and remove it from the deck. 
// Return: The top card of the deck
public Card DrawCard(Player person, bool test = false)
{
    Card card;
    if (test)
    {
        card = new Card(Suit.Clubs, Face.Ace);
    }
    else
    {
        card = deck[0];
    }


    if (person.GetHandValue() + card.Value == 21 && person.Hand.Count == 1)
        // Check natural black jack immediately after received first 2 cards.            
        person.IsNaturalBlackJack = true;
    else if (person.GetHandValue() + card.Value > 21 && card.Face == Face.Ace)
        // person hand count is not used here because it could be double Aces in first two cards.
        // only the first Aces is counted as 11 while the subsequent Aces will be 1 
        // if hand value is more than 21
        card.Value = 1;

    person.Hand.Add(card);
    deck.Remove(card);
    return card;
}

/// Randomize the order of the cards in the Deck using Fisher–Yates shuffle algorithm.
private void Shuffle()
{
    Random rng = new Random();

    int n = deck.Count;

    // Each loop find a random card to insert into new card list object.
    while (n > 1)
    {
        n--;
        int k = rng.Next(n + 1);
        Card card = deck[k];
        deck[k] = deck[n];
        deck[n] = card;
    }
}

public void ShowRemainingDeckCount()
{
    Console.WriteLine("\nRemaining cards in the deck: " + GetRemainingDeckCount());
}

public int GetRemainingDeckCount()
{
    return deck.Count;
}
}

Player class

using System;
using System.Collections.Generic;
using System.Threading;

// This player class is designed specifically for BlackJack game
// Some property for Player is only for BlackJack game
public class Player
{
public string Name { get; set; }
public List<Card> Hand { get; set; }

public bool IsNaturalBlackJack { get; set; }

public bool IsBusted { get; set; } = false;

public int TotalWins { get; set; } = 0;
public static int TotalWinsCounter { get; private set; } = 0;


public int ChipsOnHand { get; set; } = 500;

public int ChipsOnBet { get; set; }

public bool Turn { get; set; } = true;

public Player(string Name = "Dealer")
{
    this.Name = Name;
    Hand = new List<Card>(5);
}
public int GetHandValue()
{
    int value = 0;
    foreach (Card card in Hand)
        value += card.Value;

    return value;
}

public void ShowHandValue()
{
    Console.WriteLine($"{this.Name}'s hand value is: {this.GetHandValue()} ({this.Hand.Count} cards)");
}

public void ShowUpCards(bool isDealer = false)
{
    Console.WriteLine($"\n{this.Name}'s hand has:");
    if (isDealer)
    {
        Utility.WriteLineInColor($"{this.Hand[0].Symbol}{this.Hand[0].FaceName}", this.Hand[0].CardColor);

        Utility.WriteLineInColor("<Hole Card>", ConsoleColor.Magenta);

        Console.WriteLine($"{this.Name}'s Hand value is: {this.Hand[0].Value}");
    }
    else
    {
        foreach (var card in this.Hand)
            card.PrintCardColor();

        ShowHandValue();
    }
}



public void AddWinCount()
{
    this.TotalWins = ++TotalWinsCounter;
}

public void Hit(Deck deck)
{
    Console.Write($"{this.Name} hits. ");
    Utility.Sleep();

    // Take a card from the deck and put into player's Hand.
    //Card card = new Card(Suit.Hearts, Face.Ace); //deck.DrawCard();        
    Card card = deck.DrawCard(this);
    // If there is any Ace in the Hand, change all the Ace's value to 1.
    // if (this.GetHandValue() + card.Value > 21 && card.Face == Face.Ace)
    //     card.Value = 1;

    //Hand.Add(card); // Background
    card.PrintCardColor(); // UI
    Utility.Sleep();
}

public void Stand()
{
    Console.WriteLine($"{this.Name} stands."); // UI
    Utility.Sleep();

    this.ShowUpCards(); // UI
    Utility.Sleep();

    this.Turn = false;
}

public bool CanPlayerStand(bool isPlayerBusted)
{
    // Player can stand without condition
    if (!this.Name.Equals("Dealer"))
        return true;
    else if (isPlayerBusted) // for dealer to auto stand if player busted        
        return true;

    return false;
}

public void ResetPlayerHand()
{
    this.Hand = new List<Card>(5);
    this.IsNaturalBlackJack = false;
    this.IsBusted = false;
}
}

Screen class

using System;

public static class Screen
{
public static void SplashScreen()
{
    Console.Write("Loading");
    Utility.printDotAnimation(20);
    Console.Clear();
    Console.Title = "Steve C# Console-Based BlackJack Game (Version 2)";
    Console.Write("Steve C# Console-Based BlackJack Game ");
    Utility.WriteInColor(" ♠ ", ConsoleColor.White);
    Utility.WriteInColor(" ♥ ", ConsoleColor.Red);
    Utility.WriteInColor(" ♣ ", ConsoleColor.White);
    Utility.WriteInColor(" ♦ ", ConsoleColor.Red);
}

public static void PromptPlayerName()
{
    Console.Write("\n\nEnter player's name: ");
}
public static void PrintShufflingDeck()
{
    Console.Write("Shuffling cold deck");
    Utility.printDotAnimation();
}
}

Utility class

using System;
using System.Threading;

class Utility
{

public static void WriteLineInColor(string text, ConsoleColor color)
{
    Console.ForegroundColor = color;
    Console.WriteLine(text);
    Console.ResetColor();
}

public static void WriteInColor(string text, ConsoleColor color)
{
    Console.ForegroundColor = color;
    Console.Write(text);
    Console.ResetColor();
}

public static void Sleep(int miliseconds = 1500)
{
    Thread.Sleep(miliseconds);
}

public static void printDotAnimation(int timer = 10)
{
    for (var x = 0; x < timer; x++)
    {
        Console.Write(".");
        Thread.Sleep(100);
    }
    Console.WriteLine();
}

public static void Line()
{
    Console.WriteLine("\n--------------------------------------------------");
}
}

BlackJackGame class

using System;
using System.Threading;

public class BlackJackGame
{
private Deck deck;
public void Play()
{
    bool continuePlay = true;
    Screen.SplashScreen();
    Screen.PromptPlayerName();

    var player = new Player(Console.ReadLine());

    var dealerComputer = new Player();

    deck = new Deck();

    while (continuePlay)
    {
        // Initialize screen and reset player and dealer's certain property
        // for the new round.
        Console.Clear();
        player.ResetPlayerHand();
        dealerComputer.ResetPlayerHand();

        // Create a new deck if remaining cards are less than 20
        if (deck.GetRemainingDeckCount() < 20)
            deck = new Deck();

        deck.ShowRemainingDeckCount();

        // Show player bank roll
        Console.WriteLine($"{player.Name} Chips Balance: {player.ChipsOnHand}");

        if (player.ChipsOnHand <= 10)
        {
            Utility.WriteLineInColor("Insufficient chips in your account.", ConsoleColor.Red);
            Utility.WriteLineInColor("Please reload your chips from the counter to continue to play.\n", ConsoleColor.Red);

            continuePlay = false;
            break;
        }

        // Get bet amount from player
        Console.Write("Enter chips: ");
        player.ChipsOnBet = Convert.ToInt16(Console.ReadLine());
        // for brevity, no input validation here.

        // Deal first two cards to player (Background)
        deck.DrawCard(player);
        deck.DrawCard(player);

        // Show player's hand (UI)
        player.ShowUpCards();
        Utility.Sleep();

        Utility.Line();

        // Deal first two cards to dealer (Background)
        deck.DrawCard(dealerComputer);
        deck.DrawCard(dealerComputer);

        // Show dealer's hand (UI)        
        dealerComputer.ShowUpCards(true);
        Utility.Sleep();

        Utility.Line();

        // Check natural black jack
        if (CheckNaturalBlackJack(player, dealerComputer) == false)
        {
            // If both also don't have natural black jack, 
            // then player's turn to continue. 
            // After player's turn, it will be dealer's turn.
            TakeAction(player);
            TakeAction(dealerComputer, player.IsBusted);

            AnnounceWinnerForTheRound(player, dealerComputer);
        }

        Console.WriteLine("This round is over.");

        Console.Write("\nPlay again? Y or N? ");

        continuePlay = Console.ReadLine().ToUpper() == "Y" ? true : false;
        // for brevity, no input validation
    }

    PrintEndGame(player, dealerComputer);
}



private void TakeAction(Player currentPlayer, bool isPlayerBusted = false)
{
    string opt = "";
    currentPlayer.Turn = true;

    Console.WriteLine($"\n{currentPlayer.Name}'s turn. ");

    while (currentPlayer.Turn)
    {
        if (currentPlayer.Name.Equals("Dealer"))
        {
            Utility.Sleep(2000); // faking thinking time.               
            // Mini A.I for dealer.
            if (isPlayerBusted) // if player bust, dealer can stand to win                
                // break; // Dealer is required to still reveal hole card even though the player bust
                opt = "S";
            else
                opt = currentPlayer.GetHandValue() <= 16 ? "H" : "S";
        }
        else
        {
            // Prompt player to enter Hit or Stand.
            Console.Write("Hit (H) or Stand (S): ");
            opt = Console.ReadLine();
        }

        switch (opt.ToUpper())
        {
            case "H":
                currentPlayer.Hit(deck);
                currentPlayer.ShowHandValue();

                break;
            case "S":
                //if (currentPlayer.CanPlayerStand(isPlayerBusted))
                currentPlayer.Stand();

                break;
            default:
                Console.WriteLine("Invalid command.");
                break;
        }

        CheckPlayerCard(currentPlayer);
    }

    Console.WriteLine($"{currentPlayer.Name}'s turn is over.");
    Utility.Line();
    Utility.Sleep();
}



private void CheckPlayerCard(Player _currentPlayer)
{
    // If current player is busted, turn is over.
    if (_currentPlayer.GetHandValue() > 21)
    {
        Utility.WriteLineInColor("Bust!", ConsoleColor.Red);
        Utility.Sleep();

        _currentPlayer.IsBusted = true;
        _currentPlayer.Turn = false;
    }
    // If current player total card in hand is 5, turn is over.
    else if (_currentPlayer.Hand.Count == 5)
    {
        Console.WriteLine($"{_currentPlayer.Name} got 5 cards in hand already.");
        Utility.Sleep();

        _currentPlayer.Turn = false;
    }
}

private bool CheckNaturalBlackJack(Player _player, Player _dealer)
{
    Console.WriteLine();
    if (_dealer.IsNaturalBlackJack && _player.IsNaturalBlackJack)
    {
        Console.WriteLine("Player and Dealer got natural BlackJack. Tie Game!");
        _dealer.ShowUpCards();
        return true;
    }
    else if (_dealer.IsNaturalBlackJack && !_player.IsNaturalBlackJack)
    {
        Console.WriteLine($"{_dealer.Name} got natural BlackJack. {_dealer.Name} won!");
        _dealer.ShowUpCards();
        _player.ChipsOnHand -= (int)Math.Floor(_player.ChipsOnBet * 1.5);
        return true;
    }
    else if (!_dealer.IsNaturalBlackJack && _player.IsNaturalBlackJack)
    {
        Console.WriteLine($"{_player.Name} got natural BlackJack. {_player.Name} won!");
        _player.AddWinCount();
        _player.ChipsOnHand += (int)Math.Floor(_player.ChipsOnBet * 1.5);
        return true;
    }

    // guard block
    return false;
}

private void AnnounceWinnerForTheRound(Player _player, Player _dealer)
{
    Console.WriteLine();
    if (!_dealer.IsBusted && _player.IsBusted)
    {
        Console.WriteLine($"{_dealer.Name} won.");
        _dealer.AddWinCount();
        _player.ChipsOnHand -= _player.ChipsOnBet;
    }
    else if (_dealer.IsBusted && !_player.IsBusted)
    {
        Console.WriteLine($"{_player.Name} won.");
        _player.AddWinCount();
        _player.ChipsOnHand += _player.ChipsOnBet;
    }
    else if (_dealer.IsBusted && _player.IsBusted)
    {
        Console.WriteLine("Tie game.");
    }
    else if (!_dealer.IsBusted && !_player.IsBusted)
        if (_player.GetHandValue() > _dealer.GetHandValue())
        {
            Console.WriteLine($"{_player.Name} won.");
            _player.AddWinCount();
            _player.ChipsOnHand += _player.ChipsOnBet;
        }
        else if (_player.GetHandValue() < _dealer.GetHandValue())
        {
            Console.WriteLine($"{_dealer.Name} won.");
            _dealer.AddWinCount();
            _player.ChipsOnHand -= _player.ChipsOnBet;
        }

        else if (_player.GetHandValue() == _dealer.GetHandValue())
            Console.WriteLine("Tie game.");

}

private void PrintEndGame(Player player, Player dealerComputer)
{
    Console.WriteLine($"{player.Name} won {player.TotalWins} times.");
    Console.WriteLine($"{dealerComputer.Name} won {dealerComputer.TotalWins} times.");
    Console.WriteLine("Game over. Thank you for playing.");
}
}
\$\endgroup\$
  • \$\begingroup\$ Is this a follow-up on codereview.stackexchange.com/questions/214390/…? If so, I would edit both questions to link to each other. Also take advantage of the opportunity to format your code correctly. \$\endgroup\$ – dfhwze Jul 14 at 8:47
  • \$\begingroup\$ Yes, that is the initial version. \$\endgroup\$ – Steve Ngai Jul 14 at 10:09
  • 2
    \$\begingroup\$ Please don't edit the question to incorporate the suggestions. See What you may and may not do after receiving answers. \$\endgroup\$ – L. F. Jul 15 at 5:31
  • \$\begingroup\$ Oh okay sure. Okay, I made some changes as suggested and updated in my github link of the original post. \$\endgroup\$ – Steve Ngai Jul 15 at 5:59
5
\$\begingroup\$

Your two main issues have not been resolved.


Separation of Concerns

I still find it hard to split the UI and the program logic although I have created a static Screen class for that.

Even though you have tried to offset UI operations to a dedicated class Screen, your code is still full of mixed concerns logic (presentation vs application vs test).

  • Card has properties FaceName, Symbol used only at presentation layer. They might serve a purpose as default string representation, regardless how they get presented to the end user.
  • Card stores CardColor, which is definitely presentation logic.
  • Card has presentation layer methods PrintCardColor and PrintCard. They have no place in the application layer. In addition, PrintCard should be either static or have no argument.
  • Deck contains a method DrawCard which takes an argument test. This mixes test flow with normal application flow.
  • Deck has method ShowRemainingDeckCount which is presentation logic.
  • Player is even worse than the other classes. Some methods mix application with presentation logic in the body. Split application logic from methods Hit and Stand.
  • Player contains methods ShowHandValue and ShowUpCards which are presentation logic.
  • BlackJackGame also mixes presentation and application logic in Play making it impossible to use this class in other UIs.

Object-Oriented Design

I still can't figure out the purpose or need of having Hand class as suggested by some.

Your current implementation requires you to do tricks and store way too much information on your existing classes.

  • Card's property FaceName could be an extension method on enum Face.
  • Player really needs to be split into several classes: Player, Hand, Bet. I will explain below.
  • Hand should also store properties IsNaturalBlackJack, GetHandValue and ChipsOnHand.
  • I would also place betting logic IsBusted, ChipsOnBet, Turn, Bit, Stand in a separate class Bet.
  • BlackJackGame can be further divided into Rounds and a Dealer.

By not having a class Hand you are required to do a trick with Value in Card. This is hand logic, not card. A Hand should provide the value used in a bet. A card can still have its own value, but this value is not of importance in the game, only as internal logic for the hand to calculates its own value.

// set value is for Ace because Ace can have value 1 or 11.
public int Value { get; set; }
\$\endgroup\$
  • \$\begingroup\$ Thanks a lot for the code review. Okay, I will try split the 'big' class into smaller classes. For the the presentation layer, I still yet to find some good example on it. Do you have any example on splitting application layer and presentation layer? If it is a web application, I could use MVC pattern, but in console, I have yet to find any. \$\endgroup\$ – Steve Ngai Jul 14 at 10:21
  • 3
    \$\begingroup\$ The most important thing is to remove all UI logic from the application layer. You don't want any Console references in Player, Card, Deck. What you could do for a console app, is to make extension methods on your application classes for printing their properties to the Console. This approach has the least impact on your current design. Another option is to create custom presentation classes and use mapping from application to presentation classes. This might be overkill for a simple console application though. \$\endgroup\$ – dfhwze Jul 14 at 10:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.