12.3 Tic-Tac-Toe

When two players initiate a game of tic-tac-toe, what is the first thing they do? They draw a board. Therefore, we will worry about the board first. A key object we will need is a Board object that stores the tic-tac-toe board. Think about the properties of such a board. A board is typically a large square made up of 3  × 3 smaller squares. At the start of the game all the little squares will be empty, but later they will be filled in with X’s and O’s as the game progresses. Therefore, we will need a 3  × 3 array that is initially empty and can be filled in later while the game is being played. Using objects, we will be able to design the board construction.

Gem of Wisdom

In Example 12-1, we use a shorthand way of declaring an array and its members. Array.new(5) { 3 } creates an array of five elements, with each value initialized to 3. Lines 8–10 declare an array (line 8), with each element being an array initialized to EMPTY_POS (line 9).

Using our knowledge of arrays and objects, we are able to set up the board constructor as shown in Example 12-1.

Example 12-1. Initial board and constructor
     1 class Board
     2 
     3 	BOARD_MAX_INDEX = 2
     4 	EMPTY_POS = ' '
     5 
     6 	def initialize(current_player)
     7 		@current_player = current_player
     8 		@board  = Array.new(BOARD_MAX_INDEX + 1) {
     9 			Array.new(BOARD_MAX_INDEX + 1) { EMPTY_POS }
    10 		}
    11 	end
    12 end
  • Line 3 defines the largest index (remember, array indices start at 0).

  • Line 4 defines what is considered an empty position, or a position not occupied by either an X or an O.

  • Lines 6–11 define the constructor for our board.

  • Line 7 assigns an instance variable that represents the current player.

  • Lines 8–10 create the board instance variable, a 3  × 3 array.

The main program (tictactoe.rb) will use the board class. The start of the main program is given in Example 12-2.

Example 12-2. Beginning of tictactoe.rb
    1 require_relative 'board.rb'
    2 
    3 puts "Starting tic-tac-toe..."
    4 players = ['X', 'O']
    5 current_player = players[rand(2)]
    6 b = Board.new(current_player)
    7 b.display()
    8 puts
  • Lines 4 and 5 randomly pick an initial player (either X or O).

  • Line 6 creates an instance of the Board class and assigns it to b.

  • Lines 7 and 8 display the initial blank board and output a fresh blank line for readability.

The display method (which is part of board.rb) is given in Example 12-3.

Example 12-3. Display method for Board class
     1 def display
     2 	puts "+- - - - - -+"
     3 	for row in 0..BOARD_MAX_INDEX
     4 		# print has to be used when we don't want to output a line break
     5 		print "| "
     6 		for col in 0..BOARD_MAX_INDEX
     7 			s = @board[row][col]
     8 			if s == EMPTY_POS
     9 				print col + (row * 3) + 1
    10 			else
    11 				print s
    12 			end
    13 			print " | "
    14 		end
    15 		puts "
+- - - - - -+"
    16 	end
    17 end
  • Line 2 prints a row of dashes (the top of the board).

  • Line 3 starts an outer loop that runs through each row.

  • Line 6 starts an inner loop that traverses each column.

  • Line 7 assigns the current cell to the variable s.

  • Lines 8–12 print the number of the cell if it’s currently unoccupied or the current occupant if the cell is occupied.

Note the difference between print and puts: print simply writes the characters passed to it without writing an end of line upon completion. Thus, if one wishes to continue writing to a given line, print would be used. Remember, however, that if using print and a new line is desired, the newline character ( ) must be entered, as shown in line 15. This is in contrast to puts, which automatically inserts the newline character every time.

Returning to our main program, the key loop that runs the game will be implemented. The code is presented in Example 12-4. (The code begins at line 1; however, this is a continuation of tictactoe.rb.)

Example 12-4. Continuation of tictactoe.rb
     1 while not b.board_full() and not b.winner()
     2       b.ask_player_for_move(current_player)
     3       current_player = b.get_next_turn()
     4       b.display()
     5       puts
     6 end
     7 
     8 if b.winner()
     9    puts "Player " +  b.get_next_turn() + " wins." 
    10 else
    11    puts "Tie Game."
    12 end
    13 puts "Game Over"
  • Line 1 starts a loop that continues as long as the board is not full and there is no winner.

  • Line 2 prompts the current player for a move.

  • Line 3 gets the next player.

  • Lines 4 and 5 display the current board.

  • Lines 8–13 print the winner’s name if there was a winner, or “Tie Game” if the game ended in a tie.

The loop ends when there is a winner or there is a full board detected. At this point, you can see we need to discuss the board_full method, the winner method, and the get_next_turn method. The code presented in Example 12-5 is for the board_full method. This method determines if the board is full, meaning that no more pieces may be placed.

Example 12-5. board_full method
     1 def board_full
     2 	for row in 0..BOARD_MAX_INDEX
     3 		for col in 0..BOARD_MAX_INDEX
     4 			if @board[row][col] == EMPTY_POS
     5 				return false
     6 			end
     7 		end
     8 	end
     9 	# Since we found no open positions, the board is full
    10 	return true
    11 end
  • Line 4 checks each cell to see if it is unoccupied. If at least one cell is unoccupied, the board is not full and the game must continue.

  • Line 10 returns true in the event that none of the cells are open for possible capture.

The winner method is more complex. Essentially, we must check every row, every column, and every diagonal for a winner. The method is presented in Example 12-6.

Example 12-6. Winner method
     1 def winner
     2 	winner = winner_rows()
     3 	if winner
     4 		return winner
     5 	end
     6 	winner = winner_cols()
     7 	if winner
     8 		return winner
     9 	end
    10 	winner = winner_diagonals()
    11 	if winner
    12 		return winner
    13 	end
    14 	# No winners
    15 	return
    16 end

The winner method uses the helper methods winner_rows, winner_cols, and winner_diagonals, which return the player’s symbol if the player won by connecting the rows, columns, or diagonals with her or his pieces, respectively. If winner is set, then we know that the current value of winner is the player who won the game. Otherwise, we return nothing, signifying there is no winner yet.

The methods winner_rows, winner_cols, and winner_diagonals are good examples of class methods, as explained in Chapter 9. They are all fairly straightforward, as they all look for three of the same values with regard to their given task. The method winner_rows is shown in Example 12-7.

Example 12-7. winner_rows method
     1 def winner_rows
     2 	for row_index in 0..BOARD_MAX_INDEX
     3 		first_symbol = @board[row_index][0]
     4 		for col_index in 1..BOARD_MAX_INDEX
     5 			if first_symbol != @board[row_index][col_index]
     6 				break
     7 			elsif col_index == BOARD_MAX_INDEX and first_symbol != EMPTY_POS
     8 				return first_symbol
     9 			end
    10 		end
    11 	end
    12 	return
    13 end

Gem of Wisdom

Aside from being able to return values, the return keyword immediately stops execution of the current method and returns to the one from which it was called. This is used heavily in the winner method. If the winner is found in the rows, the columns and diagonals are not searched. Likewise, if the winner is found in the columns, the diagonals are not searched.

  • Line 2 begins an outer loop to look for a winner across a row. For each row, all the columns are checked. The variable first_symbol contains the symbol that must match.

  • Line 3 initializes first_symbol for row 0.

  • Lines 4–10 provide an inner loop that looks at all elements in the given column. If a cell does not match the first_symbol value, then it is not a winning combination.

    For example, if the first_symbol value was initially an O and now we encounter an X in the same column, then this is not a winning combination. If we reach the end of the columns, we have a winner, and we return the winner as its name is in the first_symbol column.

  • Line 7 contains a final check to make sure we have not found three empty positions in a column. If we do not return a winner, then we simply return on line 12 (this is essentially returning a nil or false value) to the caller of this method.

The next method, presented in Example 12-8, is very similar in that it looks for a winning column. This time, a given column is checked, and we travel down the column checking to see if we have all matching symbols.

Example 12-8. winner_cols method
     1 def winner_cols
     2 	for col_index in 0..BOARD_MAX_INDEX
     3 		first_symbol = @board[0][col_index]
     4 		for row_index in 1..BOARD_MAX_INDEX
     5 			if first_symbol != @board[row_index][col_index]
     6 				break
     7 			elsif row_index == BOARD_MAX_INDEX and first_symbol != EMPTY_POS
     8 				return first_symbol
     9 			end
    10 		end
    11 	end
    12 	return
    13 end

Finally, we look for a win across a diagonal. This is inherently more difficult because it requires a backward traversal of the columns. This is done with the winner_diagonals method shown in Example 12-9.

Example 12-9. winner_diagonals method
     1 def winner_diagonals
     2 	first_symbol = @board[0][0]
     3 	for index in 1..BOARD_MAX_INDEX
     4 		if first_symbol != @board[index][index]
     5 			break
     6 		elsif index == BOARD_MAX_INDEX and first_symbol != EMPTY_POS
     7 			return first_symbol
     8 		end
     9 	end
    10 	first_symbol = @board[0][BOARD_MAX_INDEX]
    11 	row_index = 0
    12 	col_index = BOARD_MAX_INDEX
    13 	while row_index < BOARD_MAX_INDEX
    14 		row_index = row_index + 1
    15 		col_index = col_index - 1
    16 		if first_symbol != @board[row_index][col_index]
    17 			break
    18 		elsif row_index == BOARD_MAX_INDEX and first_symbol != EMPTY_POS
    19 			return first_symbol
    20 		end
    21 	end
    22 	return
    23 end
  • Line 2 initializes our search with the upper-lefthand corner of the board.

  • Lines 3–9 traverse the diagonal from the top left to the bottom right, continuing as long as there is a match.

  • Line 10 sets the initial value to the top right.

  • Lines 11–20 check the diagonal from the top right to the bottom left. If no matches are found, we return nothing (line 22).

The only methods we have not yet described are the ask_player_for_move method and the validate_position method, which simply prompt the user for a move and ensures that the move is allowed. The ask_player_for_move method is presented in Example 12-10.

Example 12-10. ask_player_for_move method
     1 def ask_player_for_move(current_player)
     2 	played = false
     3 	while not played
     4 		puts "Player " + current_player + ": Where would you like to play?"
     5 		move = gets.to_i - 1
     6 		col = move % @board.size
     7 		row = (move - col) / @board.size
     8 		if validate_position(row, col)
     9 			@board[row][col] = current_player
    10 			played = true
    11 		end
    12 	end
    13 end
  • Line 3 starts a loop that keeps processing until a valid move is obtained. The flag played is initially set to false.

  • Line 4 asks the user for her or his move.

  • Line 5 obtains the user’s response with a call to gets, which obtains a string and then converts it to an integer.

  • Line 6 converts the number 1–9 into column number 0–2, and line 7 converts it into a row number. These conversions stem from the equation that took each cell of the array and assigned a cell number. Convince yourself that lines 6 and 7 are correct.

  • Line 8 calls another internal method, validate_position, which is a method that makes sure the user chooses a spot on the board and a spot that is not already taken. If a valid position is obtained, then line 10 sets the played flag to be true, and the loop will end when line 3 is encountered. For an invalid move, the played flag is not set to true, so the loop will continue again.

The validate_position method is given in Example 12-11.

Example 12-11. validate_position method
     1 def validate_position(row, col)
     2 	if row <= @board.size and col <= @board.size
     3 		if @board[row][col] == EMPTY_POS
     4 			return true
     5 		else
     6 			puts "That position is occupied."
     7 		end
     8 	else
     9 		puts "Invalid position."
    10 	end
    11 	return false
    12 end
  • Line 2 makes sure the row and column are within the range of the board. We know it’s a three row by three column game, but by using the size variable we could easily expand this to larger board sizes for other games, like Connect Four. In this case the size is three elements, 0, 1, and 2. The size was established earlier, but this will allow us to easily change the size of the board.

  • Line 3 checks to make sure the user has selected a position that was previously empty. If so, true is returned, and we are done.

  • Lines 5–7 handle the case for when a user selects an occupied position.

  • Lines 8–10 handle the case for when a user selects a position off the board.

  • Line 11 returns false, which indicates that an invalid move occurred. Note that line 11 is reached only when an invalid move is attempted.

Finally, we need to discuss the get_next_turn method, and we are done. It is very simple and is shown in Example 12-12.

Example 12-12. get_next_turn_method
    1 def get_next_turn
    2 	if @current_player == 'X'
    3 		@current_player = 'O'
    4 	else
    5 		@current_player = 'X'
    6 	end
    7 	return @current_player
    8 end
  • Line 2 checks to see if we were using an X, and if so, we change to the O in line 3; otherwise, it was an O, so in line 5 we turn it into an X.

At this point, we have created a working game of tic-tac-toe that you can play against yourself or a friend. The code written for this game encompasses almost all the topics covered in this book. If you understood it all, give yourself a pat on the back. If you are frustrated with this chapter and do not understand all the ideas presented, it is a good idea to go back to previous chapters and play around with the Ruby concepts presented in those chapters.

Don’t get too comfortable if you’ve done well thus far. The next section will add artificial intelligence to our tic-tac-toe game, enabling you to play against the computer.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.232.239