I've been on a Blazor kick lately, building well-known board and card games in C#, ASP.NET Core, and Blazor WebAssembly, including:

Now it's time for a new one: the dice game Yahtzee!

That's a pretty good four-of-a-kind score! Image found on Wikimedia, used under license.

In this post, we're going to build the C# model for running a game of Yahtzee; in the next, we'll use that model to build a Blazor Component and then actually run the game in our browsers!

Ready? Let's roll!

Goals

To start off, let's define what we want our Yahtzee game to be able to do when we are done building it. We want it to:

  • Keep track of all remaining unclaimed plays.
  • Automatically highlight plays that are available to be scored.
  • Ensure that the player only takes three rolls per turn.
  • Track the number of turns remaining (there are 13 turns in a game).
  • Force the user to "scratch" or mark as zero a play when they cannot claim any of the remaining plays.
  • Enforce as many rules of Yahtzee as possible.

Our model is going to diverge from a real-world game of Yahtzee in one specific way: we will only keep track of scores for a single player, whereas a real game of Yahtzee could have many players.

The Plays

We can now begin our modeling of Yahtzee in C# and Blazor; let's first take a look at the scorecard that comes with a base game.

Yahtzee is a game defined by the kinds of plays you can make; each play requires the dice to have certain values.

For the "upper section" of the scorecard, there are the "three numbers" plays. For these plays, the player scores if three or more of the given number is showing. That means we have six possible plays:

  • Three 1s
  • Three 2s
  • Three 3s
  • Three 4s
  • Three 5s
  • Three 6s

Each of these plays is worth the combined total of the numbers shown. For example:

is worth 6 + 6 + 6 + 6 = 24 points.

There's also a "bonus" row that works like this: is the sum of all those plays is greater than or equal to 63, the player gets a 35-point bonus. We'll be implementing the bonus calculation in Part 2 of this series.

The lower section of the scorecard contains various plays the user can score, each worth a specified number of points. They include:

  • 3 of a kind; worth the total of that number (e.g. three 4s is worth 3 x 4 = 12 points).
  • 4 of a kind; worth the total of that number.
  • Full House; three of one number and two of another (e.g. three 5s and two 1s), worth 25 points.
  • Small Straight; four sequential numbers (e.g. 2-3-4-5), worth 30 points.
  • Large Straight; five sequential numbers (e.g. 2-3-4-5-6), worth 40 points.
  • The titular Yahtzee; all five dice show the same number, worth 50 points.
  • Chance; any possible numbers, worth the sum of all dice.

There's also one additional mechanic: a Yahtzee thrown after a Yahtzee is already scored is worth 100 points!

Modeling the Play Types

Given that there's many different kinds of plays in Yahtzee, and that (excepting the Bonus Yahtzee special case) each play type can only be used once per game, we can model the play types as enumerations in C#, with a special attribute:

public enum PlayType
{
    [Name("Three 1s", "Roll at least three 1s, score the total of the 1s.")]
    Ones,

    [Name("Three 2s", "Roll at least three 2s, score the total of the 2s.")]
    Twos,

    [Name("Three 3s", "Roll at least three 3s, score the total of the 3s.")]
    Threes,

    [Name("Three 4s", "Roll at least three 4s, score the total of the 4s.")]
    Fours,

    [Name("Three 5s", "Roll at least three 1s, score the total of the 5s.")]
    Fives,

    [Name("Three 6s", "Roll at least three 1s, score the total of the 6s.")]
    Sixes,

    [Name("Yahtzee!", "Roll all five dice as the same number, score 50.")]
    Yahtzee,

    [Name("Three of a Kind", "Roll at least 3 of the same number, score the total of that number.")]
    ThreeOfAKind,

    [Name("Four of a Kind", "Roll at least 4 of the same number, score the total of that number.")]
    FourOfAKind,

    [Name("Small Straight", "Roll four dice in sequential order (e.g. 2-3-4-5), score 30.")]
    SmallStraight,

    [Name("Large Straight", "Roll five dice in sequential order (e.g. 2-3-4-5-6), score 40.")]
    LargeStraight,

    [Name("Full House", "Roll three of one number and two of another (e.g. 5-5-5-2-2), score 25.")]
    FullHouse,

    [Name("Chance", "Score tbe total of all five dice.")]
    Chance,

    [Name("Bonus Yahtzee!", "Worth 100 points! Only available if you already have a Yahtzee!")]
    BonusYahtzee
}

The Name attribute is a class I defined for this implementation; it looks like this:

public class NameAttribute : Attribute
{
    public string Name { get; private set; }
    public string Description { get; private set; }

    public NameAttribute(string name, string description)
    {
        this.Name = name;
        this.Description = description;
    }
}
This will come into play when we need to output the play descriptions on the Blazor component.

Defining a Play

We can now use a C# class to define what exactly a play is comprised of. In Yahtzee, plays are relatively simple; they only consist of a score and a play type. Consequently our C# class look like this:

public class Play
{
    public int PointValue { get; set; }
    public PlayType Type { get; set; }

