Tic Tac Toe Game — Ruby style CLI

Christine Tran
Aug 23, 2017 · 10 min read

Argh!!!!!!!!

This was a challenge despite following the labs step by step. Ruby may be easy to understand but executing it is tough!

CLI — Command Line Applications/Interface are programs that you interact with entirely through the computer terminal and shell. There are no graphics beyond what’s shown/seen on the terminal after the program starts. There’s nothing to click, no folders, or desktop. CLIs communicate to the user through ASCII (numbers of zeros and ones) output and only accept input from a user with the same ASCII entered into a prompt.

Here’s what I’m understanding from all this Ruby and CLI practice while building the game:

1.) When it comes to building code, we should try to see what we’re coding from a computer’s point of view. Everything it sees is virtual. Essentially, we have to build everything from instructions to the objects being used. In this case of Tic Tac Toe, we have to build the objects — the board, the tokens, the players, the winning combinations, a full board, a tied board, a winning board, etc. Then we have to build the instructions. A computer doesn’t know how to play the game, so we have to instruct it on how to play. The instructions include the winning combinations, the movements, the turns each player takes, etc.

2.) They count starting from 0 (zero). Humans like to start with 1 (one). This is vital information.

3.) A computer can play against a human. With a CLI, that’s just it; it’s an interaction between a human and a computer. To have this interaction a human user needs to provide an input to the computer determining what they (the human) wants to do.

Humans:

How do we see Tic Tac Toe?

  • The board has 9 empty squares
  • There are 2 pieces of tokens — X and O
  • There are only 2 players

How do we play Tic Tac Toe?

  • Players take turns playing
  • 1st turn — Player 1
  • Player 1 places their token (X) in only 1 square of their choice
  • 2nd turn — Player 2
  • Player 2 places their token (O) in only 1 square of their choice, except they can’t touch the square that Player 1’s token already fills
  • 3rd turn- Player 1 …. and the cycle continues until a player wins or there’s a tie game
  • There are 8 winning combinations: Top row, Middle row, Bottom row, Left column, Middle column, Right column, Left diagonal, and Right diagonal
  • There’s a draw/tie when the entire board is filled with X’s and O’s but no winning combinations can be found

In short, the above outline is our pseudocode. Writing things/instructions in simple, plain english or descriptors can help us figure out how to write code for the program.

So let’s take the above pseudocode and build out some computer logic.

CLI/Programs

How do CLI/Programs see Tic Tac Toe?

  • The board has 9 empty squares => The board is an array of 9 empty strings

[“ ”, “ ”, “ ”, “ ”, “ ”, “ ”, “ ”, “ ”, “ ”,]

  • There are 2 pieces of tokens — X and O => X and O
  • There are only 2 players => What is a player/How do we define a player

How do CLI/Programs play Tic Tac Toe?

  • Players take turns playing => Define what a turn is; how will a player take turns playing?
  • 1st turn — Player 1 …. and the cycle continues until a player wins or there’s a tie game => iterate the game playing where players take turns; a player can’t place a token on a square that’s already taken
  • There are 8 winning combinations: Top row, Middle row, Bottom row, Left column, Middle column, Right column, Left diagonal, and Right diagonal => Define those combinations
  • There’s a draw/tie when the entire board is filled with X’s and O’s but no winning combinations can be found => Define what a draw/tie is; define what a filled board is

From pseudocode to code:

Taking the pseudocode, I’ll start building my code.

Build the Board

Define the variable board

puts board = [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']

How can we display the board to show that we have a 3x3 board?

  • We need to define a method called display_board
def display_board
puts row = [" " "|" " " "|" " "]
puts separator = "-----------"
puts row
puts separator
puts row
end

  • Board is complete. The board is viewed 2 ways — from the players (human) view and the code (computer). We would display the board from the program’s point of view (an array, because programs count starting with 0).
   board     (viewed by players)   (viewed by program)
| | 1 | 2 | 3 0 | 1 | 2
----------- ----------- -----------
| | 4 | 5 | 6 3 | 4 | 5
----------- ----------- -----------
| | 7 | 8 | 9 6 | 7 | 8

#board displaying the proper board[index] in its array format
def display_board(board)
puts " #{board[0]} | #{board[1]} | #{board[2]} "
puts "-----------"
puts " #{board[3]} | #{board[4]} | #{board[5]} "
puts "-----------"
puts " #{board[6]} | #{board[7]} | #{board[8]} "
end
display_board(board)

Note: We are using string interpolation with #{board[0]} because it’ll print (puts) the current state of the board, which the method `displayboard` will pass.

  • We’ll pass the board to every method as an argument (the object within the () so the helper methods can be reused to our logic.

User Inputs

With the board built, now we need to find how a user will choose a square to enter their tokens (X or O). Since our board is in an array, a player wouldn’t know that. A player starts counting at 1. When a user chooses the square from its point of view, we need to convert it into an integer so it can match the index in the array that the program is seeing. If a user enters 5 for the center position of the board, the array corresponding to it should be [4].

# To convert the user input into an integer we have to define a method to help us do that. We would use the .to_i method to change strings (which are what user inputs are) into integers, then minus 1 because the array counts up to 8, not 9.def input_to_index(user_input)
user_input.to_i - 1
end
  • Our program will then fill in the square (as the player’s view) on the board with the converted user input (now in an integer minus 1) with the player’s token.

Make a move & validate it

Now that the program is reading the user_input as an integer, we can tell the program to make a move by placing the player’s token in that square. Start by defining how to move a piece token on the board.

def move(board, index, player)
board[index] = player
end

When playing this game, a user can’t place their token on a square that’s already taken. In other words, we have to validate the code to ensure that the square being taken is really empty. Let’s define the method that determines how to validate a taken position.

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

NOTE: Methods return either true or false by adding a literal question mark to the end of the method name

Now we validate the user’s input (which is an integer representing the board’s array). If the user’s input converted index number is between 0–8 (the array index) and its position is NOT taken then the move is valid (true)

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

Helper Methods

If the validation is good for a user/player to place their token, then the game can continue where players take turn playing the game. Here we build a method composed of the use of many methods (helper methods) Helper methods are used inside other methods to carry out larger tasks as it’s reusable. Our current helper methods are:

display_board(board)
valid_move?(board, index)
position_taken?(board, index)
input_to_index(user_input)
move(board, index, first_player = "X")

Keeping track of turns

While playing the game, we need to keep track of which player’s turn it is and how many turns have been played. At every turn, we’ll check if there is a winner.

  • If there is a winner, they are notified by saying “X wins!” or “O wins!” If there is a tie, “It’s a Tie!”

We do this by defining turn_count(board) which we will use to define a current playercurrent_player(board).

def turn_count(board)
counter = 0
board.each do |spaces|
if spaces == "X" || spaces == "O"
counter += 1
end
end
counter
end
def current_player(board)
turn_count(board) % 2 == 0 ? "X" : "O"
end

Playtime

When playing the game, the players play until there’s (a condition). A condition where there’s a winner or a tie. In other words, we are looping this play until this condition is met. We start the game at 0 until we fill up the 9 squares or until someone wins or there’s a tie.

Play Method Adef play(board)
counter = 0
until counter == 9
turn(board)
counter += 1
end
end
Play Method Bdef play(board)
until over?(board)
turn(board)
end
if won?(board)
winner(board) == "X" || winner(board) == "O"
puts "Congratulations #{winner(board)}!"
elsif draw?(board)
puts "Cats Game!"
end
end
#until the game is over...
#players will keep taking turns
#plays the first few turns of the game
#if there's a winner...
#we check who the winner is...
#and congratulate them
#if there's a draw/tie, then print the message

Play Method A was the first iteration of the method. Play Method B is the final updated method. We’re no longer counting the game is over until all the 9 squares are filled up. We’re counting the game until it is over as each player takes their turn. After they play a few turns, either there’s a winner or a draw. If there’s a winner, we have to call on the winner(board) method and put out a message.

— The turn method

  • The program asks user for input
  • The user enters a number via keyboard; this number is a string upon entry which needs to be converted to an integer because computers/programs only know numbers. This number will be associated with the board’s array index minus 1.
  • If the move is valid via valid_move? helper method, then place the user’s token on the index; if not, the program will continuously ask the user for input until a valid move is made.
  • When the move is valid, the board will be displayed with the newly placed token
def turn(board)
puts "Please enter 1-9:"
user_input = gets.strip
index = input_to_index(user_input)
if valid_move?(board, index)
move(board, index, current_player(board))
turn(board)
end
display_board(board)
end

The turn method turn(board)threw off my test while I was working on the lab. The problem lied in the if statement block of move(board, index, current_player(board)). What I forgot about Ruby methods was that 1.) a method calls (or sends) values, 2.) a method is defined to accept values. We should try to think of methods as a dictionary. A dictionary defines a word and gives it a meaning. So, when we are defining a “word”, we are defining a “method” in Ruby. When calling a method or the “word”, it sends a value. Here’s how I figured out the if statement to clear my confusion.

