In part one I got the basics of the game control working to a point where I felt I had an excellent foundation in which to build upon. In part two I wanted to start to build in some intelligence for the computer player rather than the computer aimlessly picking random squares and I wanted to tidy up my code - indentation etc.

I’m going to break it all down but here is my final code:

  1 # Initialize board hash
  2 # Draw board
  3 # Choose at random who goes first
  4 # Player/Computer has a turn if square available
  5 # check for winner
  6 
  7 require 'pry'
  8 
  9 # constants
 10 # rows columns and diagonals
 11 WINNING_LINES = [[1,2,3],[4,5,6],[7,8,9],[1,4,7],[2,5,8],[3,6,9],[1,5,9],[3,5,7]]
 12 
 13 # markers
 14 X = 'x'
 15 O = 'o'
 16 
 17 # Available squares
 18 def available_squares(squares)
 19   squares.select {|_,v| v == " "}.keys
 20 end
 21 
 22 # draw the board
 23 def draw_board(squares)
 24   system 'clear'
 25   puts "#{squares[1]}|#{squares[2]}|#{squares[3]}"
 26   puts "-----"
 27   puts "#{squares[4]}|#{squares[5]}|#{squares[6]}"
 28   puts "-----"
 29   puts "#{squares[7]}|#{squares[8]}|#{squares[9]}"
 30 end
 31 
 32 # check for winner method
 33 def check_for_winner(line, squares)
 34   if line.find {|l| l.all? {|k| squares[k] == X} }
 35     puts "player WINS!!!"
 36     true
 37   elsif line.find {|l| l.all? {|k| squares[k] == O} }
 38     puts "computer wins :("
 39     true
 40   end
 41 end
 42 
 43 # checks to see if two in a row
 44 def two_in_a_row(hsh, mrkr)
 45   if hsh.values.count(mrkr) == 2
 46     hsh.select{|k,v| v == ' '}.keys.first
 47   else
 48     false
 49   end
 50 end
 51 
 52 # player/computer picks square methods
 53 # player 1
 54 def player1(squares)
 55   if available_squares(squares).any?
 56     puts "Choose an available square from #{available_squares(squares)}"
 57     i = gets.chomp.to_i
 58     if available_squares(squares).include?(i)
 59       squares[i] = X
 60     else
 61       player1(squares)
 62     end
 63     draw_board(squares)
 64   end
 65 end
 66 
 67 # player 2
 68 def player2(line, squares)
 69   puts "Computer chooses a square"
 70   sleep 0.5
 71 
 72   defend_this_square = nil
 73   attacked = false
 74   
 75   # attack 
 76   WINNING_LINES.each do |l|
 77     defend_this_square = two_in_a_row({l[0] => squares[l[0]], l[1] => squares[l[1]], l[2] => squares[l[2]]}, O)
 78     if defend_this_square
 79       squares[defend_this_square] = O
 80       attacked = true
 81       break
 82     end
 83   end
 84   
 85   # defend  
 86   if attacked == false
 87     WINNING_LINES.each do |l|
 88       defend_this_square = two_in_a_row({l[0] => squares[l[0]], l[1] => squares[l[1]], l[2] => squares[l[2]]}, X)
 89       if defend_this_square
 90         squares[defend_this_square] = O
 91         break
 92       end 
 93     end 
 94   end
 95   squares[available_squares(squares).sample] = O unless defend_this_square
 96   draw_board(squares)
 97 end
 98 
 99 # play again?
100 play_again = 'y'
101 
102 while play_again == 'y'
103 
104   # initialize the empty hash that will store the board squares
105   board_squares = {1 => " ",2 => " ",3 => " ",4 => " ",5 => " ",6 => " ",7 => " ",8 => " ",9 => " "}
106 
107   # players stored in array so player can be chosen at random
108   players = ["player1", "player2"]
109 
110   # setting the who goes first variable
111   goes_first = players.sample
112 
113   # show players the empty board
114   draw_board(board_squares)
115 
116   # Conditional that checks which loop to execute: player 1 or player 2
117   if goes_first == 'player1'
118     begin
119       break if check_for_winner(WINNING_LINES, board_squares) == true
120       player1(board_squares)
121       break if check_for_winner(WINNING_LINES, board_squares) == true
122       player2(WINNING_LINES, board_squares)
123     end until available_squares(board_squares).empty?
124   elsif goes_first == 'player2'
125     begin
126       break if check_for_winner(WINNING_LINES, board_squares) == true
127       player2(WINNING_LINES, board_squares)
128       break if check_for_winner(WINNING_LINES, board_squares) == true
129       player1(board_squares)
130     end until available_squares(board_squares).empty?
131   end
132 
133   sleep 0.5
134   puts "GAME OVER"
135 
136   # play again?
137   sleep 0.5
138   puts "Play again? (y/n)"
139   play_again = gets.chomp
140 
141 end

