05
Jan
2012
0 Comments

Ruby Tutorial - Make a Tic Tac Toe game

I started playing around with Ruby recently and it is a really fun language, albeit the syntax can look a little strange at first. After reading all the fantastic information over at rubylearning.com I wanted to use my new acquired knowledge to build something. I first built a number guessing game, then a hang man game, both very simple and not very interesting. I wasn't going for interesting though, I was just trying to build some simple things as an exercise. After those two, I built a tic tac toe game, still not extremely interesting but more so than the others because it feels like the game is actually playing against you rather than just telling you whether you're right or wrong.
You can download the full source to Ruby Tic Tac Toe here: tic.rb
Lets start by defining our initialize method, the first thing we want to do is create a container to hold the 9 places on the tic tac toe board. It is a 3 by 3 grid so we will name them A,B,C across and 1,2,3 down. Lets store them in a Ruby hash object with their default values set to spaces indicating they are empty:
@places = {
  "a1"=>" ","a2"=>" ","a3"=>" ",
  "b1"=>" ","b2"=>" ","b3"=>" ",
  "c1"=>" ","c2"=>" ","c3"=>" "
}
Now that we have defined the 9 slots, lets define the 8 different possible winning columns. These are the slot  (the 3 horizontal, 3 vertical and 2 diagnal) combinations a player must occupy to win the game. We will store these in an array called @columns where each index is a nested array defining the positions needed to win.
@columns = [
  ['a1','a2','a3'],
  ['b1','b2','b3'],
  ['c1','c2','c3'],

  ['a1','b1','c1'],
  ['a2','b2','c2'],
  ['a3','b3','c3'],

  ['a1','b2','c3'],
  ['c1','b2','a3']
]
Next we want to randomly determine who is X and who is O and assign each to a variable called @cpu and @user:
@cpu = rand() > 0.5 ? 'X' : 'O'
@user = @cpu == 'X' ? 'O' : 'X'
Now lets give each player a name; lets name cpu as "Ruby" and ask the user to input their name and store each in the variables @cpu_name and @user_name respectively:
@cpu_name = "Ruby"
put_line
puts " RUBY TIC TAC TOE"
puts " What is your name?"
STDOUT.flush
@user_name = gets.chomp
put_bar
You will notice I called two methods here that are not yet defined; put_line and put_bar these are just convenience methods to print a line or a bar. We will define those methods shortly. For the last part of the initialize method we will let whoever is X make their first move:
if(@user == 'X')
  user_turn
else
  cpu_turn
end
That concludes the initialize method, the user_turn and cpu_turn methods are what we will call to allow either the user or the cpu make a move. We will define those later; for now, lets define the put_line and put_bar methods:
def put_line
  puts "-----------------------------------------------------------------------------"
end

def put_bar
  puts "#############################################################################"
  puts "#############################################################################"
end
Now we have enough information to define the method that draws our tic tac toe board. Lets also print out the user name and who is X and who is O to make it easier for the player:
def draw_game
  puts ""
  puts "#{@cpu_name}: #{@cpu}"
  puts "#{@user_name}: #{@user}"
  puts ""
  puts "   a b c"
  puts ""
  puts " 1 #{@places["a1"]}|#{@places["b1"]}|#{@places["c1"]}"
  puts "   -----"
  puts " 2 #{@places["a2"]}|#{@places["b2"]}|#{@places["c2"]}"
  puts "   -----"
  puts " 3 #{@places["a3"]}|#{@places["b3"]}|#{@places["c3"]}"
end
Remember, the @places hash defaults to having a space for each slot. When a player makes a move it will then contain it (X or O) instead, automatically drawing it to the board the next time we render it. Now lets define the cpu_turn method:
def cpu_turn
  move = cpu_find_move
  @places[move] = @cpu
  put_line
  puts "#{@cpu_name} marks #{move.upcase}"
  check_game(@user)
end
The first thing happening in this method is we call another function called cpu_find_move (we will define it later) which analyses the positions available to determine the best move. We then assign the returned value to the @places hash and output the move notifying the user what move was just placed. Finally we call a soon to be defined method called check_game which checks to see if anyone has won or if it is a stalemate. This method expects a parameter letting it know whose turn is next if the game is not over. Before we define the cpu_find_move method lets first define two functions that it relies on to analyse the board. The first function is called times_in_column which expects 2 parameters, the first being an array which is the column (of the columns in the @columns array) on the board we want to analyse and the second is what we are looking for (either X or O); and it will return how many times the item is in the column:
def times_in_column arr, item
  times = 0
  arr.each do |i|
    times += 1 if @places[i] == item
    unless @places[i] == item || @places[i] == " "
      #oppisite piece is in column so column cannot be used for win.
      #therefore, the strategic thing to do is choose a dif column so return 0.
      return 0
    end
  end
  times
