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.