Search code examples
rubytic-tac-toe

How to find first winning combination in Tic-Tac-Toe board?


New at Ruby so excuse the poor code. I would like to iterate through the multidimensional array WIN_COMBINATIONS and check whether at least one array has all of its elements equal to 'X' or all equal to 'O'. If so, return the matched array. This is done through the won? function but it seems to only be returning the entire multidimensional array. Any assistance would be appreciated.

class TicTacToe
  WIN_COMBINATIONS = [ 
[0,1,2], # top_row 
[3,4,5], # middle_row 
[6,7,8], # bottom_row 
[0,3,6], # left_column 
[1,4,7], # center_column 
[2,5,8], # right_column 
[0,4,8], # left_diagonal 
[6,4,2] # right_diagonal 
]

  def initialize
    @board = Array.new(9, " ")
  end

  def display_board
    puts " #{@board[0]} | #{@board[1]} | #{@board[2]} "
    puts "-----------"
    puts " #{@board[3]} | #{@board[4]} | #{@board[5]} "
    puts "-----------"
    puts " #{@board[6]} | #{@board[7]} | #{@board[8]} "
  end

  def input_to_index(board_position)
    user_input = board_position.to_i
    user_input - 1
  end

  def move(board_index, player_token = 'X')
    @board[board_index] = player_token
  end

  def position_taken?(board_position)
    if @board[board_position] == ' '
      false
    else
      true
    end
  end

  def valid_move?(board_position)
    if board_position >= 0 and board_position <= 8
      if @board[board_position] == ' '
        true
      end
    else
      false
    end
  end

  def current_player
    turn_count % 2 == 0 ? "X" : "O"
  end

  def turn_count
    @board.count{|token| token == "X" || token == "O"}
  end

  def turn
    puts "Select your move (1-9)\n"
    move = gets.chomp
    move_index = input_to_index(move)
    if valid_move?(move_index)
      token = current_player
      move(move_index, token)
      display_board
    else
      puts "Select your move (1-9)\n"
      move = gets.chomp
    end
  end

  def won?
    WIN_COMBINATIONS.each do |combinations|
      if combinations.all? {|combination| combination == 'X' or combination == 'O'}
        combinations
      else
        false
      end
    end
  end

  def draw?
    if full? and !won?
      true
    elsif won?
      false
    else
      false
    end
  end

  def over?

  end

  def winner

  end

  def play

  end
end

Solution

  • [...] it seems to only be returning the entire multidimensional array.

    There are several issues with your attempted solution:

    1. WIN_COMBINATIONS is an array of indices. These indices are numeric, so they will never be 'X' or 'O'. You have to check whether their corresponding values are 'X' or 'O'.

    2. or is a control-flow operator intended for do_this or fail scenarios. The boolean "or" operator is ||. Using or instead of || might work but may have unexpected results due to its lower precedence. You almost always want ||.

    3. The expression array.all? { |element| element == 'X' || element == 'O' } checks whether all elements are either 'X' or 'O'. It would be true for ['X','O','O'] and false for ['X',' ','O']. That's because you put the conditional inside the block. What you want is to check whether the elements are all 'X', or all 'O':

      array.all?('X') || array.all?('O')
      
    4. Your method's return value is the result of WIN_COMBINATIONS.each { ... } and Array#each always returns the array itself (i.e. WIN_COMBINATIONS) regardless of the blocks' result. To get the first element matching a condition, use find.

    Let's apply all this to your code. Given this board:

    @board = %w[
    X - O
    O X -
    - - X
    ]
    

    You could get the first matching combination via:

    WIN_COMBINATIONS.find do |indices|
      values = @board.values_at(*indices)
      values.all?('X') || values.all?('O')
    end
    #=> [0, 4, 8]
    

    values_at returns the values for the corresponding indices (* transforms the indices array to a list of arguments, so values_at(*[0,1,2]) becomes values_at(0,1,2)). The block's 2nd line then checks whether these values are all 'X', or all 'O'. Once this evaluates to true, the loop breaks and find returns the matching element. (or nil if there was no match)