Wednesday, February 15, 2006

TDDing a Card Game part 2

In the last entry I wrote a pretty simple class, SpitPile. It is responsible for enforcing the rules for playing a card in the card game Spit.
I received some feedback from Jim Hughes via the TDD yahoogroup. Jim kindly pointed out a corollary to the TDD rule "Only write code to pass a failing test". His corollary is "Always delete production code when it won't break a test". Then he suggested a place in the SpitPile where I could apply this corollary. In SpitPile#in_sequence? I have this
def in_sequence?(card) 
  (card != top_card) and ((card - top_card).abs == 1)
end
What I noticed, with Jim's guidance, is the logical duplication in that snippet. So I removed it and the tests still passed. Mmm, feel that green bar goodness.
Here is the code for SpitPile in its entirety
class SpitPile
  def initialize( first_card )
    @pile = [ first_card ]
  end
  def play( card )
    if in_sequence?(card)
      @pile << card
      return true
    end
    false
  end
  def to_s
    @pile.join(', ')
  end
  def top_card
    @pile.last
  end
  def in_sequence?( card )
    (card - top_card).abs == 1
  end
end
and the tests
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 Okay I'm stuck. I can't c
    assert !@pile.play(2)
    assert_equal "2", @pile.to_s
  end
  def test_rejects_cards_of_non_consecutive_rank
    assert @pile.play(3)
    assert !@pile.play(1)
    assert_equal "2, 3", @pile.to_s
  end
end
Onward. I think I'm going to work on a Game object to manage the flow of pay. Spit is not turn based so I'm thinking I might have to do some threading magic. That is notoriously difficult to test, but I'm not sure I'll need it yet.
Start with a test. Okay, I'm stuck. I can't think of a simple test for Game without going immediately to a Player, but I did come up with this test for Player
class SpitPlayerRules < Test::Unit::TestCase
  def test_run_without_draw
    player = Player.new(self)
    player.hand = [ 2, 3, 4, 5, 6 ]
    @pile_one = [7]
    @pile_two = [7]
    player.play
    assert_equal [7, 6, 5, 4, 3, 2] , @pile_one
  end
end
This test assumes that player will modify SpitPlayerRules@pile_one which begs the question, how? This makes me think about the interaction between Player and Game. I don't have a Game object yet, but Ruby's 'duck' typing allows me to pass the test class to the Player and have it respond to the messages that Game should respond to. Incidentally this is known as the Self-Shunt pattern. I often start with this and later refactor to use a real or mock version of the object.
What should it respond to? It seems to me that Player only needs to be able to 'spit' on a pile to pass this test. First I'll, wait, always run the test first to watch it fail. Okay it threw a NameError which means I haven't defined a Player class yet. Next it'll need a constructor with one parameter, an accessor for @hand, and a method 'play'. With that in place
class Player
  attr_accessor :hand
  def initialize( game )
  end
  def play
  end
end
the test is now failing as expected.
I'm thinking the simplest thing that could possibly work is
def initialize( game )
  @game = game
end
def play
  while(!@hand.empty?)
    @game.spit_on_one(@hand.pop)
  end
end
I'm assuming that game has a method 'spit_on_one'that takes a card. I'll make the SpitPlayerRules class do that for now.
def spit_on_one( card )
  @pile_one << card
end
I'll run the tests. Green bar.
Now I can think of a few more tests that would help flesh out the interface between Game and Player.
  • What happens when player makes an invalid move?
  • How does Player draw from its stock pile?
  • How does Player get notified of the end of game play?
  • Does player notify Game when its out of cards?
All of these questions to answer and I'm out of time to write this. I'll ponder them. Maybe somebody like Jim will virtual pair with me again, and help me figure out where to go next. Until next time here is the code listing.
#--------------------------Spit.rb
class SpitPile
  def initialize( first_card )
    @pile = [ first_card ]
  end
  def play( card )
    if in_sequence?(card)
      @pile << card
      return true
    end
    false
  end
  def to_s
    @pile.join(', ')
  end
  def top_card
    @pile.last
  end
  def in_sequence?( card )
    (card - top_card).abs == 1
  end
end

class Player
  attr_accessor :hand
  def initialize( game )
    @game = game
  end
  def play
    while(!@hand.empty?)
      @game.spit_on_one(@hand.pop)
    end
  end
end
#-----------------SpitTest.rb
require 'test/unit'
require 'spit'

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
  def test_rejects_cards_of_non_consecutive_rank
    assert @pile.play(3)
    assert !@pile.play(1)
    assert_equal "2, 3", @pile.to_s
  end
end

class SpitPlayerRules < Test::Unit::TestCase
  def test_run_without_draw
    player = Player.new(self)
    player.hand = [ 2, 3, 4, 5, 6 ]
    @pile_one = [7]
    @pile_two = [7]
    player.play
    assert_equal [7, 6, 5, 4, 3, 2] , @pile_one
  end

  # game methods
  def spit_on_one( card )
    @pile_one << card
  end
end

0 comments: