Wednesday, February 22, 2006

TDDing a Card Game part 3

Last week I ended with the following questions
  • 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?
Today I'll pick start by picking a question and translating it into a test. I'll focus on the stock pile question.
#SpitPlayerRules
def test_player_draws_when_hand_is_short
  @stock = [2]
  player = Player.new(self)
  player.hand = [3, 4, 5, 6]
  @pile_one = [7]
  @pile_two = [7]
  player.play
  assert @stock.empty?, "player did not draw as expected"
end
Remember that SpitPlayerRules is using the self shunt pattern so it is a double (think stunt double) for the still theoretical Game object. I've primed the Player under test with a hand that is one card short. Also I've assumed that somehow that Player will be able to draw from the @stock that the test/Game double has. In our implementation we need to consider how the Game communicates to the Player that the stock pile is empty. I ran that and it failed predictably.
Here is the old Player#play
def play
  while(!@hand.empty?)
    @game.spit_on_one(@hand.pop)
  end
end
Should we draw before or after we spit? For now I'll say before, and implement the draw as quickly as I can
def play
  while(!@hand.empty?)
    if @hand.size < 5 and @game.draw?
      @hand << @game.draw 
    end
    @game.spit_on_one(@hand.pop)
  end
end
Before this can run we need to implement the two methods draw and draw? on our Game double.
#SpitPlayerRules
def draw
  @stock.pop
end
def draw?
  ! @stock.empty?
end
I'll just run the test and ... D'oh I need to make @stock an empty array in #test_run_without_draw to make it pass. Good that was fairly easy. Now on to the text test.
Wait! The TDD mantra is red (check), green (check), REFACTOR. Okay is there anything we can do to make the code simpler? What is Simple Code? There are several formulations of guidelines for simplicity in code here. The first guideline is "runs all tests", and we are good there. The next is "contains no duplication". While the Player class is acceptable. Its tests are not. The duplication is the set up the of the Player under test and the Game double's state. I'll factor that out into setup method.
#SpitPlayerRules 
def setup 
  @player = Player.new(self)
  #Game double's state
  @pile_one = [7]
  @pile_two = [7]
  @stock = []
end
def test_run_without_draw
  @player.hand = [ 2, 3, 4, 5, 6 ]
  @player.play
  assert_equal [7, 6, 5, 4, 3, 2] , @pile_one
end
def test_player_draws_when_hand_is_short
  @stock << 2
  @player.hand = [3, 4, 5, 6]
  @player.play
  assert @stock.empty?, "player did not draw as expected"
end
The third guideline is "expresses all the ideas you want to express". Looking at Player#play I think there are two ideas that are contained in that one method: draw and spit. I'll extract a method for each concept.
#Player
def play
  while(!@hand.empty?)
    draw
    spit
  end
end
def draw
  if @hand.size < 5 and @game.draw?
    @hand << @game.draw 
  end    
end
def spit
  @game.spit_on_one(@hand.pop)    
end
The final guideline "minimizes classes and methods". It makes me think twice about the decision to extract #spit. I'm a little uncertain if the #spit method is pulling its weight. On the one hand it is only one line long. On the other hand it makes explicit the action in the card game and provides a place for any more advanced decision making at that point in the game. I think I'll keep it for now.
I'm going to stop here because I'm trying to spend only an hour on this entry. I think we've made some progress here. The interaction between Player and Game is starting to show up in the SpitPlayerRules class. We covered some guidelines for simple code and applied them. And we answered one of the questions we asked last week. So until next time I'll leave you with a listing of the current state of things.
#--------------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?)
      draw
      spit
    end
  end
  def draw
    if @hand.size < 5 and @game.draw?
      @hand << @game.draw 
    end    
  end
  def spit
    @game.spit_on_one(@hand.pop)    
  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 setup 
    @player = Player.new(self)
    #Game double's state
    @pile_one = [7]
    @pile_two = [7]
    @stock = []
  end
  def test_run_without_draw
    @player.hand = [ 2, 3, 4, 5, 6 ]
    @player.play
    assert_equal [7, 6, 5, 4, 3, 2] , @pile_one
  end
  def test_player_draws_when_hand_is_short
    @stock << 2
    @player.hand = [3, 4, 5, 6]
    @player.play
    assert @stock.empty?, "player did not draw as expected"
  end

  # game methods
  def spit_on_one( card )
    @pile_one << card
  end
  def draw
    @stock.pop
  end
  def draw?
    ! @stock.empty?
  end 
end

0 comments: