20/05/2010

Kata Mastermind in ioke

A few months ago I was introduced to the Dojo XP France. A french and parisian occurrence of a programming dojo concentrating on practicing and discussing TDD.

While "every monday 18:30" is a bit too much for me to follow (not to mention go to as I have almost an hour of transit to go to the usual dojo place) I have been working on some of the Katas.

The last I played with was the Mastermind kata, more precisely a Ioke implementation of the kata.

I have been able to find a very pleasing solution to the kata (hopefully a valid solution too :) ) which I present below. I tried to separate several stages of the code, I hope this will make the progression easier to follow. Obviously the best way to see the code evolve is to come to the Dojo.

Stage 1 : find the correct and well placed pegs

First the tests ...

describe("evalm", 
 ;stage 1
 it("should return [0,0] when all pegs are the wrong color", 
  evalm([]("M","M","M","M"), []("B","B","B","B")) should == [](0,0)
 )
 it("should return [1,0] when the first peg of both secret and guess is the same", 
  evalm([]("B","M","M","M"), []("B","B","B","B")) should == [](1,0)
 )
 it("should return [1,0] when the second peg of both secret and guess is the same", 
  evalm([]("M","B","M","M"), []("B","B","B","B")) should == [](1,0)
 )
 it("should return [2,0] when the first 2 peg of both secret and guess is the same", 
  evalm([]("B","B","M","M"), []("B","B","B","B")) should == [](2,0)
 )
)

Then the code ...

;stage 1
evalm=method(guess, secret, 
 if(
  guess[0]==secret[0],
  if(
   guess[1]==secret[1],
   [](2,0),
   [](1,0)
  ),  
  if(
   guess[1]==secret[1],
   [](1,0),
   [](0,0)
  ) 
 ) 
 )

Stage 2 : Refactor the code to DRY

Same tests as we refactor, thus the code ...

;stage2 == stage 1 refactored
evalm=method(guess, secret, 
 good=guess zip(secret) filter(inject(==)) count
 [](good, 0)
)

Stage 3 : Introduce the notion of misplaced pegs for the "R"ed color

By adding More tests ..

describe("evalm", 
 ;stage 1
 it("should return [0,0] when all pegs are the wrong color", 
  evalm([]("M","M","M","M"), []("B","B","B","B")) should == [](0,0)
 )
 it("should return [1,0] when the first peg of both secret and guess is the same", 
  evalm([]("B","M","M","M"), []("B","B","B","B")) should == [](1,0)
 )
 it("should return [1,0] when the second peg of both secret and guess is the same", 
  evalm([]("M","B","M","M"), []("B","B","B","B")) should == [](1,0)
 )
 it("should return [2,0] when the first 2 peg of both secret and guess is the same", 
  evalm([]("B","B","M","M"), []("B","B","B","B")) should == [](2,0)
 )
 ;stage 2 we refactor the good to use zip
 ;stage 3 introduce the misplaced
 it("should return [0,1] when one red peg is misplaced",
  evalm([]("M","R","M","M"), []("R","B","B","B")) should == [](0,1)
 )
 it("should return [0,2] when two red pegs are misplaced",
  evalm([]("M","R","M","R"), []("R","B","R","B")) should == [](0,2)
 )
)

And writing more code ...

;stage 3
evalm=method(guess, secret, 
 good=guess zip(secret) filter(inject(==)) count
 bad=guess zip(secret) filter(inject(!=))
 bad=bad filter([1]=="R") count
 [](good, bad)
)

Stage 4 : Generalize to all the colors

And yet more tests :) ...

describe("evalm", 
 ;stage 1
 it("should return [0,0] when all pegs are the wrong color", 
  evalm([]("M","M","M","M"), []("B","B","B","B")) should == [](0,0)
 )
 it("should return [1,0] when the first peg of both secret and guess is the same", 
  evalm([]("B","M","M","M"), []("B","B","B","B")) should == [](1,0)
 )
 it("should return [1,0] when the second peg of both secret and guess is the same", 
  evalm([]("M","B","M","M"), []("B","B","B","B")) should == [](1,0)
 )
 it("should return [2,0] when the first 2 peg of both secret and guess is the same", 
  evalm([]("B","B","M","M"), []("B","B","B","B")) should == [](2,0)
 )
 ;stage 2 we refactor the good to use zip
 ;stage 3 introduce the misplaced
 it("should return [0,1] when one red peg is misplaced",
  evalm([]("M","R","M","M"), []("R","B","B","B")) should == [](0,1)
 )
 it("should return [0,2] when two red pegs are misplaced",
  evalm([]("M","R","M","R"), []("R","B","R","B")) should == [](0,2)
 )
 ;stage 4 generalize to all colors
 it("should return [0,2] when a red peg and a yellow peg are misplaced",
  evalm([]("M","R","M","Y"), []("R","B","Y","B")) should == [](0,2)
 )
)

