We built the C# model in the last post, so let's use those classes to make a working Yahtzee game!

The Sample Project and Other Games

As always, there's a sample project on GitHub with the code used in this post. Unlike most others, though, this project is the real code running on the production site! Check it out:

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

Also check out the other games I've built in Blazor WebAssembly:

Starting the Component

Since we've already got the C# backend model written up, we can start building a basic Blazor Component:

@page "/yahtzee"

@code {

}

The component will need to be aware of many things in order to simulate a game of Yahtzee. This includes:

  • The dice collection we created in Part 1,
  • The play collection (scorecard), also created in Part 1,
  • The number of turns remaining,
  • The number of rolls remaining on the given turn
  • The current total score

From these, we can add the following properties (including a few calculated properties) to our Blazor component:

@page "/yahtzee"

@code {
    public DieCollection Dice { get; set; } = new DieCollection();

    public PlayCollection PlaysMade { get; set; } = new PlayCollection();

    public int TurnsRemaining { get; set; } = 13;

    public int RollsRemaining { get; set; } = 3;

    public bool IsStartOfTurn { get { return RollsRemaining >= 3; } }

    public bool IsGameOver { get { return TurnsRemaining <= 0; } }

    public int TotalScore
    {
        get
        {
            if (PlaysMade.HasBonus())
            {
                return PlaysMade.GetTotal() + 35;
            }
            return PlaysMade.GetTotal();
        }
    }
}

Initializing a Game

In order to play a game of Yahtzee, you must first have all your materials: the dice and the scorecard.

@code {
    public Yahtzee()
    {
        Initialize();
    }

    public void Initialize()
    {
        Dice.Reset();

        PlaysMade.Reset();
        TurnsRemaining = 13;
        RollsRemaining = 3;
    }
}
From here on out, all code samples are shortened for brevity.

Rolling the Dice

In the previous part of this series, we defined a Die class and a DieCollection class. Now, we need to make use of methods in those classes to simulate rolling the dice.

@code {
    public void RollDice()
    {
        Dice.Roll();
        RollsRemaining--;
    }
}

Each time we roll the dice, there are less rolls remaining on this turn.

We also need to display the dice to the user. For each die, the user should be able to click on that die to "hold" it, or prevent the die from rolling. The markup required for this look like:

@for (int i = 0; i < Dice.Dice.Count; i++)
{
    var x = i;
    var die = Dice.Dice[x];
    <div class="row">
        <div class="col">
            @if (IsStartOfTurn)
            {
                @*At the start of the turn, no die can be held.*@
                <i class="fas fa-5x [email protected]()"></i>
            }
            else if (Dice.Dice[x].IsHeld)
            {
                @*If the die is held, give it an extra style*@
                <i class="fas fa-5x [email protected]() text-primary" @onclick="@(() => die.Hold())"></i>
            }
            else
            {
                <i class="fas fa-5x [email protected]()" @onclick="@(() => die.Hold())"></i>
            }
        </div>
    </div>
}
Remember that we defined the Hold() method as part of the Die class.

Finally, let's implement a button that will roll any dice that are not currently held. This button will also reset the game if the game is over, and must disable itself if the user has used up all their rolls but not yet marked a score.

Said markup looks like this:

<div class="row">
    <div class="col">
        @if (RollsRemaining > 0 && !IsGameOver)
        {
            <button class="btn btn-primary" @onclick:preventDefault @onclick="@(() => RollDice())">Roll</button>
        }
        else if (IsGameOver)
        {
            <button class="btn btn-success" @onclick="@(() => Initialize())">Reset</button>
            <text>Thanks for playing!<br />Final score: @TotalScore</text>
        }
        else
        {
            <button class="btn btn-primary disabled" @onclick:preventDefault @onclick="@(() => RollDice())">Roll</button><text>Please mark a score.</text>
        }
    </div>
</div>

Starting the Next Turn

But wait, how exactly are we going to mark a score? We'll solve that problem, but first, let's implement a quick method to start the next turn.

Said method looks like this:

@code {
    public void NextTurn()
    {
        RollsRemaining = 3;
        TurnsRemaining--;
        Dice.ReleaseHold();
    }
}

We will make heavy use of this method while writing the code to mark the scorecard. Speaking of which...

Marking a Score

In Yahtzee, each play type (e.g. three 1s, three 2s, small straight, chance, full house, etc.) can only be played once. At that time, if the conditions are met, the player can mark down the points they get from that play. If the conditions are not met, the player can "scratch" the play, or mark it as zero points.

We need a method to handle the "scratch" possibility:

@code {
    public void ScratchPlay(PlayType type)
    {
        PlaysMade.Add(type, 0);
        NextTurn();
    }
}

We will need to display an option to "scratch" on any play that doesn't already have a score whenever the player is out of rolls for the current turn. We'll handle that during the "Building the Scorecard" section below.

The more complicated problem we have to solve is this: how do we display to the user which plays they can mark a score for? Remember how in the previous part we built a bunch of methods on the DieCollection class that would check the dice to see if each play type was available? Those methods are going to come in handy now, along with a rather large switch() statement:

@code {
    public bool CanMakePlay(PlayType type)
    {
        switch (type)
        {
            case PlayType.Ones:
                return Dice.HasThreeOnes();

            case PlayType.Twos:
                return Dice.HasThreeTwos();

            case PlayType.Threes:
                return Dice.HasThreeThrees();

            case PlayType.Fours:
                return Dice.HasThreeFours();

            case PlayType.Fives:
                return Dice.HasThreeFives();

            case PlayType.Sixes:
                return Dice.HasThreeSixes();

            case PlayType.Yahtzee:
            case PlayType.BonusYahtzee:
                return Dice.HasYahtzee();

            case PlayType.ThreeOfAKind:
                return Dice.HasThreeOfAKind();

            case PlayType.FourOfAKind:
                return Dice.HasFourOfAKind();

            case PlayType.FullHouse:
                return Dice.HasFullHouse();

            case PlayType.SmallStraight:
                return Dice.HasSmallStraight();

            case PlayType.LargeStraight:
                return Dice.HasLargeStraight();

            case PlayType.Chance:
                return true;

            default: return false;
        }
    }
}

