Ruby - Building the Tic Tac Toe game

My first encounter with Ruby has been quite wonderful. Even with the guidance by the Flatiron curriculum and the team there, it was a really big challenge for me. However, we know have a working game! I want to share my learnings below in hope that maybe it would help others - and even more so, for myself to keep going!

Below is my first attempt to rebuild a basic game of Tic Tac Toe.

HOW THE GAME WORKS

  1. Two player starts with a blank board of 9 spaces on a 3 x 3 grid

  2. Each play takes turn to place either an “X” or “O” (a marker) on the board

  3. The game is won when one player achieve three markers in a row - horizontally, vertically, or diagonally

  4. The game is draw when the board is full and there is no winning moves

  5. The program needs to guide players along the way and announce the winner if game is won

SETTING UP THE GAME IN BIN

#!/usr/bin/env ruby

require_relative '../lib/tic_tac_toe'
puts "Welcome to Tic Tac Toe!"
board = [" ", " ", " ", " ", " ", " ", " ", " ", " "]
play(board)

SETTING UP THE BOARD & DEFINE WINNING COMBINATIONS

# -> lib/tic_tac_toe.rb

def display_board(board)
  puts " # | # | # "
  puts "-----------"
  puts " # | # | # "
  puts "-----------"
  puts " # | # | # "
end

# TROUBLESHOOTING: This was easy to get wrong because of all the spacing and number of dashes. The test specs was written quite specifically so you should pay attention to the details.

DEFINE PLAYER’S MOVES AND CONVERT THEM TO INTEGERS

def input_to_index(user_input)
  user_input.to_i - 1
end

def player_move(board, index, marker)
 board[index] = marker
end

# TROUBLESHOOTING: I was amazed at how simple it is to convert a player’s input into integer ( .to_i) Super important: the use of = (assignment) is different then == (comparison). It took me 2 hours to figure out that I got an extra =.

CONDITIONS LOGIC TO CHECK IF POSITION IS TAKEN AND IF IT’S A VALID MOVE

def position_taken? (board, index)
  if board[index] == "" || board[index] == " " || board[index] == nil
    return false
  else
    return true
  end
end

def valid_move?(board, index)
  if !position_taken?(board, index) && (index).between?(0,8)
    return true
  else 
    return false
  end
end

# TROUBLESHOOTING: The use of ! in front of !positiontaken?(board, index) reads as position is NOT taken. This is a better way to set condition vs. before I was using the positiontaken?(board, index) == false. Linking || (or) && (and) is also very helpful.

ASKING FOR PLAYERS’ INPUTS, KEEP TRACK OF THE TURNS AND PLAYER’S MARKERS

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

def turn(board)
  puts "Please enter 1-9:"
  user_input = gets.strip
  index = input_to_index(user_input)
  if valid_move?(board, index)
    player_move(board, index, current_player(board))
    display_board(board)
  else
    turn(board)
  end
end

def turn_count(board)
  counter = 0
  board.each {|space|
    if space == "X" || space == "O"
      counter += 1   
    end
  }
  counter
end

# TROUBLESHOOTING: Again I was amazed at how simple it is to write the second the last line above: Is turncount(board) is divisible by 2 -> return “X”, if not, return “O”. It’s important that the playermove includes the current_player(board) so as to display the marker “X” or “O”.

CONDITIONS LOGOC TO FIND OUT IF THE GAME IS WON, FULL, DRAW, OR OVER

WIN_COMBINATIONS = [
  [0, 1, 2],
  [3, 4, 5],
  [6, 7, 8],
  [0, 3, 6],
  [1, 4, 7],
  [2, 5, 8],
  [0, 4, 8],
  [2, 4, 6]
]

def won?(board)
  WIN_COMBINATIONS.each do |single_win_combo|
    win_index_1 = single_win_combo[0]
    win_index_2 = single_win_combo[1]
    win_index_3 = single_win_combo[2]
    
    position_1 = board[win_index_1]
    position_2 = board[win_index_2]
    position_3 = board[win_index_3]
   
    if position_1 == position_2 && position_2 == position_3 && position_taken?(board, win_index_1)
      return single_win_combo
    end
  end
  return false
end

def full?(board)
  if board.any? {|index| index == nil || index == " "}
    return false
  else
    return true
  end
end

def draw?(board)
   if !won?(board) && full?(board)
     return true
   elsif!full?(board) && !won?(board)
     return false
   else won?(board)
     return false
   end
end

def over?(board)
  if draw?(board) || won?(board) || full?(board) 
    return true
  else
    return false
  end
end

# TROUBLESHOOTING: It’s important to think of position_1, position_2, position_3 as reusable and that they will cycle through all 9 spaces on the board. The elegance of comparing them to each other also got me. In draw?(board), the appearance and sequence of won?(board) and full?(board) matter.

SET UP FOR THE ENTIRE PLAY AND WINNER ANNOUNCEMENT

def winner(board)
  if won?(board)
    return board[won?(board)[0]]
  end
end

def play(board)
  counter = 0
  until counter == 9
  turn(board)
  counter += 1
  end
end

def play(board)
  until over?(board)
    turn(board) 
  end
  if won?(board)
    winner(board) == "X" || winner(board) == "O"
    puts "Congratulations #{winner(board)}!"
  else draw?(board)
    puts "Cat\'s Game!"
  end
end

# TROUBLESHOOTING: I had a hard time (still do) associating and differentiating won?(board)[0] and single_win_combo and position_1. I think an important take away for me is the difference between being INSIDE the method vs. ability to use output OUTSIDE of the method.

Reach out if you’re learning too and want to share.