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.

Blackjack in Blazor Part 1 - Rules and Modeling the Game
Let’s practice our modeling skills by breaking down the parts of Blackjack into definable, usable components.
Blackjack in Blazor Part 2 - The C# Classes
Let’s turn our Blackjack in Blazor theoretical models into full-fledged C# classes!
Blackjack in Blazor Part 3 - Game State and Blazor Components
Let’s start building the Blazor components and markup we need to play Blackjack!

As always, there's a sample repository over on GitHub that has all of the code used in this series. Check it out!

exceptionnotfound/BlazorGames
Contribute to exceptionnotfound/BlazorGames development by creating an account on GitHub.

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.

BlazorGames.net
Come play TicTacToe, Minesweeper, ConnectFour and more to learn about Blazor in ASP.NET Core.

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!