end
You will notice we are checking if any of the slots in the column are either a space or the item we are looking for. If they are neither then the only thing it can possibly be is the other player (X or O). And since we cannot win on a column that is occupied by the other player we instantly return 0. The next function we will define is called empty_in_column this one only expects one parameter which is also a column reference and just returns the first empty slot it finds in the column:
def empty_in_column arr
  arr.each do |i|
    if @places[i] == " "
      return i
    end
  end
end
Now that we have both of those defined we can now define the cpu_find_move method. The first thing we will want to do is determine if there is any move possible that will cause a win and if so, return it. You can determine this by checking to see if any of the @columns that define a win already contain 2 of cpu's moves which indicates a 3rd move into that column will be a win:
@columns.each do |column|
  if times_in_column(column, @cpu) == 2
    return empty_in_column column
  end
end
If there is no moves available that will cause a win, the next thing to look for is if there are any moves available that will cause a loose. What I mean is, if there is a place the user can move to win. If there is, we need to move there first to block it:
@columns.each do |column|
  if times_in_column(column, @user) == 2
    return empty_in_column column
  end
end
If neither of these find anything then there is no possible winning moves either way. So what we want to do now is build up to a winning move. To do this we check to see if any column has at least one of cpu's moves so we can add to it:
@columns.each do |column|
  if times_in_column(column, @cpu) == 1
    return empty_in_column column
  end
end
If that didn't find anything either, then at this point we can just move to any empty slot. To make it seem more natural lets try to find a random empty slot, but if our first random slot is not empty, then at that point lets just move to the first empty slot we find:
#no strategic spot found so just find a random empty
k = @places.keys;
i = rand(k.length)
if @places[k[i]] == " "
  return k[i]
else
  #random selection is taken so just find the first empty slot
  @places.each { |k,v| return k if v == " " }
end
That is the final part of the cpu_find_move function. So we can move on to creating the user_turn method. What we want to do here is draw the board so the user can decide where to move and then ask them to type in the slot they would like to place a move. We need to check their input to make sure it is either a valid move or the word "exit." If it is a valid move, make the move, or if it is "exit" then exit the program otherwise notify them to try again:
def user_turn
  put_line
  puts " RUBY TIC TAC TOE"
  draw_game
  puts " #{@user_name}, please make a move or type 'exit' to quit"
  STDOUT.flush
  input = gets.chomp.downcase
  put_bar
  if input.length == 2
    a = input.split("")
    if(['a','b','c'].include? a[0])
      if(['1','2','3'].include? a[1])
        if @places[input] == " "
          @places[input] = @user
          put_line
          puts "#{@user_name} marks #{input.upcase}"
          check_game(@cpu)
        else
          wrong_move
        end
      else
        wrong_input
      end
    else
      wrong_input
    end
  else
    wrong_input unless input == 'exit'
  end
end
We already know we still need to define the check_game method, we also now need to define the wrong_move and wrong_input methods. If the user input a valid slot but it is not empty then we call wrong_move but if it is not a valid slot at all then we call wrong_input. We define these methods like so:
def wrong_input
  put_line
  puts "Please specify a move with the format 'A1' , 'B3' , 'C2' etc."
  user_turn
end

def wrong_move
  put_line
  puts "You must choose an empty slot"
  user_turn
end
Now we can proceed to creating the check_game method. first we will want to see if the cpu or the user has won, if so, output who won and exit the game. If not, check to see if there are any moves left and call the appropriate players turn function, otherwise, output that the game is over and it is a draw. Here is the method definition:
def check_game(next_turn)

  game_over = nil

  @columns.each do |column|
    # see if cpu has won
    if times_in_column(column, @cpu) == 3
      put_line
      puts "Game Over -- #{@cpu_name} WINS!!!"
      game_over = true
    end
    # see if user has won
    if times_in_column(column, @user) == 3
      put_line
      puts "Game Over -- #{@user_name} WINS!!!"
      game_over = true
    end
  end

  unless game_over
    if(moves_left > 0)
      if(next_turn == @user)
        user_turn
      else
        cpu_turn
      end
    else
      put_line
      puts "Game Over -- DRAW!"
    end
  end
end
There is now only one method left to define; the moves_left method that determines if it is a stalemate or not. Here it is:
def moves_left
  slots = 0
  @places.each do |k, v|
    slots += 1 if v == " "
  end
  slots
end
And there you have it, you can now execute this program on the command line and it will play a game of tic tac toe with you. Like I said, I just started playing with Ruby, so if you see I did something wrong or find a way it could be improved, please feel free to let me know in the comments. I hope you enjoyed the tutorial.

Update: I made some changes since I posted this, you can check them out on github. You can also install the gem

Leave A Comment