1

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
Stefan
  • 109,145
  • 14
  • 143
  • 218
  • Consider defining the constant `X = ['X','X','X']`, then the there is a winner with `'X'`'s if `WIN_COMBINATIONS.include?(X)`. Similar for `'O'`. – Cary Swoveland Feb 02 '20 at 04:02

2 Answers2

1

[...] 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)

Stefan
  • 109,145
  • 14
  • 143
  • 218
  • Thank you very much, I'll keep those points in mind. Also sorry for the bad title. – Diego Vasquez Feb 02 '20 at 17:53
  • @DiegoVasquez you're welcome. Don't forget to "accept" an answer by clicking the gray checkmark. (no need to rush, you can wait some time for more answers) – Stefan Feb 02 '20 at 18:03
0

Here is how I would approach the problem:

class TicTacToe
  class OccupiedError < StandardError; end

  attr_reader :rows

  def initialize
    @rows = 3.times.map{ Array(3, nil) }
  end

  def place!(player, x:, y:)
    raise ArgumentError, "player must be :x or :o" unless [:x, :o].include?(player)
    raise OccupiedError, "slot is already occupied" unless @rows[y][x].nil?
    @rows[y][x] = player
  end

  # gets an array of columns instead of rows.
  def columns
    (0..2).map { |n| @rows.map {|row| row[n] } }
  end

  def diagonals
    [
      [@rows[0][0], @rows[1][1], @rows[2][2]], # lrt
      [@rows[0][2], @rows[1][1], @rows[2][0]]  # rtl
    ]
  end

  def all_combos
    rows + columns + diagonals
  end

  # checks all the horizontal, vertical and diagonal combinations
  def check_for_winner
    # checks all combos for three in a row
    (all_combos.find{ |a| a.all?(:x) || a.all?(:o) })&.first
  end
end

In the initialize method we create a 3*3 array which represents all the positions on the board. This makes it a lot easier since its already grouped in rows. Intead of an empty string use nil to represent an empty square as nil is falsy.

When we want to check for a winner we gather up the rows, columns and the two diagonals into an array of arrays:

[1] pry(main)> game.rows
=> [[:o, :o, :o], [nil, :x, :x], [:x, nil, nil]]
[2] pry(main)> game.all_combos
=> [[:o, :o, :o],
 [nil, :x, :x],
 [:x, nil, nil],
 [:o, nil, :x],
 [:o, :x, nil],
 [:o, :x, nil],
 [:o, :x, nil],
 [:o, :x, :x]]

From there we just have to check if any of them are all :x or :o. We don't actually have to list the winning combinations. In this case game.check_for_winner will return :o.

max
  • 96,212
  • 14
  • 104
  • 165