Welcome, Dear Readers, to the latest edition of my long-running Modeling Practice series!
The newest game for this series is now available on my sister site BlazorGames.net, and it's a casino favorite: Blackjack!
Blackjack has been a staple of casinos and other gambling parlors since at least 1768, and it remains one of the most popular games today. This is at least partly due to the fact that it is easy to play, can have a considerable amount of strategy, and may even be more winnable than other casino-style games.
Let's model Blackjack as a C# and Blazor WebAssembly program! In the process, we'll discuss how to model complicated real-world scenarios such as this one, what kinds of decisions we need to make, and what sort of compromises we might encounter when trying to make the real world into a computer program.
Rules of Blackjack
If you already know how to play Blackjack, skip to the "Modeling the Game" section below.
Blackjack is a casino-style card game in which the player(s) attempt to beat a dealer's score while getting as close to 21 points as possible, without going over. It is played with a standard four-suit, 52-card deck. Players do not compete against each other.
In order to know how to model a real-world problem as a program, we need to know the rules and boundaries of the problem. Lucky for us, Blackjack has a well-defined set of rules and regulations, and it all starts with betting.
Each player makes a bet before the initial deal. The bet can be any amount up to the amount of money they brought with them to the Blackjack table. This bet is lost if the player loses the hand.
The Initial Deal
After bets are made, the dealer then deals two cards to each player (this is called the initial deal) where each card is worth a certain number of points:
- 2-9 cards are worth the amount shown.
- Ten, Jack, Queen, and King cards are worth 10 points, and are collectively referred to as "ten-cards".
- Aces are worth either 1 or 11 points, at the player's discretion.
The dealer also receives two cards, the first face-down and the second face-up.
Hit, Stand, and Bust
After each player has two cards, each player may choose to either stand or hit. If the player stands, the dealer will not deal them any more cards; the score they have is now their final score for this hand.
If the player chooses to hit, the dealer deals them another card face-up. The player can keep hitting until they decide to stand or they bust, meaning their cards have a value of more than 21. If the player busts, they lose their bet.
The dealer's turn occurs after all players have had their turns. On the dealer's turn, they flip over their face-down card, and then either hit or stand based on the score of their cards.
The dealer will always behave in the same manner. They must hit on all scores of 16 or less, and stand on all scores of 17 or more.
Naturals AKA Blackjack
A "natural", AKA a "blackjack", is an Ace and a ten-card. If a player is dealt a blackjack, it is an automatic win for the player (unless the dealer also has a blackjack) and they receive one-and-a-half times the amount of their bet. So, if the player has bet $20 and is dealt a blackjack, they get their $20 bet back, plus $30 in winnings.
After the dealer has had their turn, the dealer will pay out any players that won their hands, and collect the bets of the players that lost their hands. The payouts and collections are calculated according to these rules:
- As mentioned above, if the player has a blackjack and the dealer does not, the player receives one-and-a-half times their bet.
- If the dealer has a blackjack and the player does not, the player has gone bust, or the dealer's score is higher than the player's score, the player loses their bet.
- If the dealer has gone bust, or the player has a higher score, the player wins their bet (so a bet of $20 returns the bet and gets an additional $20).
- If the dealer and the player have the same score, no money changes hands. This is referred to as a push.
After the payouts and collections are complete, a new hand can begin.
In addition to the "normal" gameplay of Blackjack, there are a few special plays the player can make in certain situations.
If the player, after the initial deal, has 9, 10, or 11 points showing in their hand, they can choose to "double down". This doubles their original bet, and the player receives one additional card. After this, the player is forced to stand.
If the player wins the hand, they get their doubled bet back, plus the doubled bet again. In other words, if the original bet was $20 and the player chooses to double down, their bet increases to $40. If the player wins the hand, they get the $40 bet back, plus an additional $40.
The dealer does not have the option to double down.
If the dealer's face-up card is an Ace after the initial deal, the player may choose to make an insurance bet. This bet is up to half the amount of the original bet, and is placed separately of it.
If the player makes an insurance bet, the dealer looks at the face-down card. If it is a ten-card (meaning the dealer has a blackjack), the dealer flips it over, pays the player twice the insurance bet, and (if the player does not also have a blackjack), collects the player's original bet. In this way, the player is "protected" from the dealer having a blackjack.
To model this, say the original bet was $20 and the dealer is showing an ace. The player can bet up to $10 as an insurance bet. If the dealer has a blackjack, the player "loses" their $20 bet but gains $20 from the insurance bet. In effect, the player loses no money.
However, if the dealer does not have a blackjack when the player makes an insurance bet, the player immediately loses the insurance bet amount, and play continues normally.
Modeling the Game
Before we can begin creating the C# classes and Blazor components necessary to model Blackjack, we must think about the different parts of the game that we need to model.
There are two ways to do this: top-down, and bottom-up. In the top-down method, we would look at the game as a whole and divide it into pieces, which would then be divided into more pieces, until we couldn't make a meaningful division anymore. This method is useful for situations in which you do not already understand or know about each piece.
This entire series will use the bottom-up method. In this method, we look for objects that do not have any dependencies, model them, and them model the classes that rely on them, so on up the chain until we have modeled each object.
Prior to modeling this game in earnest, we must discuss any assumptions that we are making in order to have an implementable and not-too-complex model.
First, we will assume that we only need to model a single player and a dealer. Since the players do not compete against each other, modeling multiple players would model the real-world more accurately, but make the implementation much more complex.
Second, we will assume that our game will only use a single deck of cards, and will reshuffle that deck when needed. In the real world, casinos use many decks of cards shuffled together to prevent card counting, but since this is not the real world, we will conveniently ignore that fact.
With the assumptions made, we can continue with our bottom-up modeling method. In Blackjack, the smallest object with no dependencies is the playing card, so we will start by modeling the cards and the deck they're drawn from.
Cards and the Deck
We must consider the kinds of attributes each playing card will have in order to model them.
Individual cards will each have a suit (e.g. Clubs, Diamonds, etc.) and a value (e.g. Queen, Jack, Six, etc.). For the purposes of Blackjack, the suit doesn't actually matter, but we will include it because looking at a playing card with no suit makes very little sense. Given that there are a known and limited number of suits and values, we will make both the suit and the value into enumerations.
Per the rules of Blackjack, each card has a score. This is different from the value because of ten-cards; a card's value may be a Queen, but its score is still ten points.
In short, the card object will need:
- A suit
- A value
- A score
Now let us consider the deck, which we will treat as a fully separate object and not just a collection.
The deck will need an underlying collection of some kind that keeps all the cards currently in the deck; this could be an array or something more complex. The deck will also need to be able to instantiate itself (i.e. create all the cards it needs and add them to the deck) and shuffle itself.
The dealer will need to interact with the deck, primarily to draw cards from it; we will need a method for that. Since the deck must be able to create and store the cards on instantiation, we'll need a method to add cards to the deck as well.
Therefore, the card deck will need:
- The ability to create all the necessary cards and add them to the deck.
- The ability to shuffle.
- Methods to add cards to and draw cards from the deck.
Dealer and Player Commonalities
One of the ways in which I ask programmers to better understand the problem they are trying to model is to consider two objects and find their commonalities, the things they both need to do.
In this spirit, let's consider the player and the dealer. In many ways, they are the same; they each need a set of cards for their hand, they each need to know and show their score, and they each need to know if they are busted.
So, there will be a common object that both player and dealer can inherit from. We'll call that object Person, and it will need the following abilities:
- Keep a hand of cards
- Use the hand to calculate a score
- Use the hand to determine if they are busted
True Score vs Visible Score
There is one thing that might trip us up here: the dealer's true score (i.e. the combined score of all their cards) and their visible score (the combined score of all face-up cards) are different, and the player can only know about the latter. The true score is a commonality, but the visible score is not. We'll need to deal with that in some way.
We already know that the Dealer object will inherit from the Person object we discussed in the last section. The dealer will need some attributes that are unique to him/her, including:
- The ability to deal cards to themselves and any player.
- The ability to flip over their face-down cards.
We now need to consider the special play called Insurance. In that play, they player can make a special bet if and only if the dealer is showing an Ace face-up. So, the dealer needs one additional property:
- Check if they have an Ace showing face-up.
The game area (which we will discuss later) can check that property to see if the player is allowed to use the Insurance play.
The player, like the dealer, will inherit from the Person object and gain their attributes. They will also have certain attributes unique to them, and first and foremost among these is funds.
Our simulation will assume that each player sits at the Blackjack table with a limited amount of "starter" funds. These funds allow the player to make bets, and are added to or subtracted from when the player wins or loses.
Each player has a bet that is unique to him/her that they make at the start of each hand. The player object must track these bets, as well as the special Insurance bet.
In our simulation, a "bet" is money that has not yet left the Player's funds. It is instead being "risked". So our player will need to track how their funds will change after the current hand, based on whether they win, lose, or push. We will call this the change amount.
At the end of each hand, the player object uses the change amount to determine what their new funds amount is.
Players have the option to "stand", which means they stop drawing more cards. We'll need a property to identify if a player has decided to stand, because at that point it becomes the dealer's turn.
In short, the player object must store:
- Their bets, including Insurance and Double Down bets.
- Their remaining funds.
- The change amount, the amount by which the player's funds will change after the current hand is complete.
- Whether or not the player has stood.
The Game Area
In a real-world game of blackjack, blackjack is played at a specialized table. Here, the dealer is in charge of everything, including whether or not players can use one of the special plays.
In our implementation, we don't want to make the Dealer object in charge of notifying the Player about whether or not they can make a special play, because doing so would require a kind of messaging system between the two objects, and that is too complex for this sort of modeling.
Instead, we will introduce a Game Area object that manages situations such as these. The Game Area will need to know
- What the player's and dealer's scores are.
- What part of the game is currently happening (betting, dealing, hit/stand, etc.).
- What special plays are currently available to the player, if any.
Because getting a Blackjack is a big deal in this game, we will want to output a special message to the display when a Blackjack occurs. Therefore the Game Area will want to know:
- Whether the player or the dealer (or both) has a Blackjack.
In Blackjack, certain things can only happen at certain times. For example, if a player has stood, they are not allowed to hit again on the same hand.
To keep track of the game and what state it is currently in, we will need an enumeration of the possible states. As we code up the C# model, we will determine what game states are needed and define them as members of this enumeration.
What I'm Leaving Out: Splitting
In a real-world game of blackjack, if the player is dealt two cards with the same value (e.g. two sixes, two eights, two Aces, etc.) they can choose to "split" the hand and copy their original bet for the second hand. The two hands are then treated independently, and a player can hit or stand or use special plays on each of them. Payouts are also dealt with separately.
I am leaving this out because modeling this tended to break my design in ways I couldn't resolve nicely. I'm still working on how to model this efficiently and in a way that I can explain simply, which doesn't involve duplicating all the affected attributes and properties or making a collection of collections. If I do get to that point, there will be another blog post about it.
The model for our Blazor implementation of Blackjack is complete, and here are the objects we need to code:
Card, including suits and values
- The game status
- The game area
The objects for the last two items are not clearly defined yet. As we build our C# model, we will determine how best to implement those two objects.
Did I miss something? Is there a better way of modeling Blackjack in C# and Blazor? Or can you make my implementation better? I want to know about all these things! Sound off in the comments below.
In the next part of this series, we will code up the C# model for Blackjack, including each of the objects above and how they interact. Card counters, get ready for some action!