In the previous posts in this series, we have built our Blackjack in Blazor demo app up to a point where we have many of the pieces we need to make a functioning implementation. In this post, we're going to finish it off.
Lucky for us, we did the markup and method definitions for the last piece of this implementation, the main Blazor component, in the previous part. Now all we need to do is write the C# code that will make it work.
Catching Up
You'll probably want to read the first three parts of this series before reading this one.
As always, there's a sample repository over on GitHub that has all of the code used in this series. Check it out!
A Quick Recap
Here's the markup for the main Blackjack Blazor component that we coded up in the previous post.
@page "/blackjack"
@using BlazorGames.Models.Blackjack;
@using BlazorGames.Models.Blackjack.Enums;
@using BlazorGames.Pages.Partials;
<PageTitle Title="Blackjack" />
@code {
Dealer dealer = new Dealer(); //Creating a new Dealer also
//creates a new, shuffled CardDeck
Player player = new Player();
GameState state = GameState.NotStarted;
}
<div class="row">
<div class="col-3">
<div>
@{
int cardCount = dealer.Deck.Count + 1;
}
@while (cardCount > 0)
{
<div class="blackjack-drawdeck">
<img src="images/blackjack/cardBack.png" />
</div>
cardCount -= 13;
}
</div>
</div>
<div class="col-3">
<BlackjackHand Cards="dealer.Cards" />
</div>
<div class="col-3">
<BlackjackScore Status="status" Player="dealer" />
</div>
</div>
<div class="row">
<div class="col-3">
<BlackjackFunds Funds="player.Funds" Change="player.Change"/>
</div>
<div class="col-3">
@if (state == GameState.Betting)
{
@if (player.Funds < 10)
{
<span class="display-3 text-danger">Out of money!</span>
}
@if (player.Funds >= 10)
{
<button class="btn btn-primary"
@onclick="(() => Bet(10))">
Bet $10
</button>
}
@if (player.Funds >= 20)
{
<button class="btn btn-primary"
@onclick="(() => Bet(20))">
Bet $20
</button>
}
@if (player.Funds >= 50)
{
<button class="btn btn-primary"
@onclick="(() => Bet(50))">
Bet $50
</button>
}
}
@if (state == GameState.Payout)
{
<BlackjackHandResult Player="player" Dealer="dealer" />
}
@if(state == GameState.Dealing
|| state == GameState.Shuffling
|| state == GameState.InProgress)
{
<BlackjackMessage Status="status" Bet="player.Bet"/>
}
</div>
</div>
<div class="row">
<div class="col-3">
@if (state == GameState.NotStarted || player.Funds < 10)
{
<button class="btn btn-secondary"
@onclick="(() => InitializeHand())">
Start Game
</button>
}
@if (!player.IsBusted
&& state == GameState.InProgress
&& !player.HasStood)
{
<button class="btn btn-primary"
@onclick="(() => Stand())">Stand</button>
<button class="btn btn-primary"
@onclick="(() => Hit())">Hit</button>
@if (player.Score >= 9
&& player.Score <= 11
&& player.Cards.Count == 2
&& player.Funds >= player.Bet * 2)
{
<br />
<button class="btn btn-warning"
@onclick="(() => DoubleDown())">Double Down</button>
}
@if (dealer.HasAceShowing && !player.HasInsurance)
{
<br />
<button class="btn btn-warning"
@onclick="(() => Insurance())">
Insurance ($@(player.Bet / 2))
</button>
}
}
@if (state == GameState.Payout)
{
<button class="btn btn-secondary"
@onclick="(() => NewHand())">Keep Going!</button>
}
</div>
<div class="col-3">
<BlackjackHand Cards="player.Cards" />
</div>
<div class="col-3">
<BlackjackScore Status="status" Player="player"/>
</div>
</div>
Also in the previous post, we defined a set of methods that needed to be implemented:
InitializeHand()
NewHand()
Stand()
Hit()
Insurance()
DoubleDown()
Bet(int amount)
Let's get started implementing those methods!
Useful Custom Method: Delay()
In order to force the application to take "pauses" at certain times, we're going to make use of a special method called Delay()
, which looks like this:
public async Task Delay(int millis)
{
await Task.Delay(millis);
StateHasChanged();
}
The method StateHasChanged()
in Blazor is a special method that tells the renderer to re-render the display. Therefore every time we delay, the display will be re-rendered.
From here on out, the methods we need to implement follow a rough order.
Step 1: Start a New Hand
The method InitializeHand()
is invoked whenever a new hand is started.
In the real world, if the dealer is only playing with one deck of cards, they would need to check if the deck needs to be shuffled, and do so. So this method will also need to do that check, and set the game state to Shuffling
while that is happening. If the deck does not need to be shuffled, then the player will be able to make a bet for the new hand, so the GameState
will need to be set to Betting
.
Here's the code for our method:
public async Task InitializeHand()
{
if (dealer.Deck.Count < 13)
{
state = GameState.Shuffling;
dealer.Deck = new CardDeck();
await Delay(1000);
}
state = GameState.Betting;
}
Step 2: Placing a Bet
Setting the GameState
to Betting
means the buttons to place a bet are available.
The player cannot bet more than they have funds for. After the bet is placed, the dealer can make the initial deal. Our Bet()
method will take these into account, and be written like this:
public async Task Bet(decimal amount)
{
if (player.Funds >= amount)
{
player.Bet += amount;
await Deal();
}
}
Step 3: The Initial Deal
After the bet is placed, the Dealer makes the initial deal by dealing a card to the player first, then a face-down card to him/herself, then another card to the Player, then a face-up card to him/herself. After each card is dealt, we will call StateHasChanged()
so the display will render the dealt card.
Once all the cards are dealt, the GameState
will be set to InProgress
.
public async Task Deal()
{
state = GameState.Dealing;
//Deal a card to each player. The dealer's card is not visible.
await dealer.DealToPlayer(player);
StateHasChanged();
var dealerCard = dealer.Deal();
dealerCard.IsVisible = false;
await dealer.AddCard(dealerCard);
StateHasChanged();
//Deal another card to each player and the dealer;
//these will all be visible.
await dealer.DealToPlayer(player);
StateHasChanged();
await dealer.DealToSelf();
StateHasChanged();
state = GameState.InProgress;
}
Step 3.1: Player Has Blackjack
At this point, there's only one condition in which the player is not offered the choice to stand or hit: if they have a blackjack.
Let's modify the Deal()
method to take this situation into account:
public async Task Deal()
{
//...Rest of implementation
//If the player has a natural blackjack, the hand is over.
if (player.HasNaturalBlackjack)
{
EndHand();
}
}
Step 4: Dealer's Turn
The Dealer does not take their turn until the Player completes theirs. Before we can build out the methods for the Player's actions, we need to have a method for the dealer to take their turn.
In Blackjack, the dealer will always hit if their total score is 16 or less, and will always stand on 17 or more. We can use a recursive method to make the Dealer take a turn until their score is 17 or greater:
public async Task DealerTurn()
{
if(dealer.Score < 17)
{
await dealer.DealToSelf();
StateHasChanged();
await DealerTurn();
}
}
Step 5: Player Actions
When the GameState
is set to InProgress
, the player is now able to choose which action to take. During each hand, they can either hit or stand; in some circumstances, they can use the double down or insurance special plays.
The implementation of the player action methods assumes the existence of a method EndHand()
, which we will implement later.
Let's deal with each of the player actions one at a time.
Step 5.1: Hit
If the player chooses to hit, the Dealer will give them a new card. At that point, if the player is busted, the hand is over. Otherwise, the GameState
does not change; the player may still choose to hit or stand.
public async Task Hit()
{
await dealer.DealToPlayer(player);
if(player.IsBusted)
{
EndHand();
}
}
Step 5.2: Stand
If the player chooses to stand, the Dealer reveals their hidden card and takes their turn. Afterward, the hand is over.
public async Task Stand()
{
player.HasStood = true;
dealer.Reveal();
await DealerTurn();
EndHand();
}
Step 5.3: Double Down
If the player has a 9, 10, or 11 score and exactly two cards, the Double Down special play is available. On this play, the following things happen:
- The player's bet is doubled
- The player gets one additional card, and is forced to stand after that.
This results in a DoubleDown()
method that looks like this:
public async Task DoubleDown()
{
player.HasStood = true;
//The player may only do this if their shown score is 9, 10, or 11.
//If this happens, the player doubles their bet.
player.Bet *= 2;
await Delay(300);
//The player then gets one additional card
await player.AddCard(dealer.Deal());
//At this point, the player is forced to stand.
await Stand();
}
Step 5.4: Insurance
If the dealer's visible card is an Ace, the player make make a special Insurance bet. During this play, the GameState
is set to Insurance
.
If the player chooses to use this play, the insurance bet will be half of the original bet. The dealer will then look at their hidden card. If the hidden card is a ten-card, the player loses their original bet but wins the insurance bet at 2-1; in effect, the player loses no money.
However, if the dealer's hidden card is NOT a ten-card, the player immediately loses their insurance bet, and play continues normally.
public void Insurance()
{
state = GameState.Insurance;
if(dealer.HasAceShowing)
{
//Insurance bet is half the original bet
player.InsuranceBet = player.Bet / 2;
if(dealer.Score == 21) //If the dealer has Blackjack
{
//Reveal the hidden card
dealer.Reveal();
//Pay the player twice the insurance bet
player.Change += player.InsuranceBet * 2;
//Go to the Payout state
status = GameState.Payout;
StateHasChanged();
EndHand();
}
else
{
//Player loses their insurance bet
player.Change -= player.InsuranceBet;
}
}
//Unless the hand is over, play continues normally.
state = GameState.InProgress;
}
Step 6: End Hand and Payouts
We now need a method to handle the end of a hand and payouts or collections. During this time, the GameState
is Payout
.
At the end of a hand of Blackjack, the following things can happen:
- If the player has Blackjack, they get paid 1.5 times their original bet.
- If the player has a higher score and neither is bust, OR the dealer has gone bust, the player wins the bet.
- If the player is bust, OR the dealer has a higher score and neither is bust, the player loses their bet.
- If there's a push, no money changes hands.
In order to determine how to write up this method, let's consider what the "normal" outcome is for the player. To put it simply, the player's normal outcome is a loss. After all, casinos don't really want you to beat them.
So, let's create conditions for a player win or a push, and leave the loss condition as the default.
public void EndHand()
{
state = GameState.Payout;
if (player.HasNaturalBlackjack && dealer.Score != 21)
{
//Player gets their bet back, plus 1.5 * the bet
player.Change += player.Bet * 1.5M;
}
else if (!player.IsBusted && dealer.IsBusted)
{
//If the player is not busted but the dealer is,
//the player gets the amount of their bet back, plus the bet again.
player.Change += player.Bet;
}
else if (!dealer.IsBusted
&& !player.IsBusted
&& player.Score > dealer.Score)
{
//This is a win condition;
//the player has more than the dealer and neither are busted.
player.Change += player.Bet;
}
else if (!dealer.IsBusted
&& !player.IsBusted
&& player.Score == dealer.Score) //Push
{
//If there's a push, no money changes hands
}
else //In all other situations, the player loses their bet.
{
player.Change += player.Bet * -1;
}
//No matter what, the player's bet gets reset,
//and the HasStood flag becomes false
player.Bet = 0;
player.HasStood = false;
}
Note that all of these conditions are modifying the Change
property of the Player
object; the player does not actually collect their winnings or payout their losses here.
Because the GameState
is set to Payout
, the button for "Keep Going" will be visible to the player, and will invoke our last method: NewHand()
.
Step 7: New Hand
This last method needs to do the following things:
- Payout (or collect) the player's bets.
- Reset the player's bets and change values.
- Reset the
GameState
- Call
InitializeHand()
to start a new hand.
Here's the code:
public async Task NewHand()
{
//Payout the player's bets
player.Collect();
//Clear the hands
player.ClearHand();
dealer.ClearHand();
//Reset the game state
state = GameState.NotStarted;
//Start a new hand!
await InitializeHand();
}
Try It Out!
You can play this implementation of Blackjack for yourself right now, over at my sister project site BlazorGames.net.
Or, you can just watch this GIF of me playing Blackjack in Blazor:
Summary
In this final part of our Blackjack in Blazor series, we put all of our C# and Blazor pieces together to make the final, working game.
Got ideas on how to improve this implementation? Can you figure out how to model a split play? I want to hear your suggestions! Leave them in the comments below.
Thanks for playing, and Happy Coding!