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!"