Watch Code Kata

Download Code Kata

List of Katas

Kata: TicTacToe (added Game-Over tests)

  • Framework: ruby.rspec
  • Author: Stefan Roock
  • Twitter: StefanRoock

Final Solution

# This exception will be thrown when an illegal ttt move occurs:
# - Violation of correct order of moves: X, O, X...
# - Playing on an already occupied field  
class IllegalMoveException < Exception  
end



========== next file ==========

class Player
  NONE = '-'
  X = 'X'
  O = 'O'
end


========== next file ==========

class Printer

  # Prints a single line of three-line ttt board.
  # Format is "---" for empty board,
  # "X-O" for line with two stones.
  def print_line line
    puts line
  end
  
end


========== next file ==========

require 'player'
require 'illegal_move_exception'

class GameOverCallback
  
  # Called whenever the game is finished, i.e. if a player wins or if the board is full
  def the_winner_is winner
    puts "Winner is #{winner}"
  end
  
end

class TicTacToe
  
  def initialize
    @cells = []
    (0..8).each{ @cells << Player::NONE }
    @next_player = Player::X
  end
  
  # Output current board state to printer.
  # Print each board line separately starting from top row
  def print_board printer
    printer.print_line @cells[0..2].join
    printer.print_line @cells[3..5].join
    printer.print_line @cells[6..8].join
  end

  # Play X stone on board.
  # Trigger game over callback if one player wins or if board is full after this move.
  # fieldIndex ranges from 1 to 9
  # throws IllegalMoveException
  def play_X fieldIndex
    if (@cells[fieldIndex-1] != Player::NONE) or (@next_player != Player::X) then raise IllegalMoveException end
    @cells[fieldIndex-1] = Player::X
    @next_player = Player::O
    check_end_of_game
  end

  # Play O stone on board.
  # Trigger game over callback if one player wins or if board is full after this move.
  # fieldIndex ranges from 1 to 9
  # throws IllegalMoveException
  def play_O fieldIndex
    if (@cells[fieldIndex-1] != Player::NONE) or (@next_player != Player::O) then raise IllegalMoveException end
    @cells[fieldIndex-1] = Player::O
    @next_player = Player::X
    check_end_of_game
  end

  # Set callback object.
  # Setting the callback object is optional i.e. game can be played and finished without it.
  def set_game_over_callback callback
    @game_over_callback = callback
  end

  # Returns the next Player (X or O) to make a move. If game is already over, always return Player.None
  def who_is_next
    @next_player
  end

  def check_end_of_game
    unused_fields = @cells.find_all{|cell| cell == Player::NONE}
    if player_won? Player::O
      @next_player = Player::NONE
      @game_over_callback.the_winner_is(Player::O)
    elsif player_won? Player::X
      @next_player = Player::NONE
      @game_over_callback.the_winner_is(Player::X)
    elsif unused_fields.empty?
      @next_player = Player::NONE
      @game_over_callback.the_winner_is(Player::NONE) if @game_over_callback
    end
  end
  
  def player_won? player
    player_line = [player, player, player]
    (line(0,2) == player_line) or (line(3,5) == player_line) or (line(6,8) == player_line) or (line(0,6) == player_line) or (line(1,7) == player_line) or (line(2,8) == player_line) or (line(0,8) == player_line) or (line(2,6) == player_line)
  end
  
  def line start_index, end_index
    diff = end_index - start_index
    mid_index = start_index + (diff+1)/2
    [@cells[start_index], @cells[mid_index], @cells[end_index]]
  end
  
end


========== next file ==========

require 'tic_tac_toe'

