Yahtzee, a widely recognized dice game that combines elements of chance and strategy, offers a valuable opportunity to showcase the functionality and elegance of C#’s LINQ (Language Integrated Query) features. This document will examine how LINQ can simplify the evaluation process of Yahtzee dice rolls, transforming complex conditional logic into clear and concise code that effectively conveys the game’s scoring patterns.
Representing Dice Throws
Before we begin working with LINQ operations, it is essential to establish a straightforward representation of a Yahtzee throw. In this game, a player rolls five six-sided dice, allowing us to represent a throw as a collection of integers:
/// <summary>
/// Represents a Yahtzee throw with a list of dice values.
/// </summary>
public class YahtzeeThrow
{
/// <summary>
/// Gets the list of dice values.
/// </summary>
public List<int> Dice { get; }
/// <summary>
/// Initializes a new instance of the <see cref="YahtzeeThrow"/> class with the specified dice values.
/// </summary>
/// <param name="dice">The list of dice values.</param>
/// <exception cref="ArgumentException">Thrown when the number of dice is not exactly 5 or when any dice value is not between 1 and 6.</exception>
public YahtzeeThrow(List<int> dice)
{
if (dice.Count != 5)
{
throw new ArgumentException("A Yahtzee throw must contain exactly 5 dice");
}
if (dice.Any(d => d is < 1 or > 6))
{
throw new ArgumentException("Dice values must be between 1 and 6");
}
Dice = dice;
}
}
Evaluating Scoring Categories with LINQ
Yahtzee features thirteen distinct scoring categories, each necessitating unique patterns of dice. This document explores how LINQ can facilitate the efficient evaluation of these categories.
Upper Section Categories
The upper section of a Yahtzee scorecard awards players for accumulating specific numbers. In each category, ranging from Ones to Sixes, the score is calculated as the sum of the dice that display the corresponding number.
/// <summary>
/// Scores the number of ones in the throw.
/// </summary>
/// <returns>The sum of ones.</returns>
public int ScoreOnes() => Dice.Where(d => d == 1).Sum();
/// <summary>
/// Scores the number of twos in the throw.
/// </summary>
/// <returns>The sum of twos.</returns>
public int ScoreTwos() => Dice.Where(d => d == 2).Sum();
/// <summary>
/// Scores the number of threes in the throw.
/// </summary>
/// <returns>The sum of threes.</returns>
public int ScoreThrees() => Dice.Where(d => d == 3).Sum();
/// <summary>
/// Scores the number of fours in the throw.
/// </summary>
/// <returns>The sum of fours.</returns>
public int ScoreFours() => Dice.Where(d => d == 4).Sum();
/// <summary>
/// Scores the number of fives in the throw.
/// </summary>
/// <returns>The sum of fives.</returns>
public int ScoreFives() => Dice.Where(d => d == 5).Sum();
/// <summary>
/// Scores the number of sixes in the throw.
/// </summary>
/// <returns>The sum of sixes.</returns>
public int ScoreSixes() => Dice.Where(d => d == 6).Sum();
We can enhance this further by implementing a more comprehensive approach:
/// <summary>
/// Scores the upper section for a specified number.
/// </summary>
/// <param name="number">The number to score.</param>
/// <returns>The sum of the specified number.</returns>
/// <exception cref="ArgumentException">Thrown when the number is not between 1 and 6.</exception>
public int ScoreUpperSection(int number) => number is < 1 or > 6
? throw new ArgumentException("Number must be between 1 and 6")
: Dice.Where(d => d == number).Sum();
Lower Section Categories
The lower section categories entail more sophisticated evaluations, focusing on patterns formed by the dice:
Three of a Kind and Four of a Kind
These categories necessitate having at least three or four dice displaying the same value:
/// <summary>
/// Scores the three of a kind category.
/// </summary>
/// <returns>The sum of all dice if there are at least three of the same value, otherwise 0.</returns>
public int ScoreThreeOfAKind() => Dice.GroupBy(d => d).Any(g => g.Count() >= 3)
? Dice.Sum()
: 0;
/// <summary>
/// Scores the four of a kind category.
/// </summary>
/// <returns>The sum of all dice if there are at least four of the same value, otherwise 0.</returns>
public int ScoreFourOfAKind() => Dice.GroupBy(d => d).Any(g => g.Count() >= 4)
? Dice.Sum()
: 0;
Full House
A Full House consists of three dice of one value combined with two dice of another:
/// <summary>
/// Scores the full house category.
/// </summary>
/// <returns>25 points if the throw is a full house, otherwise 0.</returns>
public int ScoreFullHouse()
{
List<int> groups = [.. Dice.GroupBy(d => d).Select(static g => g.Count()).OrderByDescending(c => c)];
return (groups.Count == 2 && groups[0] == 3 && groups[1] == 2) ? 25 : 0;
}
Small Straight and Large Straight
Straights refer to sequences of consecutive dice values:
/// <summary>
/// Scores the small straight category.
/// </summary>
/// <returns>30 points if the throw is a small straight, otherwise 0.</returns>
public int ScoreSmallStraight()
{
List<int> distinctOrdered = [.. Dice.Distinct().Order()];
// Check patterns for small straight (4 consecutive values)
return (distinctOrdered.Count >= 4 && distinctOrdered
.Zip(distinctOrdered.Skip(1), (a, b) => b - a)
.Count(diff => diff == 1) >= 3)
? 30
: 0;
}
/// <summary>
/// Scores the large straight category.
/// </summary>
/// <returns>40 points if the throw is a large straight, otherwise 0.</returns>
public int ScoreLargeStraight()
{
List<int> distinctOrdered = [.. Dice.Distinct().Order()];
return (distinctOrdered.Count == 5 && distinctOrdered.Last() - distinctOrdered.First() == 4)
? 40
: 0;
}
Yahtzee
A Yahtzee is achieved when all five dice display the same value:
/// <summary>
/// Scores the Yahtzee category.
/// </summary>
/// <returns>50 points if all dice have the same value, otherwise 0.</returns>
public int ScoreYahtzee() => Dice.Distinct().Count() == 1 ? 50 : 0;
Chance
The Chance category calculates the total sum of all dice:
/// <summary>
/// Scores the chance category.
/// </summary>
/// <returns>The sum of all dice.</returns>
public int ScoreChance() => Dice.Sum();
Optimal Scoring Strategy
Utilizing LINQ can help identify the most advantageous scoring category for a given throw:
/// <summary>
/// Gets the optimal score and category for the current throw.
/// </summary>
/// <returns>A tuple containing the category and the score.</returns>
public (string Category, int Score) GetOptimalScore()
{
Dictionary<string, int> scores = new()
{
{"Ones", ScoreOnes()},
{"Twos", ScoreTwos()},
{"Threes", ScoreThrees()},
{"Fours", ScoreFours()},
{"Fives", ScoreFives()},
{"Sixes", ScoreSixes()},
{"Three of a Kind", ScoreThreeOfAKind()},
{"Four of a Kind", ScoreFourOfAKind()},
{"Full House", ScoreFullHouse()},
{"Small Straight", ScoreSmallStraight()},
{"Large Straight", ScoreLargeStraight()},
{"Yahtzee", ScoreYahtzee()},
{"Chance", ScoreChance()}
};
return scores.OrderByDescending(static s => s.Value).Select(static s => (s.Key, s.Value)).First();
}
Analyzing Throw Quality
LINQ can be employed to assess the overall quality of a throw:
/// <summary>
/// Analyzes the current throw and provides a recommendation.
/// </summary>
/// <returns>A string containing the analysis and recommendation.</returns>
public string AnalyzeThrow()
{
(string category, int score) = GetOptimalScore();
if (category == "Yahtzee")
{
return "Excellent throw! You rolled a Yahtzee!";
}
if (category is "Large Straight" or "Small Straight" or "Full House")
{
return $"Great throw! Optimal play is {category} for {score} points.";
}
if (category == "Four of a Kind")
{
return $"Good throw! You can score Four of a Kind for {score} points.";
}
int highestDiceCount = Dice.GroupBy(d => d).Max(g => g.Count());
return highestDiceCount <= 2
? "Average throw. Consider re-rolling to build a stronger combination."
: $"Decent throw. Best option is {category} for {score} points.";
}
Evaluating Reroll Strategy
LINQ can also provide recommendations on which dice to retain for a strategic reroll:
/// <summary>
/// Suggests which dice to keep for the next throw.
/// </summary>
/// <returns>A list of dice values to keep.</returns>
public List<int> SuggestDiceToKeep()
{
// Find the most common value
int mostCommonValue = Dice
.GroupBy(d => d)
.OrderByDescending(g => g.Count())
.ThenByDescending(g => g.Key)
.First()
.Key;
// Check for potential straight
List<int> distinctValues = [.. Dice.Distinct().Order()];
bool potentialStraight = distinctValues.Count >= 3 && distinctValues
.Zip(distinctValues.Skip(1), (a, b) => b - a)
.Count(diff => diff == 1) >= 2;
if (potentialStraight)
{
return [.. distinctValues.Where((d, i) => i == 0 || distinctValues[i - 1] == d - 1)];
}
// Default strategy: keep dice of the most common value
return [.. Dice.Where(d => d == mostCommonValue)];
}
Conclusion
The incorporation of LINQ into the Yahtzee scoring process transforms what would typically be complex, conditional code into clear, concise queries. This methodology not only enhances code maintainability and readability but also clarifies the underlying scoring logic.
By leveraging LINQ’s potent query capabilities such as GroupBy, Where, Sum, and OrderBy, we can articulate intricate dice patterns in a declarative manner that aligns with our understanding of Yahtzee combinations. This illustrates how C#’s functional programming features provide elegant solutions for challenges relating to collections and pattern recognition, making it an ideal choice for developing dice games and other applications grounded in probability.
The value of employing LINQ in Yahtzee lies in its ability to enable developers to concentrate on the “what” rather than the “how” regarding dice evaluation, resulting in code that is not only more concise but also better reflects the game’s inherent patterns and strategies.