turn method — if statement

The if statement of turn(board)has 3 helper methods. Their individual definitions will help us determine where to place an X or an O on the index/square on the board.

valid_move?(board, index)

The definition of valid_move?(board, index) is that if an index (the value being accepted) is between numbers 0 and 8 AND its position on the board (the other value being accepted) is NOT(!) taken, then the move must be true/valid.

move(board, index, current_player(board))

The definition of move(board, index, current_player(board)) is that a move will be made upon a valid move given the board, the user’s input index, and the current_player's token.

current_player(board)

Given this definition, and the above two helper methods’ definition, the if statement of turn(board)says to make a move on the board to the index selected and place the current_player’s token.

Winner? Draw?

  • There are 8 winning combinations:
WIN_COMBINATIONS = [ 
[0,1,2], # top_row
[3,4,5], # middle_row
[6,7,8], # bottom_row
[0,3,6], # left_column
[1,4,7], # center_column
[2,5,8], # right_column
[0,4,8], # left_diagonal
[6,4,2] # right_diagonal
]

WIN_COMBINATIONS is a constant but it’s the parent array to our the children array. The children array contains the individual 3 element array of indexes that tells us the matching wins [0,1,2], [3,4,5], etc. The parent array contains 8 elements, which are each of the children array.

parent_array => WIN_COMBINATIONS = [ children_arrays ]
children_arrays => [0,1,2],[3,4,5],[6,7,8]...[6,4,2]

To determine the winner, we have to grab each index of the parent array to determine the winner.

def won?(board)
WIN_COMBINATIONS.each do |win_combination|
win_index_1 = win_combination[0]
win_index_2 = win_combination[1]
win_index_3 = win_combination[2]
position_1 = board[win_index_1] # value of board at win_index_1
position_2 = board[win_index_2] # value of board at win_index_2
position_3 = board[win_index_3] # value of board at win_index_3
position_1 == position_2 && position_2 == position_3 && position_taken?(board, win_index_1)
end
end
#position_1 == position_2 && position_2 == position_3 && position_taken?(board, win_index_1) The above code means to return first element (position_1) & make sure the position is taken by X or O

While determining the winner, is the board full of X’s and O’s or is it empty? Just because the board is full doesn’t mean there will be a winner. Here’s the winning or tied scenario:

  • Board is full and there’s no winner => it’s a tied game
  • Board is not full and there’s no winner => it’s a tied game
  • There’s a winner, despite the board being empty or full

Given all that, let’s define what a full board is (full board helper method).

def full?(board)
board.all? {|i| i == "X" || i == "O"}
end

Now define what a tied game/board is.

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

If the game is tied or there’s a winner, let’s end the game.

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

Announcing the winner. When a winner is determined, we look for the winning combination’s element. If the element has a token of X, the winner is X, otherwise it’s O.

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

END of Tic Tac Toes.

**I’m tired of Tic Tac Toes… -_- **

I hope all this makes sense to someone. Or at least be helpful. I try to be pithy when giving instructions, descriptions, or other forms of info, but I often say more than I should. Maybe with coding and programming, the more info given the better? I have a better understanding of Ruby, tests, and CLI now, but hope to make it crystal clear soon. I just hope I can apply what I’ve done within this post to future labs.


Originally published at christineiscoding.com on August 23, 2017.

)

Christine Tran

Written by

Full-Stack Developer and UX Designer

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade