Building 'Tic Tac Toe' part two
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:
- Computer checks to see if it can attack
- If it cannot attack it will see if it needs to defend
- 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.