The Rock-Scissors-Paper OCP Dojo
Monday, July 12th, 2010Last 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.