In order for TicTacToe to work, you require some kind of mechanism to find out whether or not a player has won. I had a Board object with a winner and won? method to work out whether or not there was a winner.

My Board object consists of a 1D array called positions which keeps track of which marks are present in which position. So a 3x3 board is a 9-element array, where the first element represents the top left position, and the last element represents the bottom right position.

Initially, in order to find a winner, I had a simple loop that iterated across all possible winning configurations. For example I had the following array (of arrays), and found the winner by indexing positions array and checked if the same mark existed on every position;

WINNING POSITIONS = [[0, 1, 2], [3, 4, 5], [6, 7, 8], ...]

This works fine for 3x3, but if you want to have a board with an arbitrary size, this doesn’t work. Instead of hard coding the winning positions, Ruby’s Enumerable Module provides some powerful ways of manipulating arrays.

To extract the rows, the each_slice method splits the array into slices by a particular size. Here, I split it into slices where the size is the square root of the number of positions on the board (i.e. a board with 9 positions will have a row size of 3)

def rows
  @positions.each_slice(Math.sqrt(@positions.size)).to_a
end

To extract the columns, the transpose method takes a 2d array, and swaps the rows and columns.

def columns
  rows.transpose
end

Diagonal lines are slightly trickier, but with the collect method, you can build a new array to pull out the diagonal lines from positions

def diagonal_top_left
  rows.collect.with_index do |row, index|
    row[index]
  end
end

def diagonal_top_right
  rows.collect.with_index do |row, index|
    row.reverse[index]
  end
end

Now that you have all of these winning lines, you can use the find method to find which line, if any, on the board has the same mark sitting on them. My winner method ended up looking something like this;

def winner
  winning_line = winning_lines.find do |line|
    all_equal?(line)
  end

  extract_mark_from_winning_line(winning_line) unless winning_line.nil?
end

The result is I can create a board size with any size, and these methods will always be able to extract the correct winning lines.

This blog post only scratches the surface regarding the methods which are available in the Enumerable module, but it is well worth a read to see what is possible.