Skip to content
Permalink
Browse files

Break out rule logic into TicTacToe::Rules mixin.

This change causes TicTacToe::Game to become purely a controller class, with no
game specific logic within it.  Through the use of callbacks, TicTacToe::Rules
manages to be completely free of control code.

At this point, it is worth asking whether we've gone too far.  This design is
certainly more than what is required for such a trivial game, but the
implementation techniques we've used can certainly come in handy in more complex
scenarios.

One tricky thing about this level of abstraction is coming up with good names.
I'm not happy with 'check_move' as a name for a method that actually executes a
move, but it's a bit weird for it to be asymmetric to the check_win and
check_draw methods.  Perhaps the more generic move_action, win_action,
draw_action would have been better?  At this point, we need to say enough is
enough and call it a night.

Fun exercise though, right?
  • Loading branch information...
practicingruby committed Dec 3, 2010
1 parent efcbf51 commit 0fef18d320af2bd1a08f5115a2b94e552205f218
Showing with 37 additions and 31 deletions.
  1. +1 −0 lib/tictactoe.rb
  2. +5 −31 lib/tictactoe/game.rb
  3. +31 −0 lib/tictactoe/rules.rb
@@ -1,2 +1,3 @@
require_relative "tictactoe/board"
require_relative "tictactoe/rules"
require_relative "tictactoe/game"
@@ -1,5 +1,7 @@
module TicTacToe
class Game
include TicTacToe::Rules

def initialize
@board = TicTacToe::Board.new
@players = [:X, :O].cycle
@@ -12,10 +14,10 @@ def play
loop do
start_new_turn
show_board
move

check_for_win
check_for_draw
check_move { |error_message| puts error_message }
check_win { puts "#{current_player} wins" }
check_draw { puts "It's a tie" }
end
end
end
@@ -32,34 +34,6 @@ def game_over
throw :finished
end

def move
row, col = move_input
board[row, col] = current_player
rescue TicTacToe::Board::InvalidRequest => error
puts error.message
retry
end

def check_for_win
return false unless board.last_move

win = board.intersecting_lines(*board.last_move).any? do |line|
line.all? { |cell| cell == current_player }
end

if win
puts "#{current_player} wins!"
game_over
end
end

def check_for_draw
if @board.covered?
puts "It's a tie!"
game_over
end
end

def move_input
print "\n>> "
response = gets
@@ -0,0 +1,31 @@
module TicTacToe
module Rules
def check_move
row, col = move_input
board[row, col] = current_player
rescue TicTacToe::Board::InvalidRequest => error
yield error.message if block_given?
retry
end

def check_win
return false unless board.last_move

This comment has been minimized.

Copy link
@mithesh109

mithesh109 Oct 10, 2018

Hello,
I don't understand the use of this line in the code.
Can you please explain?

This comment has been minimized.

Copy link
@practicingdev

practicingdev Oct 10, 2018

It is possible for board.last_move to be nil, on the first move. This line handles that case.


win = board.intersecting_lines(*board.last_move).any? do |line|
line.all? { |cell| cell == current_player }
end

if win
yield
game_over
end
end

def check_draw
if @board.covered?
yield
game_over
end
end
end
end

0 comments on commit 0fef18d

Please sign in to comment.
You can’t perform that action at this time.