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; }
/// Initialize 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.");
}
}