But there's a big difference between checking if we can make a play and actually making that play. We also need a method to mark down the score for a play, and then start the next turn:

@code {
    public void ClaimPlay(PlayType type)
    {
        switch (type)
        {
            case PlayType.Ones:
                PlaysMade.Add(type, Dice.GetSumOf(1));
                break;

            case PlayType.Twos:
                PlaysMade.Add(type, Dice.GetSumOf(2));
                break;

            case PlayType.Threes:
                PlaysMade.Add(type, Dice.GetSumOf(3));
                break;

            case PlayType.Fours:
                PlaysMade.Add(type, Dice.GetSumOf(4));
                break;

            case PlayType.Fives:
                PlaysMade.Add(type, Dice.GetSumOf(5));
                break;

            case PlayType.Sixes:
                PlaysMade.Add(type, Dice.GetSumOf(6));
                break;

            case PlayType.Yahtzee:
                PlaysMade.Add(type, 50);
                break;

            case PlayType.BonusYahtzee:
                PlaysMade.Add(type, 100);
                break;

            case PlayType.ThreeOfAKind:
                PlaysMade.Add(type, Dice.GetOfAKindTotal(3));
                break;

            case PlayType.FourOfAKind:
                PlaysMade.Add(type, Dice.GetOfAKindTotal(4));
                break;

            case PlayType.FullHouse:
                PlaysMade.Add(type, 25);
                break;

            case PlayType.SmallStraight:
                PlaysMade.Add(type, 30);
                break;

            case PlayType.LargeStraight:
                PlaysMade.Add(type, 40);
                break;

            case PlayType.Chance:
                PlaysMade.Add(type, Dice.Dice.Sum(x => x.Value));
                break;
        }

        NextTurn();
    }
}

Building the Scorecard

Since we've defined the play types as enumerations, we can complete the last part of our markup to output a row for each play type.

However, we also need to output three additional rows:

  • One row for the "top section bonus" that appears if the total score of the "three numbers" plays is more than 63.
  • One row for the grand total score.
  • One row for the "bonus Yahtzee" situation that only appears if the player already has a Yahtzee.

We will also display the turns remaining and rolls remaining as part of the scorecard to complete our UI. Here's that markup:

<div class="row">
    <div class="col">
        <h4>Scorecard</h4>
    </div>
</div>
<div class="row">
    <div class="col-4">
        Rolls Remaining This Turn: @RollsRemaining
    </div>
    <div class="col-4">
        Turns Remaining: @TurnsRemaining
    </div>
</div>
<!-- Scorecard rows -->
@foreach (PlayType type in (PlayType[])Enum.GetValues(typeof(PlayType)))
{
    @if (type != PlayType.BonusYahtzee 
         || (type == PlayType.BonusYahtzee && PlaysMade.HasYahtzee()))
    {
        <div class="row @(type == PlayType.BonusYahtzee ? "bg-warning" : "")">
            <div class="col-6 border border-dark font-weight-bold">
                @type.GetDisplayName()
                <!--Here we use the custom NameAttribute to output
					descriptions of the play types -->
                <i class="fa fa-question-circle" 
                   title="@type.GetDisplayDescription()"></i>
            </div>
            <div class="col-6 border border-dark p-1 min">
                @if (PlaysMade.HasPlay(type))
                {
                    @PlaysMade.GetScore(type)
                }
                else if (CanMakePlay(type) && !IsStartOfTurn)
                {
                    <a class="btn btn-primary btn-sm" @onclick="@(() => ClaimPlay(type))">Claim</a>
                }
                else if (RollsRemaining == 0)
                {
                    <a class="btn btn-outline-secondary btn-sm" 
                       @onclick="@(() => ScratchPlay(type))">Scratch</a>
                }
            </div>
        </div>
    }
}
<div class="row">
    <div class="col-6 border border-dark font-weight-bold">
        TOP SECTION BONUS 
        <i class="fa fa-question-circle" 
           title="If your scores for ones, twos, threes, fours, fives, and sixes total 63 or more, you get a 35 point bonus!"></i>
    </div>
    <div class="col-6 border border-dark p-1">
        @if (PlaysMade.HasBonus())
        {
            <text>35</text>
        }
        else
        {
            <text>0</text>
        }
    </div>
</div>
<div class="row">
    <div class="col-6 border border-dark font-weight-bold">GRAND TOTAL</div>
    <div class="col-6 border border-dark p-1">
        @TotalScore
    </div>
</div>

Whew! That was a long one.

Check Out BlazorGames.net!

This code is running right now on the BlazorGames.net site. Check it out!

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

Also, if you think you can improve on my implementation, please feel free to leave a pull request on the GitHub repo!

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

I Want Your Opinions!

We made the classic dice game Yahtzee using C#, ASP.NET Core, and Blazor WebAssembly! Hopefully it wasn't too difficult to follow along with these tutorials.

Did you see an error, or something you think I should know about? Got an idea about how to improve this implementation? Share in the comments below!

Wanna see more board games turned into programming projects? Please consider buying me a coffee. Your support helps me get these kinds of projects created, and keep ads off this site. Thanks!

Matthew Jones
Iā€™m a .NET developer, blogger, speaker, husband and father who just really enjoys the teaching/learning cycle.One of the things I try to do differently with Exception...

Happy Coding!