So this program is effectively in two parts. The first part holds all of the methods and constants required in the game and the second part is the game itself which resides inside a while loop. The while loop basically checks to see if the play_again variable is set to yes or no.

First off with have a constant variable called WINNING_LINES. This is an array of arrays that is storing the lines on our tic tac toe board. There are 8 lines in total and I will iterate through these arrays when I am checking for certain conditions in my game methods.

A constant variable is denoted by a leading capital although the convention is to write them all in CAPS. The scope of a constant differs to local variables. You should use a constant variable when its value shouldn’t be changed although beware it is possible to change a constant variable in ruby with a little warning from the interpreter.

I have then stored ‘x’ & ‘o’ in the constants X and O for use later on in our game methods.

I covered the methods draw_board, available_squares and player1 in part 1 and they remain unchanged. I also wrote a player2 method but in this part I began to build this method out to give it some added power. In addition to these methods I’ve added two new methods, two_in_a_row and check_for_winner which I’ll explain shortly. First lets look at the player2 method before the change.

Before:

1 def player2(squares)
2   puts "Computer chooses a square"
3   sleep 0.5
4   i = available_squares(squares).sample
5   squares[i] = O
6   draw_board(squares)
7 end

You can see that at this point the method just outputs a message and then assigns a random square to a local variable named i from the available_squares method. Using the i variable it then assigns an ‘o’ to the square on the board and then draws the board.

At this point the computer is hideously disadvantaged as all it can do is select a random available square. It cannot attack and it cannot defend so that is what I needed to build in.

We’ll look at how the two new methods work shortly but for now just know the following:

  • The two_in_a_row method checks to see if there is a combination that exists whereby a player has two of their markers in a line and the remaining square is emtpy. For example true if ‘x’ ‘ ‘ ‘x’ false if ‘x’ ‘o’ ‘x’.
  • The check_for_winner method checks to see if either a line is filled exclusively with either all ‘x’ or ‘o’ for example true if ‘x’ ‘x’ ‘x’ or ‘o’ ‘o’ ‘o’.

I added a parameter to the method to start with so that we can now pass in the WINNING_LINES as well as the board_squares.

def player2(line, squares)

I added two new local variables to work with within the method named defend_this_square and attacked and assigned them nil and false respectively.

defend_this_square = nil
attacked = false

Now we want one of three things will happen on the computers turn:

  1. Computer checks to see if it can attack
  2. If it cannot attack it will see if it needs to defend
  3. If it doesn’t need to defend it will just choose a random available square.

Computer attacks

1 # attack 
2 WINNING_LINES.each do |l|
3   defend_this_square = two_in_a_row({l[0] => squares[l[0]], l[1] => squares[l[1]], l[2] => squares[l[2]]}, O)
4   if defend_this_square
5     squares[defend_this_square] = O
6     attacked = true
7     break
8   end
9 end

The first part starting on line 2 and ending on line 9 is selecting each array (l) in the WINNING_LINES array.

Line 3 - For each of the arrays in the WINNING_LINES array we are reassigning defend_this_square with the result of the two_in_a_row method call that runs the check we described above that will either return a integer (which will also evaluate to true) or false.

Line 4 gets executed if defend_this_square is true (contains a number). If defend_this_square is true on an iteration of WINNING_LINES.each, two_in_a_row must have identified an instance where there were two ‘o’ and an empty square and it will have returned the empty square number into our defend_this_square variable.

Line 5 then gets executed which assigns board_squares[defend_this_square] with a ‘o’ therefor completing a line.

Next we reassign the attacked varaible as true so that option 2 and 3 do not get executed and finally on line 7 we break out of the loop.

Computer defends

If defend_this_square had not have evaluated to true on any of the iterations then the defence check would execute:

 1 # defend  
 2 if attacked == false
 3   WINNING_LINES.each do |l|
 4     defend_this_square = two_in_a_row({l[0] => squares[l[0]], l[1] => squares[l[1]], l[2] => squares[l[2]]}, X)
 5     if defend_this_square
 6       squares[defend_this_square] = O
 7       break
 8     end 
 9   end 
10 end

So here we can see that if the computer had attacked the attacked variable would now equal true and therefor the above would not be carried out. Assuming that the computer didn’t attack we once again iterate through each of the WINNING_LINES arrays and check for a two_in_a_row instance for X. If it finds an instance it assigns the empty square to defend_this_square and then assigns board_squares[defend_this_square] with an ‘o’ to stop the player from completing a line on their next go. After this it breaks out of the loop.

Finally if neither of these are carried out the computer chooses a random availble square:

squares[available_squares(squares).sample] = O unless defend_this_square

Unless defend_this_square evaluated to true squares[available_squares(squares).sample] = O.

So there we have the newly updated player2 method and we have a pretty good understanding of what two_in_a_row and check_for_winner does so now let us understand how they work.