For more code ...

;stage 4
evalm=method(guess, secret, 
 good=guess zip(secret) filter(inject(==)) count
 bad=guess zip(secret) filter(inject(!=))
 colors=bad map:set(x,x[0])
 bad=colors map(x,bad filter([1]==x) count) sum 
 [](good, bad)
)

Stage 5 : Refactor for DRY

Same tests, different code ...

;stage 5
evalm=method(guess, secret, 
  sorted = guess zip(secret) groupBy(inject(==))
 good = (sorted[true]||[]) count
 badpairs = sorted[false]||[]
 colors = badpairs map:set(x,x[0])
 bad = colors map(x,badpairs filter([1]==x) count) sum || 0
 [](good, bad)
)

While this code is longer, it uses groupBy instead of zipping and filtering the list twice. I would like to improve on this by ensuring that groupBy always returns an empty list [] whether there are values or not. However I haven't found an elegant way to do this ... yet.

Stage 6: the complete solution with acceptance tests

The tests

describe("evalm", 
 ;stage 1
 it("should return [0,0] when all pegs are the wrong color", 
  evalm([]("M","M","M","M"), []("B","B","B","B")) should == [](0,0)
 )
 it("should return [1,0] when the first peg of both secret and guess is the same", 
  evalm([]("B","M","M","M"), []("B","B","B","B")) should == [](1,0)
 )
 it("should return [1,0] when the second peg of both secret and guess is the same", 
  evalm([]("M","B","M","M"), []("B","B","B","B")) should == [](1,0)
 )
 it("should return [2,0] when the first 2 peg of both secret and guess is the same", 
  evalm([]("B","B","M","M"), []("B","B","B","B")) should == [](2,0)
 )
 ;stage 2 we refactor the good to use zip
 ;stage 3 introduce the misplaced
 it("should return [0,1] when one red peg is misplaced",
  evalm([]("M","R","M","M"), []("R","B","B","B")) should == [](0,1)
 )
 it("should return [0,2] when two red pegs are misplaced",
  evalm([]("M","R","M","R"), []("R","B","R","B")) should == [](0,2)
 )
 ;stage 4 generalize to all colors
 it("should return [0,2] when a red peg and a yellow peg are misplaced",
  evalm([]("M","R","M","Y"), []("R","B","Y","B")) should == [](0,2)
 )
)
 ;stage 5 refactor to use groupBy
 ;stage 6 acceptance tests
describe("recette",
 it("should return [0,0] for guess(M,M,M,M) secret(B,B,B,B)", 
  evalm([]("M","M","M","M"), []("B","B","B","B")) should == [](0,0)
 )
 it("should return [4,0] for guess(B,B,B,B) secret(B,B,B,B)", 
  evalm([]("B","B","B","B"), []("B","B","B","B")) should == [](4,0)
 )
 it("should return [0,4] for guess(A,B,C,D) secret(D,A,B,C)", 
  evalm([]("A","B","C","D"), []("D","A","B","C")) should == [](0,4)
 )
 it("should return [2,2] for guess(A,B,C,B) secret(C,B,A,B)", 
  evalm([]("A","B","C","B"), []("C","B","A","B")) should == [](2,2)
 )
)

And the code

;stage 5
evalm=method(guess, secret, 
  sorted = guess zip(secret) groupBy(inject(==))
 good = (sorted[true]||[]) count
 badpairs = sorted[false]||[]
 colors = badpairs map:set(x,x[0])
 bad = colors map(x,badpairs filter([1]==x) count) sum || 0
 [](good, bad)
)

Aucun commentaire: