In part three We started with four 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?
Bad Move
What happens when player makes an invalid move? An invalid move could be trying to draw a card when the Player already has five cards in its hand or trying to play a card out of sequence. Thinking ahead a bit and looking at the last entry, we know at some point we want to let third parties write Players, so I don't think we want Player to be responsible for enforcing the five card in the hand limit. The other invalid move is part of the interface between player and game, and is addressed by the rules of the game. Specifically, the Players will turn a new spit card when both Player's are stuck. We are, for the moment, dealing with one player, so we will focus on how a Player notifies the Game that it's stuck. Let's express that in Ruby
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_when_stuck_player_asks_for_new_spit
@spit_reserve = [6]
@player.hand = [2, 2, 3, 4, 5]
@player.play
assert @spit_reserve.empty?, "player did not ask for a new spit"
end
end
To Prefactor or Not To Prefactor
I expect that Game needs to notify Player when a move is invalid, and that will introduce duplication in the game double because it must implement the logic that is in SpitPile. There are at least two options here. One, we could press on and let the actual duplication show us what we need. Two, we could back out the failing test and replace the array, @pile_one, with a SpitPile. In this case I'm going with option two because I think the need for SpitPile is clear. Under other circumstances I might choose differently.
Looking over the current code I noticed that @pile_two is unused, so I'll remove it. Moving on to the replacement. We need to change the initialization of @pile_one in the setup and the #<< in spit_on_one. I'll show that and run the tests.
class SpitPlayerRules < Test::Unit::TestCase
def setup
@player = Player.new(self)
#Game double's state
@pile_one = SpitPile.new(7)
@stock = []
end
# game methods
def spit_on_one( card )
@pile_one << card
end
def draw
@stock.pop
end
def draw?
! @stock.empty?
end
D'oh! The #test_run_without_draw fails. The message points to the following
class SpitPlayerRules < Test::Unit::TestCase
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
Obviously SpitPile is not an Array, so that comparison won't work. I'll just add a test for a SpitPile#to_a method, and then implement and use it.
class SpitPileRules < Test::Unit::TestCase
def setup
@pile = SpitPile.new( 2 )
end
def test_to_a
assert @pile.play(3)
assert_equal [2, 3], @pile.to_a
end
end
class SpitPile
def initialize( first_card )
@pile = [ first_card ]
end
def to_a
@pile
end
end
class SpitPlayerRules < Test::Unit::TestCase
def test_run_without_draw
@player.hand = [ 2, 3, 4, 5, 6 ]
@player.play
assert_equal [7, 6, 5, 4, 3, 2] , @pile_one.to_a
end
Passing the Test
Okay, let's restore the test for an invalid move and a stuck player and see if we can make it pass.
class SpitPlayerRules < Test::Unit::TestCase
def setup
@player = Player.new(self)
#Game double's state
@pile_one = SpitPile.new(7)
@stock = []
@spit_reserve = []
end
def test_when_stuck_player_asks_for_new_spit
@spit_reserve = [6]
@player.hand = [2, 2, 3, 4, 5]
@player.play
assert @spit_reserve.empty?, "player did not ask for a new spit"
end
end
I had a little trouble making that pass. Mostly because when I first wrote the test I wrote @spit_reserve = [6]That didn't provide the Player with enough valid moves. Changing it to
@spit_reserve = [3, 6]solved that problem. Let's review the code that makes the test pass. It's in Player#spit, which is called from Player#play
class Player
attr_accessor :hand
def initialize( game )
@game = game
end
def spit
card_to_play = @hand.pop
unless @game.spit_on_one(card_to_play)
@invalid_moves += 1
@hand.unshift(card_to_play)
end
if @invalid_moves == 5
@game.stuck
@invalid_moves = 0
end
end
end
As you may notice the logic here is crufty. I think that it's okay for now because we are using this Player to explore the interaction between game and Player. I fully expect to make a few other Player class for AI and human play. This simple Player is essentially just playing cards in the order they are in @hand and counting how many @invalid_moves he gets. Then he tells the game when he's stuck. This required that I add the following to the game double and SpitPile.
class SpitPlayerRules < Test::Unit::TestCase
# game methods
def stuck
@pile_one.new_spit(@spit_reserve.pop)
end
end
class SpitPile
def new_spit( card )
@pile << card
end
end
And now the tests are all passing.
Stopping For Today
I'm still concerned about not having a game class, but I can see its shape a little more clearly now. I think one more tweak to the Player class and then I'll extract the game. I'm out of time for now, so that'll have to wait for the next entry. I might even get ambitious and get a new entry up before next week. Until then.
0 comments:
Post a Comment