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.

  1. – rock beats scissors
  2. – scissors does not beat rock
  3. – rock does not beat rock
  4. – scissors beats paper
  5. – 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.

7 Responses to “The Rock-Scissors-Paper OCP Dojo”

  1. Tonino Says:

    Great! It could be even better if that work will be done also for the ocp kata bowling solution as well.

  2. Franco Lombardo Says:

    “Simplicity is the ultimate sophistication.” – Leonardo da Vinci

  3. matteo Says:

    @Tonino: I did that… look at http://github.com/xpmatteo/bowling-workshop and select the “martian bowling” branch :)

  4. Stefano Masini Says:

    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

  5. matteo Says:

    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?

  6. Indrit Says:

    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.

  7. matteo Says:

    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

Leave a Reply