two_in_a_row

1 # checks to see if two in a row
2 def two_in_a_row(hsh, mrkr)
3   if hsh.values.count(mrkr) == 2
4     hsh.select{|k,v| v == ' '}.keys.first
5   else
6     false
7   end
8 end

For this method to work we want to pass in two parameters, a hash and a marker. The marker is either the X or O constant variable declared at the top of our program. The hash is a little more complex.

This hash is created in the player2 method like so:

WINNING_LINES.each do |l|
  defend_this_square = two_in_a_row({l[0] => squares[l[0]], l[1] => squares[l[1]], l[2] => squares[l[2]]}, O)
end

So lets say we are on the iteration of WINNING_LINES.each where l is [4,5,6] and square numbers 4 & 6 contained ‘o’ and 5 was empty:

{l[0] => squares[l[0]], l[1] => squares[l[1]], l[2] => squares[l[2]]}

would be

{ 4 => 'o', 5 => ' ', 6 => 'o'}

The above is the hash which is passed into two_in_a_row. Next we ask if the values of X appears twice in the hash and if it does we select and return the first key that equals ‘ ‘ in our example it would return the integer 5. If the condition is not met the method returns false.

check_for_winner

 1 # check for winner method
 2 def check_for_winner(line, squares)
 3   if line.find {|l| l.all? {|k| squares[k] == X} }
 4     puts "player WINS!!!"
 5     true
 6   elsif line.find {|l| l.all? {|k| squares[k] == O} }
 7     puts "computer wins :("
 8     true
 9   end
10 end

In order for us to check if a player has 3 in a row we need to pass in the arrays in WINNING_LINES and the board_squares. Lines 3 to 5 and lines 6 to 8 essentially do the same thing in as much as one tests to see if player has won and the second tests to see if the computer has won and outputs a message if either is true.

So lets look at the first in detail.

line.find {|l| l.all? {|k| squares[k] == X} }

line is going to be our WINNING_LINES array and I am using the .find method to iterate through each array (l), and if all the elements in that array equal ‘x’ then it is true and the block is executed:

puts "player WINS!!!"
true

The game loop

Now we have all of the methods and constants our game requires to function lets call them in to our game loop.

 1 # play again?
 2 play_again = 'y'
 3 
 4 while play_again == 'y'
 5 
 6   # initialize the empty hash that will store the board squares
 7   board_squares = {1 => " ",2 => " ",3 => " ",4 => " ",5 => " ",6 => " ",7 => " ",8 => " ",9 => " "}
 8 
 9   # players stored in array so player can be chosen at random
10   players = ["player1", "player2"]
11 
12   # setting the who goes first variable
13   goes_first = players.sample
14 
15   # show players the empty board
16   draw_board(board_squares)
17 
18   # Conditional that checks which loop to execute: player 1 or player 2
19   if goes_first == 'player1'
20     begin
21       break if check_for_winner(WINNING_LINES, board_squares) == true
22       player1(board_squares)
23       break if check_for_winner(WINNING_LINES, board_squares) == true
24       player2(WINNING_LINES, board_squares)
25     end until available_squares(board_squares).empty?
26   elsif goes_first == 'player2'
27     begin
28       break if check_for_winner(WINNING_LINES, board_squares) == true
29       player2(WINNING_LINES, board_squares)
30       break if check_for_winner(WINNING_LINES, board_squares) == true
31       player1(board_squares)
32     end until available_squares(board_squares).empty?
33   end
34 
35   sleep 0.5
36   puts "GAME OVER"
37 
38   # play again?
39   sleep 0.5
40   puts "Play again? (y/n)"
41   play_again = gets.chomp
42 end

We covered up to line 19 in part 1. Assuming that player1 goes first (it’s the same for player2) we then go to line 20 where our begin end until starts. First off on line 21 we check to see if check_for_winner is true and if it is we break out and end game. If it is not true the player1 method is invoked and after that we check to see if check_for_winner is true again. We break if it is and we move on to player2 method if it is not. We continue this loop until check_for_winner is true or no are no available squares left.

Tidying up and tweaks

The ever helpfull guys over on the #ruby channel at irc made the following observations that I’ve duly corrected.

  • Don’t reassign constants in a loop, they should be constant values so put them at top of the script
  • else; false; end is equiv to else; return false; end as its the last expression of a method so no need for the return
  • General indentation that I’d missed
  • I orignally named my two_in_a_row method two_in_a_row?. You should only use a ‘?’ if your method is going to return a boolean and because it often returns a value the question mark should be omitted

Thanks:

Chris, Albert and Brandon at Tealeaf for reading and running my code and making suggestions for improvement and thanks Chris for writing the neat little two_in_a_row method and help me understand what was going on there. Hanmac, tobiasvl, jhass, shevy & workmad3 and the #ruby irc channel in general for always being on hand to help out without ever seeming to want anything in return.