    public Play(PlayType type, int points)
    { 
        Type = type;
        PointValue = points;
    }
}

The complexities, such as they are, start arriving when we begin to consider how to deal with plays in aggregate.

Modeling the Plays

See, in Yahtzee, once you have written a score for a given play, you cannot use that play again. If you have already marked 40 points for your Large Straight and you roll 1-2-3-4-5, you cannot mark another 40 points. So, we need a "container" object that will moderate the plays and ensure that a single play is only over marked once.

We are calling that class, unsurprisingly, PlayCollection.

PlayCollection is going to need a list of the "marked" plays and their scores, as well as accessor methods that can:

  • Keep track of all plays that have been scored
  • Retrieve an individual play's score
  • Check if the player has gotten the "top section bonus"
  • Get the total score for the player AND
  • Reset itself so that a new game can be played.

Our class can do all of these things:

public class PlayCollection
{
    public List<Play> Plays { get; set; } = new List<Play>();

    public bool HasPlay(PlayType type)
    {
        return Plays.Select(x => x.Type).Contains(type);
    }

    public bool HasYahtzee()
    {
        return Plays.Any(x => x.PointValue > 0 && x.Type == PlayType.Yahtzee);
    }

    public bool HasBonus()
    {
        return (GetScore(PlayType.Ones)
                + GetScore(PlayType.Twos)
                + GetScore(PlayType.Threes)
                + GetScore(PlayType.Fours)
                + GetScore(PlayType.Fives)
                + GetScore(PlayType.Sixes)) > 63;
    }

    public void Add(PlayType type, int value)
    {
        Plays.Add(new Play(type, value));
    }

    public int GetScore(PlayType type)
    {
        var matchingPlay = Plays.FirstOrDefault(x => x.Type == type);
        if(matchingPlay != null)
        {
            return matchingPlay.PointValue;
        }

        return 0;
    }

    public int GetTotal()
    {
        return Plays.Sum(x => x.PointValue);
    }

    public void Reset()
    {
        Plays.Clear();
    }
}

Great! Now we can keep track of our plays and scores. But we've been neglecting the other half of the equipment you need to play a game of Yahtzee: the dice.

Modeling the Dice

For each "turn" in Yahtzee, you get three rolls. For each roll, you can choose which of the five dice you want to roll; dice which are not being rolled are "held".

Therefore the class which represents a single die must be able to be "held", in addition to having a value from 1 to 6. Our class will be the following:

public class Die
{
    public int Value { get; set; } = 1;

    public bool IsHeld { get; set; }

    public Die(int value)
    {
        Value = value;
    }

    public void Hold()
    {
        IsHeld = !IsHeld;
    }
}
Code shortened for brevity; full code available on GitHub.

Just like with Play and PlayCollection, the modeling gets interesting when we start having to consider multiple dice. Our DieCollection class needs to be able to roll, hold, and manage the dice classes. Hence, our class starts out like this:

public class DieCollection
{
    public List<Die> Dice { get; set; } = new List<Die>();

    public void Add(int value)
    {
        Dice.Add(new Die(value));
    }

    public void Roll()
    {
        Random rand = new Random();
        foreach(var die in Dice)
        {
            if(!die.IsHeld)
                die.Value = rand.Next(1, 7);
        }
    }

    public void ReleaseHold()
    {
        Dice.ForEach(x => x.IsHeld = false);
    }

    public void Reset()
    {
        Dice.Clear();
        Add(1);
        Add(2);
        Add(3);
        Add(4);
        Add(5);
    }

    public int GetSumOf(int value)
    {
        return Dice.Where(x => x.Value == value).Sum(x => x.Value);
    }
}

However, one of our goals from the beginning of this post was this:

  • Automatically highlight plays that are available to be scored.

We therefore need our DieCollection class to be able to identify which plays a given set of dice can make.

Identifying Available Plays

If you parse out what each available play actually means, you find that a lot of them use the same logic. All of the "three numbers" and the three-of-a-kind plays are the same kind of play, except that the former expects a specific number.

Three Numbers and Three-Of-A-Kind

We can therefore have a method which detects three-of-a-kind for a specific number:

public class DieCollection
{
    //...Other methods

    public bool HasThreeOfAKind(int value)
    {
        return Dice.Where(x => x.Value == value).Count() >= 3;
    }
}

From this method, we can generate methods which detect if the "three numbers" plays are available:

public class DieCollection
{
    //...Other methods
    
    public bool HasThreeOnes()
    {
        return HasThreeOfAKind(1);
    }

    public bool HasThreeTwos()
    {
        return HasThreeOfAKind(2);
    }

    public bool HasThreeThrees()
    {
        return HasThreeOfAKind(3);
    }

    public bool HasThreeFours()
    {
        return HasThreeOfAKind(4);
    }

    public bool HasThreeFives()
    {
        return HasThreeOfAKind(5);
    }

    public bool HasThreeSixes()
    {
        return HasThreeOfAKind(6);
    }

