Skip to content
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...
practicingruby committed Dec 3, 2010
1 parent 4b65143 commit efcbf51bcc1f7d4d094c671b60761229aec3dded
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
@@ -1 +1,2 @@
require_relative "tictactoe/board"
require_relative "tictactoe/game"
@@ -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
@@ -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|

This comment has been minimized.

Copy link
@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.

This comment has been minimized.

Copy link
@practicingruby

practicingruby May 20, 2012

Author Owner

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.
You can’t perform that action at this time.