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 |
Sharing
Link to Kata: http://codersdojo.org/statistics/4f581fd932291314a186071d04854a8c61c3a9ca
Short link to Kata: http://bit.ly/knEFPQ
@