describe TicTacToe do
  
  def print_line line
    @printed_lines << line
  end
  
  def board
    @game.print_board(self)
    @printed_lines
  end
  
  before (:each) do
    @game = TicTacToe.new
    @printed_lines = []
  end
  
  it "should start with an empty board" do
    board.should == ['---', '---', '---']
  end
  
  it "should start with player X" do
    @game.who_is_next.should == Player::X
  end
  
  it "should prevent O to make first move" do
    lambda{@game.play_O 1}.should raise_error
  end
  
  it "should let X make the first move" do
    @game.play_X 1
    board.should == ['X--', '---', '---']
  end
  
  it "should let O make the second move" do
    @game.play_X 5
    @game.play_O 9
    board.should == ['---', '-X-', '--O']
    @game.who_is_next.should == Player::X
  end

  it "should prevent X playing twice in a row" do
    @game.play_X 3
    lambda{@game.play_X 4}.should raise_error
  end
  
  it "should prevent O playing twice in a row" do
    @game.play_X 1
    @game.play_O 3
    lambda{@game.play_O 4}.should raise_error
  end


  it "should prevent O from playing on an occupied field" do
    @game.play_X 4
    lambda{@game.play_O 4}.should raise_error
  end

  it "should prevent X from playing on an occupied field" do
    @game.play_X 4
    @game.play_O 3
    lambda{@game.play_X 3}.should raise_error
  end

  it "should ensure that the game can be finished without game_over_callback explicitly set" do
    @game.play_X 1
    @game.play_O 4
    @game.play_X 2
    @game.play_O 5
    @game.play_X 6
    @game.play_O 3    
    @game.play_X 7    
    @game.play_O 8    
    @game.play_X 9
    @game.who_is_next.should == Player::NONE
  end

end

describe TicTacToe, "Game Over" do

  def the_winner_is winner
    @winner = winner
  end
  
  def play_game moves
    moves.each do |move|
      if @game.who_is_next == Player::X then @game.play_X(move) else @game.play_O(move) end
    end
    @winner.should == nil
  end  
  
  before (:each) do
    @game = TicTacToe.new
    @game.set_game_over_callback(self)
    @winner = nil
  end
  
  it "should detect no winner on full board" do
    play_game [1, 4, 2, 5, 6, 3, 7, 8]
    @game.play_X 9
    @winner.should == Player::NONE
    @game.who_is_next == Player::NONE
  end
    
  it "should detect X winning in row 1" do
    play_game [1, 4, 2, 5]
    @game.play_X 3
    @winner.should == Player::X
    @game.who_is_next.should == Player::NONE
  end  
  
  it "should detect X winning in row 2" do
    play_game [4, 7, 5, 8]
    @game.play_X 6
    @winner.should == Player::X
    @game.who_is_next.should == Player::NONE
  end    
    
  it "should detect O winning in row 2" do
    play_game [1, 4, 2, 5, 7]
    @game.play_O 6
    @winner.should == Player::O
    @game.who_is_next.should == Player::NONE
  end    

  it "should detect X winning in row 3 on full board" do
    play_game [7, 4, 8, 5, 6, 1, 2, 3]
    @game.play_X 9
    @winner.should == Player::X
  end    
    
  it "should detect X winning in column 1" do
    play_game [1, 2, 4, 5]
    @game.play_X 7
    @winner.should == Player::X
  end

  it "should detect O winning in column 1" do
    play_game [2, 1, 5, 4, 3]
    @game.play_O 7
    @winner.should == Player::O
  end

  it "should detect O winning in column 2" do
    play_game [1, 2, 4, 5, 3]
    @game.play_O 8
    @winner.should == Player::O
  end

  it "should detect X winning in column 3" do
    play_game [3, 2, 6, 5]
    @game.play_X 9
    @winner.should == Player::X
  end

  it "should detect X winning in diagonal 1" do
    play_game [1, 2, 5, 3]
    @game.play_X 9
    @winner.should == Player::X
  end

  it "should detect O winning in diagonal 2" do
    play_game [1, 3, 2, 5, 4]
    @game.play_O 7
    @winner.should == Player::O
  end
        
end

Statistics

Framework Started Number of Moves Duration Number of modifications
kata per move kata per move
ruby.rspec 31-May-2011, 02:13:12 PM 16 91m 38s 344 seconds 109 6.8
Chart?chtt=seconds+per+move&cht=bvg&chxt=x,y&chbh=a,0,2&chs=600x200&chxr=1,0,4103.0&chds=0,4103.0&chco=00ff00|00ff00|00ff00|00ff00|ff0000|00ff00|00ff00|00ff00|00ff00|00ff00|00ff00|00ff00|00ff00|00ff00|00ff00|00ff00&chd=t:47.0,24.0,2.0,393.0,518.0,33.0,47.0,49.0,69.0,4103.0,48.0,37.0,35.0,40.0,44.0,9

Longest three moves

Duration in seconds Move
4103 10 Goto move
518 5 Goto move
393 4 Goto move

Sharing

Link to Kata: http://codersdojo.org/statistics/4f581fd932291314a186071d04854a8c61c3a9ca

Short link to Kata: http://bit.ly/knEFPQ

@