My in-laws taught me a dice game a couple years ago, and we play every once in a while. A recent excellent answer from @radarbob inspired me to proceed with translating the rules of that dice game into code.
Here's what came out of it:

So it works perfectly, and somehow I'm just as unlucky with this virtual version as in the real-life one (had to click dozens of time to get a freakin' opening roll; by that time my mother-in-law already has thousands of points, every time). At least in this version I can inject some CrookedDie implementation if I want!
I'd like the CalculateRollScore method reviewed, to see what could be improved.
namespace DiceGame
{
public interface IRollScoreRules
{
int CalculateRollScore(IEnumerable<IRollResult<int>> results);
}
public class GameRollScoreRules : IRollScoreRules
{
private IntegerValueDiceHandful _dice;
public GameRollScoreRules(IntegerValueDiceHandful dice)
{
_dice = dice;
}
private int CountNominalValue(IEnumerable<IRollResult<int>> results, int value)
{
return results.Count(e => e.Value == value);
}
public virtual bool IsOpeningRoll(IEnumerable<IRollResult<int>> results)
{
// need minimum 500 points to open score sheet:
return CalculateRollScore(results) >= 500;
}
public virtual int CalculateRollScore(IEnumerable<IRollResult<int>> results)
{
var score = 0;
// if less than 4 1's were rolled, each rolled 1 is 100pts:
score += results.GroupBy(e => e.Value)
.Where(g => g.Key == 1)
.Where(g => g.Count() < 4)
.Sum(g => g.Count() * 100);
// if less than 3 5's were rolled, each rolled 5 is 50pts:
score += results.GroupBy(e => e.Value)
.Where(g => g.Key == 5)
.Where(g => g.Count() < 3)
.Sum(g => g.Count() * 50);
// if more than 3 of anything other than 1 were rolled, determine number of "extra dice":
var extras = results.GroupBy(e => e.Value)
.Where(g => g.Key != 1 && g.Count() > 3)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Count() - 3);
var extraOnes = results.GroupBy(e => e.Value)
.Where(g => g.Key == 1 && g.Count() > 4)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Count() - 4);
// any triplet is 100x nominal value; each extra die is another 100x nominal value:
score += results.GroupBy(e => e.Value)
.Where(g => (g.Key != 1 && g.Count() >= 3))
.Sum(g => (g.Key * 100) + (extras.ContainsKey(g.Key) ? extras[g.Key] : 0) * (g.Key * 100));
score += results.GroupBy(e => e.Value)
.Where(g => (g.Key == 1 && g.Count() >= 4))
.Sum(g => (g.Key * 100) + (extraOnes.ContainsKey(g.Key) ? extraOnes[g.Key] : 0) * (g.Key * 100));
// 4x 1's is 1000x nominal value; each extra die is another 1000x nominal value:
score += results.GroupBy(e => e.Value)
.Where(g => g.Key == 1 && g.Count() >= 4)
.Sum(g => (g.Key * 1000) + (extraOnes.ContainsKey(g.Key) ? extraOnes[g.Key] : 0) * (g.Key * 1000));
return score;
}
}
}