Sunday, April 17, 2011

Greed Kata–Second attempt

When I wrote the post on my first attempt, I mentioned that I thought I was “missing something obvious.” It occurred to me after I put up that post.

A die can only be used in one scoring combination

This is probably obvious to everyone else but I’m only responsible for what goes on in my head.

This realization push me to think that calculating the score using the Pipes and Filters pattern would probably be a good fit. I started with the data that each scorer, the filter, would operate on:

   1: public class GameContext
   2:  {
   3:      public int Score;
   4:      public int[] DiceValues;
   5:  }

Each filter would use the die values stored in the DiceValues field and update the Score field.


For the scorers, I started with the single value rules:



A single one (1) is worth 100 points.
A single five (5) is worth 50 points.


Working through lead to the following:



   1: public class ValueScorer
   2: {
   3:     private readonly int _die;
   4:     private readonly int _value;
   5:  
   6:     public ValueScorer(int die, int value)
   7:     {
   8:         _die = die;
   9:         _value = value;
  10:     }
  11:  
  12:     public GameContext Compute(GameContext context)
  13:     {
  14:         if (0 == context.DiceValues.Length) return context;
  15:         var occurances = Array.FindAll(context.DiceValues, v => v== this._die).Length;
  16:         context.Score += occurances * this._value;
  17:  
  18:         List<int> diceValues = RemoveValuesUsedInAScoringCombination(context.DiceValues, occurances);
  19:         context.DiceValues = diceValues.ToArray();
  20:  
  21:         return context;
  22:     }
  23:  
  24:     private List<int> RemoveValuesUsedInAScoringCombination(int[] currentDiceValues, int occurances)
  25:     {
  26:         var diceValues = new List<int>(currentDiceValues);
  27:         foreach (var ii in Enumerable.Range(1, occurances))
  28:         {
  29:             diceValues.Remove(this._die);
  30:         }
  31:         return diceValues;
  32:     }
  33: }


The next set of rules:


A set of three ones (1) is worth 1000 points
A set of three of any other number is worth 100 time that number (ex. {2,2,2} = 200 points}.

For which I ended up with:

 


   1: public class TripleScorer
   2: {
   3:     public GameContext Compute(GameContext context)
   4:     {
   5:         if (0 == context.DiceValues.Length) return context;
   6:  
   7:         var triples = context.DiceValues.GroupBy(d => d).Where(g => g.Count() >= 3);
   8:  
   9:         context.Score += triples.Sum(g => (g.Count() / 3) * ((1 == g.Key) ? 1000 : g.Key * 100));
  10:  
  11:         List<int> diceValues = RemoveValuesUsedInAScoringCombination(context.DiceValues, triples);
  12:         context.DiceValues = diceValues.ToArray();
  13:  
  14:         return context;
  15:     }
  16:  
  17:     private List<int> RemoveValuesUsedInAScoringCombination(int[] currentDiceValues, IEnumerable<IGrouping<int, int>> triples)
  18:     {
  19:         var diceValues = new List<int>(currentDiceValues);
  20:         foreach (var tripleGroup in triples)
  21:         {
  22:             int digitsToRemove = (tripleGroup.Count()/3)*3;
  23:             foreach (var ii in Enumerable.Range(1,digitsToRemove))
  24:             {
  25:                 diceValues.Remove(tripleGroup.Key);
  26:             }
  27:         }
  28:         return diceValues;
  29:     }
  30: }


The method RemoveValuesUsedInAScoringCombination() is key to making this approach work. This method updates the DiceValues array in the context to remove the values that were used. This piece enforces the constraint that a die is only used once in a scoring combination.


The driver is really straight forward and look like:



   1: public class Scorer
   2: {
   3:     public int Computer(int[] diceValues)
   4:     {
   5:         if (0 == diceValues.Length) return 0;
   6:  
   7:         var tripleScorer = new TripleScorer();
   8:         var oneScorer = new ValueScorer(1,100);
   9:         var fiveScorer = new ValueScorer(5,50);
  10:  
  11:         var context = new GameContext() {DiceValues = diceValues, Score = 0};
  12:  
  13:         context = tripleScorer.Compute(context);
  14:         context = oneScorer.Compute(context);
  15:         context = fiveScorer.Compute(context);
  16:  
  17:         return context.Score;
  18:     }
  19: }

