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)
)