Permalink
Browse files

Decompose board logic into a board object, clean up Game logic further.

Compared to our previous version, this code is much better structured.  If you
look at Board's implementation, you'll notice that all of its code works to
serve a single purpose: to implement a simple tic-tac-toe grid structure.  While
more verbose than what we had before, each individual feature is well isolated,
making it easier to test both manually and via unit tests.

If we revisit the Game class, we see that the flow is now very easy to follow,
and the main purpose of this code is now presentation and controller logic.  But
unlike Board which has a nice homogeneous feel to it, this code has a bit of a
leaky abstraction in that a few of its methods still look like they're
implementing some business logic.  While that's not something you'd need to
obsess over in ordinary day to day coding, for the purposes of this exercise,
let's try one more commit to address that point.
  • Loading branch information...
1 parent 4b65143 commit efcbf51bcc1f7d4d094c671b60761229aec3dded @practicingruby committed Dec 3, 2010
Showing with 109 additions and 59 deletions.
  1. +1 −0 lib/tictactoe.rb
  2. +63 −0 lib/tictactoe/board.rb
  3. +45 −59 lib/tictactoe/game.rb
View
1 lib/tictactoe.rb
@@ -1 +1,2 @@
+require_relative "tictactoe/board"
require_relative "tictactoe/game"
View
63 lib/tictactoe/board.rb
@@ -0,0 +1,63 @@
+module TicTacToe
+ class Board
+ InvalidRequest = Class.new(StandardError)
+
+ LEFT_DIAGONAL_POSITIONS = [[0,0],[1,1],[2,2]]
+ RIGHT_DIAGONAL_POSITIONS = [[2,0],[1,1],[0,2]]
+ SPAN = (0..2)
+ CELL_COUNT = 9
+
+ def initialize
+ @data = [[nil,nil,nil],
+ [nil,nil,nil],
+ [nil,nil,nil]]
+
+ @last_move = nil
+ end
+
+ attr_reader :last_move
+
+ def [](row, col)
+ @data.fetch(row).fetch(col)
+ rescue IndexError
+ raise InvalidRequest, "Position is not within the grid"
+ end
+
+ def []=(row, col, marker)
+ if self[row, col]
+ raise InvalidRequest, "Position is already occupied"
+ end
+
+ @data[row][col] = marker
+ @last_move = [row,col]
+ end
+
+ def to_s
+ @data.map { |row| row.map { |e| e || " " }.join("|") }.join("\n")
+ end
+
+ def intersecting_lines(r1, c1)
+ [left_diagonal, right_diagonal, row(r1), column(c1)]
+ end
+
+ def covered?
+ @data.flatten.compact.length == CELL_COUNT
+ end
+
+ def left_diagonal
+ LEFT_DIAGONAL_POSITIONS.map { |e| self[*e] }
+ end
+
+ def right_diagonal
+ RIGHT_DIAGONAL_POSITIONS.map { |e| self[*e] }
+ end
+
+ def row(index)
+ SPAN.map { |column| self[index, column] }
+ end
+
+ def column(index)
+ SPAN.map { |row| self[row, index] }
+ end
+ end
+end
View
104 lib/tictactoe/game.rb
@@ -1,93 +1,79 @@
module TicTacToe
class Game
def initialize
- @board = [[nil,nil,nil],
- [nil,nil,nil],
- [nil,nil,nil]]
-
+ @board = TicTacToe::Board.new
@players = [:X, :O].cycle
end
attr_reader :board, :players, :current_player
def play
- start_new_turn
-
- loop do
- display_board
-
- row, col = move_input
- next unless valid_move?(row,col)
-
- board[row][col] = current_player
-
- if winning_move?(row, col)
- puts "#{current_player} wins!"
- return
+ catch(:finished) do
+ loop do
+ start_new_turn
+ show_board
+ move
+
+ check_for_win
+ check_for_draw
end
-
- if draw?
- puts "It's a draw!"
- return
- end
-
- start_new_turn
end
end
def start_new_turn
@current_player = @players.next
end
- def display_board
- puts board.map { |row| row.map { |e| e || " " }.join("|") }.join("\n")
+ def show_board
+ puts board
end
- def winning_move?(row, col)
- left_diagonal = [[0,0],[1,1],[2,2]]
- right_diagonal = [[2,0],[1,1],[0,2]]
-
- lines = []
+ def game_over
+ throw :finished
+ end
- [left_diagonal, right_diagonal].each do |line|
- lines << line if line.include?([row,col])
- end
+ def move
+ row, col = move_input
+ board[row, col] = current_player
+ rescue TicTacToe::Board::InvalidRequest => error
+ puts error.message
+ retry
+ end
- lines << (0..2).map { |c1| [row, c1] }
- lines << (0..2).map { |r1| [r1, col] }
+ def check_for_win
+ return false unless board.last_move
- lines.any? do |line|
- line.all? { |row,col| board[row][col] == current_player }
+ win = board.intersecting_lines(*board.last_move).any? do |line|
@npras
npras May 20, 2012

Hi Greg,

I'm an 'expert beginner' in ruby and rails. Been reading through your blog off late religiously. Thanks for the great work :)
A small doubt in this line. What is the * before board.last_move? Couldn't find about it anywhere! Not even in irb.

@practicingruby
practicingruby May 20, 2012

The * operator takes an array and converts it into a series of arguments.

so for example,

def foo(a,b,c)
   #...
end

can be called in this way:

x = [1,2,3]
foo(*x)

It's commonly referred to as the Array splat operator. Hope that helps.

+ line.all? { |cell| cell == current_player }
end
- end
- def draw?
- board.flatten.compact.length == 9
+ if win
+ puts "#{current_player} wins!"
+ game_over
+ end
end
- def valid_move?(row,col)
- begin
- cell_contents = board.fetch(row).fetch(col)
- rescue IndexError
- puts "Out of bounds, try another position"
- return false
- end
-
- if cell_contents
- puts "Cell occupied, try another position"
- return false
+ def check_for_draw
+ if @board.covered?
+ puts "It's a tie!"
+ game_over
end
-
- true
end
def move_input
print "\n>> "
- row, col = gets.split.map { |e| e.to_i }
- puts
-
- [row, col]
+ response = gets
+
+ case response
+ when /quit/i
+ puts "Wimp!"
+ throw :finished
+ else
+ row, col = response.chomp.split.map { |e| e.to_i }
+ puts
+
+ [row, col]
+ end
end
-
end
end

0 comments on commit efcbf51

Please sign in to comment.