What I find really nice about this approach is that adding additional scoring combinations is just a matter of creating another scorer. For example, say that the ruling body for the game Greed introduces the following rule:



A set of five consecutive numbers (ex. {1,2,3,4,5} or {2,3,4,5,6}) is called a Straight and is worth 2000 points.


This is easily implemented with a Scorer such as:



   1: public class StraightScorer
   2: {
   3:     public GameContext Compute(GameContext context)
   4:     {
   5:         if (0 == context.DiceValues.Length) return context;
   6:  
   7:         foreach (int startingAt in Enumerable.Range(1,2))
   8:         {
   9:             if (this.DiceValuesContainStraight(startingAt, context.DiceValues))
  10:             {
  11:                 context.Score += 2000;
  12:                 context.DiceValues = this.RemoveValuesUsedInAScoringCombination(context.DiceValues, startingAt).ToArray();
  13:             }
  14:         }
  15:  
  16:         return context;
  17:     }
  18:  
  19:     private bool DiceValuesContainStraight(int straightStartsAt, int[] diceValues)
  20:     {
  21:         foreach (var n in Enumerable.Range(straightStartsAt,5))
  22:         {
  23:             if (!diceValues.Contains(n)) return false;
  24:         }
  25:         return true;
  26:     }
  27:  
  28:     private List<int> RemoveValuesUsedInAScoringCombination(int[] currentDiceValues, int startingAt)
  29:     {
  30:         var diceValues = new List<int>(currentDiceValues);
  31:         foreach (var n in Enumerable.Range(startingAt, 5))
  32:         {
  33:             diceValues.Remove(n);
  34:         }
  35:         return diceValues;
  36:     }
  37:  
  38: }

The driver gets modified as:



   1: public class Scorer
   2: {
   3:     public int Computer(int[] diceValues)
   4:     {
   5:         if (0 == diceValues.Length) return 0;
   6:  
   7:         var straightScorer = new StraightScorer();
   8:         var tripleScorer = new TripleScorer();
   9:         var oneScorer = new ValueScorer(1,100);
  10:         var fiveScorer = new ValueScorer(5,50);
  11:  
  12:         var context = new GameContext() {DiceValues = diceValues, Score = 0};
  13:  
  14:         context = straightScorer.Compute(context);
  15:         context = tripleScorer.Compute(context);
  16:         context = oneScorer.Compute(context);
  17:         context = fiveScorer.Compute(context);
  18:  
  19:         return context.Score;
  20:     }
  21: }

Modifying the previous version would not have been nearly as simple.

Wednesday, April 13, 2011

Greed Kata–First attempt

I’ve had this on my TODO list since this year’s CodeMash conference. If you’re not already familiar with the Greed kata, or kata’s in general, take a moment and read the article on Steve Gentile’s site. You can also take a look at his solution.

For my first attempt, I made an effort to stay away from Linq. I had no specific reason for this additional constraint. I was really just curious how the solution would turn out. So with that lead-in, here’s my first attempt:

   1: public class Scorer
   2: {
   3:     public int Compute(int[] diceValues)
   4:     {
   5:         if (0 == diceValues.Length) return 0;
   6:  
   7:         Dictionary<int, int> valueGroups = GroupByDiceValue(diceValues);
   8:  
   9:         int totalScore = 0;
  10:         foreach (KeyValuePair<int, int> group in valueGroups)
  11:         {
  12:             totalScore += ScoreGroup(group.Value, group.Key);
  13:         }
  14:         
  15:         return totalScore;
  16:     }
  17:  
  18:     private int ScoreGroup(int count, int value)
  19:     {
  20:         int groupScore = 0;
  21:         if (count>=3)
  22:         {
  23:             if (value == 1) groupScore += 1000;
  24:             else groupScore += value * 100;
  25:  
  26:             count -= 3;
  27:         }
  28:             
  29:         if (1 == value)
  30:         {
  31:             groupScore += count * 100;
  32:         }
  33:             
  34:         if (5 == value)
  35:         {
  36:             groupScore += count * 50;
  37:         }
  38:         return groupScore;
  39:     }
  40:  
  41:     private Dictionary<int, int> GroupByDiceValue(int[] diceValues)
  42:     {
  43:         Array.Sort(diceValues);
  44:         var valueGroups = new Dictionary<int, int>();
  45:         foreach (int value in diceValues)
  46:         {
  47:             int count = 0;
  48:             valueGroups.TryGetValue(value, out count);
  49:             valueGroups[value] = ++count;
  50:         }
  51:         return valueGroups;
  52:     }
  53: }

