The Rock-Scissors-Paper OCP Dojo
Last time at the Milano XP User Group we had our first retrospective. We gathered lots of useful insights. One of the selected actions was “if you propose a Kata, you must publish your solution“.
Guilty as charged: when I proposed Rock-Scissors-Paper for an evening of coding, we got an acute case of analysis-paralysis. It was a humbling evening: we didn’t manage to implement the first requirement, “rock beats scissors.” I provide here my solution. It’s brand new code, written today, so it benefits from the experience.
This is the list of requirements. I didn’t get it right immediately.
Context: we want to implement a gaming engine for the game rock-scissors-paper. We don’t care about players or rounds; we only want to implement the “beats” relation.
- – rock beats scissors
- – scissors does not beat rock
- – rock does not beat rock
- – scissors beats paper
- – paper beats rock
The clarifying statement should kill most of the discussion that we had that evening. We are not interested in modelling players or rounds or winning. Only the “beats” relations.
This kata was meant to be executed with the OCP Dojo rules. I will use Ruby today, because I want to take a break from Java. But the solution will not use any trick that can’t be done in Java. I promise :)
Rock beats scissors
This one is easy. That is, easy if you decide how to implement the “rock” and the “scissors”. I decided for the simplest thing, that is a Ruby “symbol”. In Java I would have used a string.
require "test/unit" class Rsp def beats(first, second) true end end class TestRsp < Test::Unit::TestCase def setup @rsp = Rsp.new end def test_rock_beats_scissors assert @rsp.beats(:rock, :scissors) end end
Note that I have substituted the “factory” of the OCP rules with a simple setup method. The @-prefixed variable is Ruby’s way to define a member variable.
Scissors does not beat rock
This one almost escaped me at first. If your requirements only mention “X beats Y”, then return true
will always be a valid implementation! Also, note that we don’t need to represent “ties”. Winning or tieing are not part of our requirements; but it should be clear that the “beats” relation can be used to determine victory or tie.
So this is the famous “second test” that in most OCP Dojos forces the creation of design. If it were not for the rules, I’d be tempted to write something as boring as
return true if first.equals(:scissors) and second.equals(:rock)
inside the Rsp#beats method. Instead I decide I want to solve the problem with a rule-based style. Let’s delegate the decision of “beating” to a rule.
class Rule def initialize first, second @first, @second = first, second end def beats(first, second) @first == first and @second == second end end class Rsp def initialize(rule) @rule = rule end def beats(first, second) return true if @rule.beats(first, second) false end end class TestRsp < Test::Unit::TestCase def setup @rsp = Rsp.new Rule.new(:rock, :scissors) end def test_rock_beats_scissors assert @rsp.beats(:rock, :scissors) end def test_scissors_does_not_beat_rock assert_false @rsp.beats(:scissors, :rock) end end
Now if we wanted to be strict I’d have to show that I can make the first test pass and the second test fail by simply passing in a different Rule. I could do that by defining
class AlwaysTrueRule def beats(a, b); return true; end end
This way if I pass an instance of AlwaysTrueRule, it will make the first test pass and the second fail. If I pass in the correct rule, both tests will pass.
Rock does not beat rock
Now this works with no modification needed to the code. From this point on, it will be clear that we don’t need to test all cases where “beats” does not hold. The implementation makes it obvious that only the cases that are explicitly covered by the rules pass.
Scissors beats paper
This forced me to change the single rule to a list of rules.
class Rsp def initialize(rules) @rules = rules end def beats(first, second) for rule in @rules return true if rule.beats(first, second) end false end end class TestRsp < Test::Unit::TestCase def setup @rsp = Rsp.new [ Rule.new(:rock, :scissors), Rule.new(:scissors, :paper), ] end # ... def test_scissors_beats_paper assert @rsp.beats(:scissors, :paper) end end
The rest
At this point the design is more or less complete. The remaining test, “paper beats rock” only needs to add a new Rule to the list. No “coding” needed, only configuration of existing objects.
The test of a design is how well it resists to changes. I would say that at this point, implementing Rock-scissors-paper-spock-lizard or even RSP-15 would be trivial.
I can imagine even wierder variations, for instance:
- Peace beats anything other than peace
- Random has a 0.5 chance of beating anything other than random
- Thrice beats anything other than thrice on the third time it’s played
and I think that by writing a custom Rule, these could also be easily done. Therefore I think that the design so far is a success. The amazing thing is that I did all the coding in 1 pomodoro (25 minutes)! That includes taking notes and saving intermediate versions, for blogging about later.
Why did we take so long when we tried this at the coding dojo? I think that the trick is in defining the scope more precisely:
We don’t care about players or rounds; we only want to implement the “beats” relation.
By thinking of “beats” as a mathematical relation, that is, a subset of Stuff × Stuff for you mathematically inclined, we simplify our job. Also, giving up on the idea of defining a Rock class helps.
July 12th, 2010 at 17:38
Great! It could be even better if that work will be done also for the ocp kata bowling solution as well.
July 15th, 2010 at 14:07
“Simplicity is the ultimate sophistication.” – Leonardo da Vinci
July 21st, 2010 at 15:35
@Tonino: I did that… look at http://github.com/xpmatteo/bowling-workshop and select the “martian bowling” branch :)
July 31st, 2010 at 07:23
Hi Matteo,
I’m intrigued by the OCP Dojo rules, but I’m not sure I grasped them entirely.
I’ve tried this kata in Python and I was a bit disappointed by how trivial the solution was. This is my final version:
class Item(object):
def beats(self, other):
return other.__class__.__name__ in self._beats
class Rock(Item):
_beats = [‘Scissors’]
class Scissors(Item):
_beats = [‘Paper’]
class Paper(Item):
_beats = [‘Rock’]
class Beats_TestCase(unittest.TestCase):
def test_rock_beats_scissors(self):
self.assertTrue(Rock().beats(Scissors()))
def test_scissors_does_not_beat_rock(self):
self.assertTrue(not Scissors().beats(Rock()))
def test_rock_does_not_beat_rock(self):
self.assertTrue(not Rock().beats(Rock()))
def test_scissors_beats_paper(self):
self.assertTrue(Scissors().beats(Paper()))
def test_paper_beats_rock(self):
self.assertTrue(Paper().beats(Rock()))
As you can see, it’s a typical case of unit-testing that 2+2 equals 4. It’s stating the obvious. In this case the implementation is so trivial that it equals the behavior. So it’s basically like testing the implementation, which is wrong in general.
The point is, I don’t see how I contravened the OCP Dojo rules. Is it maybe because my “factories” are the object constructors? Should I have used an actual factory method instead?
I follow your post and I like your considerations, but I feel like I won’t be able to come to this kind of interesting reasonings if I fall into the 2+2 trap so easily.
Cheers,
Stefano
July 31st, 2010 at 14:28
Hi Stefano,
the test of a design is how easy it is to adapt the design to new requirements. Suppose you wanted to extend your program to handle Rock-scissors-paper-spock-lizard? Suppose you needed to support both so that the players can decide which set of rules to use before each game?
October 9th, 2010 at 12:27
Hi Matteo,
a very simple and good solution.
I think we must analyse why we stalled at coding dojo, I think that could help us for the next similar meetings. We must question ourself how to arrive to a conclusion in a simple and a quick-time way still allowing potentially all participants to exploit/declare their ideas/design.
Thx.
October 9th, 2010 at 16:16
Hi Indrit, thank you for your feedback.
I can tell you how I did unstall when I started working again on this kata. I was thinking of “beats” as a mathematical binary relation. There is a lot of power in modelling problems with mathematics. And it’s not complicated mathematics. A book that explains a bit: http://www.usingz.com/text/online/index.html