Welcome back to our Deep Dive series on building the casino game Blackjack in C# and Blazor WebAssembly!
In this second part, we're going to use the modeling ideas we wrote down in the previous post to build a complete C# model for a Blackjack game.
You might want to read the previous post in this series before reading this one. Here it is:
Also, there's a sample GitHub repository that has all of the code used in this series.
All caught up? Good! Let's jump right in.
Basic Enumerations
Let's begin our C# modeling with the most basic type we can model: enumerations for the card's suit and value.
public enum CardSuit
{
Hearts,
Clubs,
Diamonds,
Spades
}
public enum CardValue
{
Ace = 1,
Two = 2,
Three = 3,
Four = 4,
Five = 5,
Six = 6,
Seven = 7,
Eight = 8,
Nine = 9,
Ten = 10,
Jack = 11,
Queen = 12,
King = 13
}
We will deal with the score later in this post.
Playing Cards
The next-most-simple object we can model is the individual playing cards. You may recall from the previous post that we decided that the Card
object should have the following attributes:
- A suit
- A value
- A score
Suits and values are already defined as enumerations, and by making them properties of the Card
object we can calculate a score.
Visibility
We need to consider one additional situation for this model, and that is this: the dealer has one card that is not visible to the player. So what model should that property (visibility) be a part of?
There are several ways to answer this question; the one we are going with is that we will make a new property IsVisible
on the Card object. This property will be settable outside of the object so that the Dealer can mark their cards as not visible.
IsTenCard
Lastly, and as a convenience, we can define the property IsTenCard
to identify if the given Card
instance is a Ten, Jack, Queen, or King, and thus worth ten points.
public class Card
{
//... Other properties and methods
public bool IsTenCard
{
get
{
return Value == CardValue.Ten
|| Value == CardValue.Jack
|| Value == CardValue.Queen
|| Value == CardValue.King;
}
}
}
Image URLs
We are implementing the display for our cards as individual images, with names like cardClubsJack.png, cardHeartsSix.png, cardSpadesAce.png, etc.
The images look like this:
Our Card object will need a property which stores the name of the image to display for that Card.
public class Card
{
//... Other properties and methods
public string ImageName { get; set; }
}
The Deck
The next object we can build is the deck of cards.
A Collection of Cards
The deck, at its core, is very simple: it's a collection of Card
objects. But what kind of collection?
One of the properties of a real-world deck of cards is that, when drawing a card, we always draw from the top of the deck. There is a collection class in .NET that implements similar functionality: the Stack<T>
class. Stack<T>
defines a Pop()
method, which removes and returns the "topmost" object in the collection.
Our new CardDeck
object will need a property of type Stack<Card>
which stores the individual Card
objects.
Count, Add, and Draw
We must now consider a few properties. First, we need to keep track of how many cards remain in the deck; if the number of cards gets below a certain value, we need to reshuffle the deck.
public class CardDeck
{
//... Other methods and properties
public int Count
{
get
{
return Cards.Count;
}
}
}
As we decided in the previous post, we need methods to add cards to the deck and to draw a card from the deck. These methods are fairly straightforward to implement:
Initialization and Shuffling
The most complex part of the CardDeck
implementation is this: when we create a new instance of CardDeck
, we need to first populate the cards with the correct number and suits, and then shuffle the cards thoroughly.
Let's start by creating a constructor for our CardDeck
object, which first creates and inserts all of the cards needed.
We now need to extend this constructor to shuffle the cards. For this, we'll be using the venerable Fisher-Yates Shuffling Algorithm I blogged about a while back:
Here's that implementation:
public class CardDeck
{
//... Other properties and methods
public CardDeck()
{
//... Rest of constructor
//First, convert the new Stack<T> of Card objects to an array.
var array = cards.ToArray();
Random rnd = new Random();
//Step 1: For each unshuffled item in the collection
for (int n = array.Count() - 1; n > 0; --n)
{
//Step 2: Randomly pick an element
// which has not been shuffled
int k = rnd.Next(n + 1);
//Step 3: Swap the selected element with the
// last "unstruck" element in the collection
Card temp = array[n];
array[n] = array[k];
array[k] = temp;
}
//Finally, insert the now-shuffled cards into the Cards property.
for (int n = 0; n < array.Count(); n++)
{
Cards.Push(array[n]);
}
}
}
}
Our CardDeck
object is now ready for use! We can move on to the more-complex objects, starting with Person
.
The Person Object
Recall from the last post that we need a common Person
object that both Player
and Dealer
will inherit from. That Person
needs the following abilities:
- Keep a hand of cards
- Use the hand to calculate a score
- Use the hand to determine if they are busted
The Visible Score Problem
First, though, we need to solve an unsolved problem from the previous post: how do we deal with the Dealer
object's visible score (e.g. the score from only the face-up cards)?
This is one of those places where the modeler (i.e. us) can make "executive decisions". There are a couple of ways to model the visible score, such as making it a property of the Dealer object, a property of the Person object, or an external method. For this post, I have chosen the second option: we will make a property VisibleScore
that is part of the root Person
object, since it is Person
that holds the property for Cards
. Please note that none of these options are inherently worse than the others, they just require different implementation details.
With all that in mind, we can build a Person
object with a set of properties to represent those abilities. Here's the skeleton object:
public class Person
{
public List<Card> Cards { get; set; } = new List<Card>();
}
We now need to fill in the other properties.
True Scores and Visible Scores
The most complex thing the Person
object will do is calculate that person's current score. Because we want a property for the VisibleScore
and a property Score
for the true score, we will use a private method to calculate the score.
The algorithm to calculate the score goes like this:
- PASS IN a value that determines whether we calculate the visible score only, or the true score.
- IF the sum value of all cards is less than or equal to 21, return that sum.
- IF there are no Aces in the person's hand AND the sum is greater than 21, the person has bust, so return the score.
- IF there are Aces in the person's hand...
- WHILE there are Aces left that have not been converted
- CONVERT a single Ace to being worth 1 point.
- IF the score is now less than or equal to 21, return the score.
- END WHILE
- IF the score is STILL greater than 21, the person has bust, so return the score.
The resulting code looks something like this:
public class Person
{
//... Other properties and methods
//1. Pass in a value for the "only visible score" situation
private int ScoreCalculation(bool onlyVisible = false)
{
var cards = Cards;
//1. Filter the Cards to only include the visible cards
// if that parameter is set to true.
if (onlyVisible)
{
cards = Cards.Where(x => x.IsVisible).ToList();
}
//2. If the sum total of all cards is <= 21, return that value
var totalScore = cards.Sum(x => x.Score);
if (totalScore <= 21) return totalScore;
//3. If there are no Aces and the sum total is > 21,
// the person has bust
bool hasAces = cards.Any(x => x.Value == CardValue.Ace);
if (!hasAces && totalScore > 21) return totalScore;
//By this point, the sum will be greater than 21
// if all the Aces are worth 11
//So, make each Ace worth 1, until the sum is <= 21
var acesCount = cards.Where(x => x.Value == CardValue.Ace).Count();
var acesScore = cards.Sum(x => x.Score);
//5. While there are Aces left...
while (acesCount > 0)
{
//6. Make an Ace worth 1 point
acesCount -= 1;
acesScore -= 10;
//7. If the score is now less than or equal to 21,
// return the score.
if (acesScore <= 21) return acesScore;
} //8
//9. If the score never gets returned, the person has bust
return cards.Sum(x => x.Score);
}
}
We can then make two properties; one for the true score, and one for the visible score.
public class Person
{
//... Other properties and methods
public int Score
{
get
{
return ScoreCalculation();
}
}
public int VisibleScore
{
get
{
return ScoreCalculation(true);
}
}
}
Checking for Blackjack and Bust
In the previous post, we decided that we wanted a special display that normally displays the Person's score, but will also show when they get a blackjack and when they bust.
To do this, we first need a property that checks to see if the Person
has a blackjack. A person has a blackjack if;
- Their score is exactly 21 AND
- They have exactly two cards AND
- One of their cards is an Ace AND
- The other card is a ten-card
Here's the property for this:
We also want a convenience property that shows if the Person
has bust:
The Score Display
Using the properties HasNaturalBlackjack
and IsBusted
that we just defined, we can now create a property to display the Person
object's score. This property uses all of the properties of Person
that we have defined so far:
public class Person
{
//... Other properties and methods
public string ScoreDisplay
{
get
{
if (HasNaturalBlackjack && Cards.All(x => x.IsVisible))
return "Blackjack!";
//We use Visible Score because this display
//should only show the Dealer's visible score,
//and the Player's visible score and true score
//will always be the same value.
var score = VisibleScore;
if (score > 21)
return "Busted!";
else return score.ToString();
}
}
}
Convenience Methods - Adding a Card and Clearing the Hand
There are two convenience methods we still need, though your implementation may forego them.
First we need a straightforward method to add cards to the Person's hand:
Finally, we need a method to clear the Person
's hand:
public class Person
{
//...Other properties and methods
public void ClearHand()
{
Cards.Clear();
}
}
With all of that, our root Person
object is complete! Now we can move on to the Dealer
and Player
objects.
The Dealer Object
Let's start with the Dealer object.
The Dealer
needs to inherit from the root Person
object:
public class Dealer : Person { }
From there, we can add the properties unique to the Dealer
The Card Deck
Let's start with a property for the CardDeck
object:
Dealing
The Dealer
object will also need three methods:
- A method that draws a card from the deck.
- A method that draws a card from the deck to give to the player.
- A method that draws a card from the deck to give to the dealer.
These methods end up being pretty straightforward:
public class Dealer : Person
{
//... Other properties and methods
public Card Deal()
{
return Deck.Draw();
}
public async Task DealToSelf()
{
await AddCard(Deal());
}
public async Task DealToPlayer(Player player)
{
await player.AddCard(Deal());
}
}
Has an Ace Showing
We also need one convenience property that is used for the Insurance special play: whether or not the Dealer
has an Ace showing.
public class Dealer : Person
{
//... Other properties and methods
public bool HasAceShowing => Cards.Count == 2
&& VisibleScore == 11
&& Cards.Any(x => x.IsVisible == false);
}
With that, our Dealer
is complete, and we can move on to implementing the Player
object.
The Player Object
The Player
object, like the Dealer
, needs to inherit from Person
.
public class Player : Person { }
Funds
The most distinguishing characteristic of the Player
is that they have funds, i.e. the money they walked up to the table with. Let's create a property for those funds, and set an initial amount:
public class Player : Person
{
public decimal Funds { get; set; } = 200M; //$200
}
Bets
We also need properties to keep track of the Player
's bets: their main bet they make before each hand, and the optional Insurance bet.
public class Player : Person
{
//... Other properties and methods
public decimal Bet { get; set; }
public decimal InsuranceBet { get; set; }
}
We will also want a convenience property to show whether or not the Player
has made an insurance bet:
public class Player : Person
{
//... Other properties and methods
public bool HasInsurance => InsuranceBet > 0M;
}
The Change Amount
After each hand, the player's funds may change based on whether they won or lost the bet. Let's write a property that keeps track of this change amount.
public class Player : Person
{
//... Other properties and methods
public decimal Change { get; set; }
}
Has Stood
We need a convenience property to show whether or not the player has decided to stand:
public class Player : Person
{
//... Other properties and methods
public bool HasStood { get; set; }
}
Payouts
Finally, we need a method for the player to adjust their Funds
property based on whether they won or lost the bet after each hand.
public class Player : Person
{
//... Other properties and methods
public void Collect()
{
Funds += Change;
Change = 0M;
InsuranceBet = 0M;
}
}
Now our Player
object is complete and ready to gamble his/her chips!
Summary
Phew! That was a lot of work. But now our implementation of Blackjack in C# is ready to go, and we can start building the Blazor components which will make up the display area of the game.
We can now build components for:
- The Player and Dealer hands
- The Player and Dealer scores (visible and total)
- The Player's funds and how they change after each hand
- The current bet
- The status of the Player (e.g. whether or not they have stood) AND
- The result of the hand (e.g. whether the Player wins or loses)
Implementing these components and more is exactly what we will do in the next part of this series. Stick around!
Do you have a way you can improve on my design? Submit a pull request or let me know in the comments! I am always looking for input from my dear readers.
Happy Coding!