Tic Tac Toe in Ruby

ruby game

Tue Feb 01 17:23:04 -0800 2011

Just wanted to do it, 'is all. An N x N tictactoe game on the CLI in ruby. Actually, I wanted to see just how quickly I could write a functional one. It took about an hour and a half.

Nostalgia has it that I wrote a 3 x 3 version in assembly, yes assembly, nearly a decade back. By my vague recollection, back then, it did take longer than 1.5 hours. Significantly longer.

View as gist.

# the anatomy of a slot
Slot = Struct.new(:row, :column, :value)

# The board has n x n slots
class Board
  
  # Constants
  X = "X"
  O = "O"

  # attr accessor
  attr_accessor :grid, :slots, :game_over, :current_player, :winner
    
  # constructor
  def initialize(n)
    # initialize the instance variables
    @grid, @slots = Array.new(n), Hash.new 
    @game_over, @current_player, @winner = false, Board::X, nil

    # build the board
    slot_counter = 0
    @grid.each_with_index do |row, i|      
      @grid[i] = Array.new(n)
      @grid[i].each_with_index do |column, j|
        @grid[i][j] = Slot.new(i, j, slot_counter)
        @slots[slot_counter] = @grid[i][j]
        slot_counter += 1
      end
    end      
  end
    
  def draw
    system("clear")
    @grid.each_with_index do |row, i|
      row_print = row.collect{ |s| s.value }.join(" | ")
      puts row_print
      puts "-" * row_print.length if @grid.length != (i + 1)
    end
    puts ""
  end
  
  def move
    print "#{@current_player} -- Please enter a number from the board: "
    slot_number = gets.chomp.to_i
    if !@slots.has_key?(slot_number)
      move
    elsif @slots[slot_number].value == Board::X || @slots[slot_number].value == Board::O
      puts "Please enter a number that is not already marked!"
      move
    else
      @board_updated = true
      @slots[slot_number].value = @current_player
    end
    
    # post-move analysis.
    if won?(@slots[slot_number])
      @game_over = true
      @winner    = @current_player
    elsif stalemate?
      @game_over = true      
    else
      switch_turn if @board_updated
    end
  end
  
  def switch_turn
    @current_player = (@current_player == Board::X) ? Board::O : Board::X
    @board_updated = false
  end
  
  def won?(slot)
     horizontal_win?(slot) || 
     vertical_win?(slot) || 
     diagonal_win?(:orientation => "l2r") || 
     diagonal_win?(:orientation => "r2l")
  end
  
  def stalemate?
    stalemate = true
    @slots.each { |k,slot| stalemate = false if slot.value.is_a?(Fixnum) }
    stalemate
  end
  
  def game_over?
    @game_over
  end

  private 
  
  def horizontal_win?(slot)
    slot_values = @grid[slot.row].collect{ |s| s.value == @current_player }
    slot_values.length == @grid.length && slot_values.uniq.to_s == "true"
  end
  
  def vertical_win?(slot)
    slot_values = @grid.collect{ |r| r[slot.column] }.collect{ |s| s.value == @current_player }
    slot_values.length == @grid.length && slot_values.uniq.to_s == "true"
  end
  
  def diagonal_win?(options = {})
    slots = []    
    case options[:orientation]
      when "l2r" then 0.upto(@grid.length - 1) { |i| slots << @grid[i][i]  } 
      when "r2l" then (@grid.length - 1).downto(0){ |i| slots << @grid[i][@grid.length - 1 - i] }
    end
    slot_values = slots.collect { |s| s.value == @current_player }
    slots.length == @grid.length && slot_values.uniq.to_s == "true"
  end

end

# new game
board = Board.new(3)
while !board.game_over? do 
  board.draw
  board.move
end
board.draw

# result
puts !board.winner.nil? ? "#{board.winner} won the game!" : "The game was a draw!"
blog comments powered by Disqus