In my previous post I committed to writing an entry a week in this blog. Coming up with content is hard! Of course since I've called this my Software Engineering blog I've narrowed the topic down somewhat.
One important technique in any programmer's toolkit is Test Driven Development. Contrary to the name, TDD is a Design technique. We write a test to specify and constrain the problem at hand. Then after watching it fail, we write the simplest code that will cause the test to pass. Finally, and most importantly we refactor to eliminate any duplication that we created in the previous step. We do this loop several times and soon we've designed and coded a piece of software. For the next few entries I'm going to use TDD to create a card game.
Spit, also called Speed, is a card game I used to play when I was younger, though I always played the five card hand variant. I'm going to focus the five card hand variant because it's what I know. Others have programmed versions of the game before. I never have though, so this ought to be fun.
The layout is as follows:
stockpile ------> 15 cards 10 cards 1 card 1 card 10 cards <---- spit piles and spit reserves stockpile ------> 15 cardsThe object of the game is to place all of your cards from your stockpile into the spit piles before the other player. This is not a turn based game. You may only have five cards in your hand at time. Cards of the next higher or lower rank may be played from your hand to either spit pile. If both players are stuck you simultaneously flip the top card from the spit reserves on to the spit pile.
As this post is already pretty long we'll just tackle the rules for playing a card on the spit pile. My language of choice at the moment is ruby. Ruby's Test::Unit is the ruby member of the xUnit family.
We start with a test.
require 'test/unit'
class SpitPileRules < Test::Unit::TestCase
def test_accepts_card_of_next_higher_rank
@pile = SpitPile.new( 2 )
assert @pile.play(3)
assert_equal "2, 3", @pile.to_s
end
end
I've named this test case SpitPileRules because it is specifying the rules of play for the spit pile. The first test shows we can play a card of higher rank. At the moment cards are represented as numbers because in Spit we don't care about the suite. We're checking the contents by looking at the string representation of the pile. Also, we expect the SpitPile#play to return a boolean showing success. The next step is to pass this test as quickly as possible, wait first we run it to watch it fail. Okay, now we can pass it.
class SpitPile
def initialize( first_card )
end
def play ( card )
true
end
def to_s
"2, 3"
end
end
That passes. Now we refactor to remove duplication. What duplication, you might ask. Well, the string in SpitPile#to_s duplicates the expected value in SpitPileRules#test_accepts_card_of_next_higher_rank. So I'll replace it with a member variable, and make sure the variable is populated correctly.
class SpitPile
def initialize( first_card )
@pile = [ first_card ]
end
def play ( card )
@pile << card
true
end
def to_s
@pile.join(', ')
end
end
Run the tests, and it still passes. What happens when we add a card of lower rank? Let's write the test.
def test_accepts_card_of_next_lower_rank @pile = SpitPile.new( 2 ) assert @pile.play(1) assert_equal "2, 1", @pile.to_s endOkay, let's make that pass. Wait, first I'll run the test, and it passes. That's fine, that happens very rarely to me, but as SpitPile stands it passes this new test. On to the next test. We could either check if it rejects cards of non consecutive rank or if it rejects cards that are of the same rank. I think I'll do the same rank first.
def test_rejects_card_of_same_rank @pile = SpitPile.new( 2 ) assert !@pile.play(2) assert_equal "2", @pile.to_s endRun that, and it fails. Good. Now we make it pass.
def play ( card )
if card != @pile.first
@pile << card
return true
end
false
end
Passed. Okay, now we have an opportunity to refactor. This time we see duplication in the test specifically all three tests have: @pile = SpitPile.new( 2 )Let's factor that out into a setup method.
class SpitPileRules < Test::Unit::TestCase
def setup
@pile = SpitPile.new( 2 )
end
def test_accepts_card_of_next_higher_rank
assert @pile.play(3)
assert_equal "2, 3", @pile.to_s
end
def test_accepts_card_of_next_lower_rank
assert @pile.play(1)
assert_equal "2, 1", @pile.to_s
end
def test_rejects_card_of_same_rank
assert !@pile.play(2)
assert_equal "2", @pile.to_s
end
end
After running tests I'm sure it's still passing so I will move on to non consecutive cards.
def test_rejects_cards_of_non_consecutive_rank assert @pile.play(3) assert !@pile.play(5) assert_equal "2, 3", @pile.to_s endIt fails. Now we can make it pass.
def play ( card )
if (card != @pile.first) and ((card - @pile.first).abs == 1)
@pile << card
return true
end
false
end
That passes, but I just noticed that it might not pass if I change that case like this:
def test_rejects_cards_of_non_consecutive_rank assert @pile.play(3) assert !@pile.play(1) assert_equal "2, 3", @pile.to_s endIt doesn't. The reason is I'm adding card to the end of the pile with <<, but I'm checking against
@pile.firstso I'll change that to
@pile.lastand we're green. Now it's time to refactor. The obvious duplication of @pile.last can be factored out to something more meaningful.
def play( card )
if (card != top_card) and ((card - top_card).abs == 1)
@pile << card
return true
end
false
end
def top_card
@pile.last
end
Tests are still green. Next we'll clarify what those conditions mean.
def play( card )
if in_sequence?(card)
@pile << card
return true
end
false
end
def in_sequence?(card)
(card != top_card) and ((card - top_card).abs == 1)
end
Okay. I've spent about two hours on writing this article including about 15 minutes of coding. I see a few directions I can go from here. I think SpitPile#in_sequence? really doesn't belong to SpitPile. It seems like it should be on Card, or Deck or something like that. Also, I'm not entirely comfortable with to_s. I think it may be tying the rules of a SpitPile to closely too a UI, but it will do for now.
Now that we have SpitPile should we create a Player class that can interact with SpitPile, or a Game class that controls the flow of the game around SpitPile? Do we make some of the other piles as independent classes? I'll decide soon and work on it in the next entry.
Thanks for following along.
0 comments:
Post a Comment