    public bool HasThreeOfAKind()
    {
        return HasThreeOnes()
            || HasThreeTwos()
            || HasThreeThrees()
            || HasThreeFours()
            || HasThreeFives()
            || HasThreeSixes();
    }
}

Four-Of-A-Kind

The logic for the four-of-a-kind play is very similar:

public class DieCollection
{
    //...Other methods

    public bool HasFourOfAKind(int value)
    {
        return Dice.Where(x => x.Value == value).Count() >= 4;
    }
    
    public bool HasFourOfAKind()
    {
        return HasFourOfAKind(1)
            || HasFourOfAKind(2)
            || HasFourOfAKind(3)
            || HasFourOfAKind(4)
            || HasFourOfAKind(5)
            || HasFourOfAKind(6);
    }
}

Getting the Score for the Three Numbers Plays

When making the score for any of the three-numbers plays, we need to sum those numbers. This results in a method that looks like this:

public class DieCollection
{
    //...Other Methods
    
    public int GetSumOf(int value)
    {
        return Dice.Where(x => x.Value == value).Sum(x => x.Value);
    }
}

Getting the Score for the Of-A-Kind Plays

Getting the score for the three-of-a-kind and four-of-a-kind plays is trickier; we don't know which number is being use for the of-a-kind play, so we have to find it.

public class DieCollection
{
    //...Other methods
    
    public int GetOfAKindTotal(int count) //Count will be 3 or 4
    {
        var groups = Dice.GroupBy(x => x.Value)
                         .Select(group => new
                         {
                             Value = group.Key,
                             Count = group.Count()
                         })
                         .OrderByDescending(x => x.Count);

        var mostCommonValue = groups.First().Value;

        if (Dice.Where(x => x.Value == mostCommonValue).Count() >= count)
        {
            return Dice.Where(x => x.Value == mostCommonValue)
                       .Sum(x => x.Value);
        }
        else return 0;
    }
}

Full House

Now we can try to find the Full House play. The definition of a Full House is that there are three of one number and two of another. For example, any of these are a full house:

If we use LINQ to group the dice by value, we can say that:

  1. There must be exactly two groups of numbers AND
  2. One of the groups must have exactly three dice, and the other must have exactly two.

This logic results in a method that looks like this:

public class DieCollection
{
    //...Other methods
    
    public bool HasFullHouse()
    {
        var values = Dice.GroupBy(x => x.Value)
                         .Select(group => new
                         {
                             Value = group.Key,
                             Count = group.Count()
                         })
                         .OrderByDescending(x => x.Count);

        if (values.Count() != 2) return false;

        if (values.ElementAt(0).Count == 3
            && values.ElementAt(1).Count == 2)
            return true;

        return false;
    }
}

Small and Large Straights

For small straights, there are only three possible combinations of numbers:

For large straights, there are only two:

Because of this fact, we can cheat a bit when creating the methods to search for the straights plays.

public class DieCollection
{
    //...Other methods
    
    public bool HasSmallStraight()
    {
        var hasOne = Dice.Exists(x => x.Value == 1);
        var hasTwo = Dice.Exists(x => x.Value == 2);
        var hasThree = Dice.Exists(x => x.Value == 3);
        var hasFour = Dice.Exists(x => x.Value == 4);
        var hasFive = Dice.Exists(x => x.Value == 5);
        var hasSix = Dice.Exists(x => x.Value == 6);

        if (hasOne && hasTwo && hasThree && hasFour) return true;
        if (hasTwo && hasThree && hasFour && hasFive) return true;
        if (hasThree && hasFour && hasFive && hasSix) return true;

        return false;
    }

    public bool HasLargeStraight()
    {
        var hasOne = Dice.Exists(x => x.Value == 1);
        var hasTwo = Dice.Exists(x => x.Value == 2);
        var hasThree = Dice.Exists(x => x.Value == 3);
        var hasFour = Dice.Exists(x => x.Value == 4);
        var hasFive = Dice.Exists(x => x.Value == 5);
        var hasSix = Dice.Exists(x => x.Value == 6);

        if (hasOne && hasTwo && hasThree && hasFour && hasFive) return true;
        if (hasTwo && hasThree && hasFour && hasFive && hasSix) return true;

        return false;
    }
}

There are absolutely more concise ways to do this, and if you have one, I want to hear about it in the comments below!

Chance

We don't need a method for the Chance play, since it is just the sum of all the dice.

Yahtzee!

There's only one kind of play left: the glorious Yahtzee!

public class DieCollection
{
    //...Other methods
    
    public bool HasYahtzee()
    {
        var value = Dice.First().Value;

        return Dice.All(x => x.Value == value);
    }
}

Model Complete!

Phew! With all of that coding, we have completed the "back-end" of our Yahtzee game in Blazor WebAssembly. But now we have to put it all together in a Blazor Component. Stick around for the second and final part of this series!

The beginning of a beautiful friendship. Photo by Brett Jordan / Unsplash

Don't forget to check out the sample project over on GitHub!

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

If this post helped you, or was interesting, informative, or useful to you, please consider buying me a coffee. Your support helps me get these kinds of projects done, 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!