And here are the tests:



   1: [TestFixture]
   2: public class ScorerTests
   3: {
   4:     
   5:     [Test]
   6:     public void Compute_WhenNoValues_Score_0()
   7:     {
   8:         var scorer = new Scorer();
   9:         Assert.That(scorer.Compute(new int[]{}), Is.EqualTo(0));
  10:     }
  11:  
  12:     [Test]
  13:     public void Compute_ThreeOnes_Score_1000()
  14:     {
  15:         var scorer = new Scorer();
  16:         Assert.That(scorer.Compute(new int[] {1,1,1,2,3}), Is.EqualTo(1000));
  17:     }
  18:  
  19:     [Test]
  20:     public void Compute_ThreeTwos_Score_200()
  21:     {
  22:         var scorer = new Scorer();
  23:         Assert.That(scorer.Compute(new int[] { 3, 2, 2, 2, 3 }), Is.EqualTo(200));
  24:     }
  25:  
  26:     [Test]
  27:     public void Compute_ThreeThreess_Score_300()
  28:     {
  29:         var scorer = new Scorer();
  30:         Assert.That(scorer.Compute(new int[] { 4, 4, 3, 3, 3 }), Is.EqualTo(300));
  31:     }
  32:  
  33:     [Test]
  34:     public void Compute_ASingleOne_Score_100()
  35:     {
  36:         var scorer = new Scorer();
  37:         Assert.That(scorer.Compute(new int[] { 4, 4, 1, 2, 2 }), Is.EqualTo(100));
  38:     }
  39:  
  40:     [Test]
  41:     public void Compute_ASingleFive_Score_50()
  42:     {
  43:         var scorer = new Scorer();
  44:         Assert.That(scorer.Compute(new int[] { 4, 4, 5, 2, 2 }), Is.EqualTo(50));
  45:     }
  46:  
  47:     [Test]
  48:     public void Compute_FourOnes_Score_1100()
  49:     {
  50:         var scorer = new Scorer();
  51:         Assert.That(scorer.Compute(new int[] { 1, 1, 1, 1, 2 }), Is.EqualTo(1100));
  52:     }
  53:  
  54:     [Test]
  55:     public void Compute_ThreeOnesAndTwoFives_Score_1100()
  56:     {
  57:         var scorer = new Scorer();
  58:         Assert.That(scorer.Compute(new int[] { 5, 5, 1, 1, 1 }), Is.EqualTo(1100));
  59:     }
  60:  
  61:     [Test]
  62:     public void Compute_ThreeThreesAndOneFives_Score_350()
  63:     {
  64:         var scorer = new Scorer();
  65:         Assert.That(scorer.Compute(new int[] { 4, 5, 3, 3, 3 }), Is.EqualTo(350));
  66:     }
  67:  
  68:     [Test]
  69:     public void Compute_NoValidScoringCombinations_Score_0()
  70:     {
  71:         var scorer = new Scorer();
  72:         Assert.That(scorer.Compute(new int[] { 2, 2, 4, 4, 3 }), Is.EqualTo(0));
  73:     }
  74:  
  75: }

I’m not unhappy with the solution but the voice in my head is nagging me that I’m missing something obvious.

Sunday, April 10, 2011

Best constraint in an EULA–ever!

I found this in an End User License Agreement that I was reading though this morning:

You will not use the Software for, and will not permit the Software to be used for, any purposes prohibited by law, including, without limitation, for the development, design, manufacture or production of missiles or nuclear, chemical or biological weapons.

I should read more of this things.