2 of 6 added 6 characters in body

Dice game rules implementation review

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:

enter image description here enter image description here enter image description here

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;
        }
    }
}