We'll build a two-player CLI version of Tic Tac Toe by defining a set of methods to encapsulate the game logic.
- Build and use helper methods within the Tic Tac Toe application
- Write a method that is responsible for the game loop in Tic Tac Toe
- Code the main executable file that will run the game in the CLI
If you've been following along with the progression of Tic Tac Toe labs you've
probably already built lots of the methods required to solve this lab. As you
solve this lab, when you encounter a requirement you have previously solved,
like defining the WIN_COMBINATIONS
constant, or the #display_board
method,
you should take a second and find your old code. The ability to recognize
previously solved problems and integrate a known solution is crucial to
programming (and problem solving in general). Us programmers, we call that "Copy
and Pasting."
├── Gemfile
├── Gemfile.lock
├── README.md
├── bin
│  └── tictactoe
├── lib
│  └── tic_tac_toe.rb
└── spec
├── 01_tic_tac_toe_spec.rb
├── 02_play_spec.rb
├── 03_cli_spec.rb
└── spec_helper.rb
This file sets up gems for our project and can mostly be ignored. Make sure to
run bundle
before starting this project so that you have all the required
gems.
This is our main executable and will be how we run our game.
All of our game methods will be coded here.
There are three test files that should be completed in order.
01_tic_tac_toe_spec.rb
sets tests for our helper methods. 02_play_spec.rb
tests the main #play
method. 03_cli_spec.rb
tests the CLI.
There are infinite ways to build Tic Tac Toe. We, however, have been building up to a very specific structure with the methods we've been building throughout this course. We'll be using many of those methods here. These methods represent the basic requirements and logic of the game.
Why build and utilize the helper methods we've been constructing?
Let's take a look at what it would require to print out a board three times, without any helper methods:
board = [" ", " ", " ", " ", " ", " ", " ", " ", " "]
puts " #{board[0]} | #{board[1]} | #{board[2]} "
puts "-----------"
puts " #{board[3]} | #{board[4]} | #{board[5]} "
puts "-----------"
puts " #{board[6]} | #{board[7]} | #{board[8]} "
puts " #{board[0]} | #{board[1]} | #{board[2]} "
puts "-----------"
puts " #{board[3]} | #{board[4]} | #{board[5]} "
puts "-----------"
puts " #{board[6]} | #{board[7]} | #{board[8]} "
puts " #{board[0]} | #{board[1]} | #{board[2]} "
puts "-----------"
puts " #{board[3]} | #{board[4]} | #{board[5]} "
puts "-----------"
puts " #{board[6]} | #{board[7]} | #{board[8]} "
Now, let's take a look at the same task, encapsulated into a method that we can simply call, or invoke, three times.
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
board = [" ", " ", " ", " ", " ", " ", " ", " ", " "]
display_board(board)
display_board(board)
display_board(board)
When our logic is well encapsulated by individual methods everything about our code is easier. We can re-use the same logic again and again, painlessly.
You'll approach building Tic Tac Toe in this manner, first defining helper methods according to the test requirements and then slowly putting them together to define the complete play logic.
You'll then use this play method within a CLI and build a fully functioning 2 player Tic Tac Toe CLI in Ruby!
We'll be representing a Tic Tac Toe board using an array of " "
strings. We'll
pass this board to every method as an argument so the helper methods can
interact with or introspect on the board.
We'll be getting user input via gets
and a player will choose a position on
the board by entering 1-9. Our program will then fill out the appropriate
position on the board with the player's token. The term 'position' will refer to
the spot on the board as the player sees it (1-9).
We will keep track of which player's turn it is and how many turns have been played. We will check to see, at every turn, if there is a winner. If there is a winner, we'll congratulate them. If there is a tie, we will inform our players.
Open up lib/tic_tac_toe.rb
. You'll be coding your helper methods here. Run the
tests for these methods by typing rspec spec/01_tic_tac_toe_spec.rb
in the
terminal. Use the test output, along with the guidelines below, to build these
methods.
Define a constant in lib/tic_tac_toe.rb
WIN_COMBINATIONS
and set it equal to
a nested array filled with the index values for the various win combinations in
tic tac toe.
WIN_COMBINATIONS = [
[0,1,2], # Top row
[3,4,5] # Middle row
# ETC, an array for each win combination
]
Define a method that prints the current board representation based on the
board
argument passed to the method.
An empty board would be:
board = [" "," "," "," "," "," "," "," "," "]
display_board(board)
Outputting:
| |
-----------
| |
-----------
| |
A board with an "X" in the middle:
board = [" ", " ", " ", " ", "X", " ", " ", " ", " "]
display_board(board)
Outputting:
| |
-----------
| X |
-----------
| |
Once the user inputs where they would like to go on the board, we then have to convert this to the board index multiple times. Instead of doing that in a lot of places, we can remove this repetitive code and put it in this helper method. This method takes the user_input (which is a string), converts it to an Integer, and subtracts 1. Remember that, from the player's point of view, the board contains spaces 1-9. But, an array's indexes start their count at 0.
Your #move
method must take in three arguments: 1) the board array,
2) the index in the board array that the player would like to fill out with
an "X" or and "O", and 3) the player's character (either "X" or "O"). We
previously had you write this method with a default argument of "X" for the
third argument, but that is no longer needed.
The #position_taken?
method will be responsible for evaluating the position
selected by the user against the Tic Tac Toe board and checking to see whether
or not that index on the board array is occupied. If the user would like to fill
out position 1, our #position_taken?
method will check to see if that board
index is vacant or if it contains an "X" or an "O". If the position is free, the
method should return false
(i.e. "not taken"), otherwise it will return
true
.
Build a method valid_move?
that accepts a board and an index to check and
returns true
if the move is valid and false
or nil
if not. A valid move
means that the submitted position is:
- Present on the game board.
- Not already filled with a token.
This method takes in an argument of the board array and returns the number of turns that have been played.
The #current_player
method should take in an argument of the game board and
use the #turn_count
method to determine if it is "X"
's turn or "O"
's.
Build a method #turn
to encapsulate the logic of a single complete turn
composed of the following routine:
- Asking the user for their move by position 1-9.
- Receiving the user input.
- Convert user input to an index
- If the move is valid, make the move and display board.
- Otherwise (that is, if the move is invalid) ask for a new position until a valid move is received.
All these procedures will be wrapped into our #turn
method. However, the
majority of the logic for these procedures will be defined and encapsulated in
individual methods which you've already built.
You can imagine the pseudocode for the #turn
method:
ask for input
get input
convert input to index
if move is valid
make the move for index and show board
else
ask for input again until you get a valid move
end
Your #won?
method should accept a board as an argument and return false if
there is no win combination present in the board and return the winning
combination indexes as an array if there is a win. Use your WIN_COMBINATIONS
constant in this method.
The #full?
method should accept a board and return true if every element in
the board contains either an "X" or an "O".
Build a method #draw?
that accepts a board and returns true if the board has
not been won and is full and false if the board is not won and the board is not
full, and false if the board is won.
Build a method #over?
that accepts a board and returns true if the board has
been won, is a draw, or is full.
The #winner
method should accept a board and return the token, "X" or "O" that
has won the game given a winning board.
The play method is the main method of the tic tac toe application and is responsible for the game loop. A tic tac toe game must allow players to take turns, checking if the game is over after every turn, and at the conclusion of the game, whether because it was won or because it was a draw, reporting to the user the outcome of the game. You can imagine the pseudocode:
until the game is over
take turns
end
if the game was won
congratulate the winner
else if the game was a draw
tell the players it has been a draw
end
Run the tests for the #play
method by typing rspec spec/02_play_spec.rb
in
your terminal.
It'll help you a bit to understand how the tests are written for #play
and
what the failure output means. Open up spec/02_play_spec.rb
. You'll see the
first test block:
it 'asks for players input on a turn of the game' do
board = [" ", " ", " ", " ", " ", " ", " ", " ", " "]
allow($stdout).to receive(:puts)
allow(self).to receive(:over?).and_return(false, true)
expect(self).to receive(:gets).at_least(:once).and_return("1")
play(board)
end
This test is built to confirm that your implementation of the #play
method
will at some point ask the user for input via the gets
method. In order to
ensure this requirement of the play method, our test needs to set up some
conditions and expectations.
1 . First, we have a sample game board, board
, as it would look at the start
of the game, entirely empty.
board = [" ", " ", " ", " ", " ", " ", " ", " ", " "]
2 . We state that any output to $stdout
(which is how Ruby sends output to
your terminal) should basically be ignored. Otherwise when we run our tests,
we'll see all of our program's output and it'll be super confusing.
allow($stdout).to receive(:puts)
3 . Our play method is going to involve looping. The test suite is going to
ensure that when you build your game loop, you use the #over?
method as a
condition of when to end the loop. When we call #play
in our test this loop
will never end because there is no player input and no one ever wins the game in
most of our tests. This would create an infinite loop in our test. So to avoid
this, we fake the behavior of #over?
, making sure it returns false the first
time it is called and true the second.
allow(self).to receive(:over?).and_return(false, true)
In this test it is as though the #over?
method was defined to automatically
return false
the first time it is called and true
all other times. Because
the #play
loop depends on #over?
, when #over?
returns true, the loop
should end.
In our tests, we can stub the functionality of any method to behave exactly as we would like for this test to function and create the conditions that allow us to specify functionality. This technique of faking or hardcoding the return value and behavior of a method is called Mocking or Stubbing. It's a bit of an advanced technique in testing and for the most part, you don't need to worry about it.
4 . The main expectation of our test is that when #play
is called, at some
point the user will be prompted for input via the gets
method. We set exactly
this expectation:
expect(self).to receive(:gets).at_least(:once).and_return("1")
The final bit of that, and_return("1")
, states that when gets
is called and
the expectation is met, the call to gets
should also be stubbed and return
"1".
5 . Finally, after setting up the expectations of the behavior of #play
, the
test suite evokes the method. All expectations set must be met by the
termination of that method call.
play(board)
With a non-functioning #play
method, let's look at the output of the failure.
def play(board)
end
If we ran the test suite against that code in lib/tic_tac_toe.rb
, we'd see.
1) ./lib/tic_tac_toe.rb #play asks for players input on a turn of the game
Failure/Error: expect(self).to receive(:gets).at_least(:once).and_return("1")
(#<RSpec::ExampleGroups::LibTicTacToeRb::Play:0x007fdafb0dea20>).gets(*(any args))
expected: at least 1 time with any arguments
received: 0 times with any arguments
# ./spec/02_play_spec.rb:10:in `block (3 levels) in <top (required)>'
The key output is the failed expectation.
Failure/Error: expect(self).to receive(:gets).at_least(:once).and_return("1")
expected: at least 1 time with any arguments
received: 0 times with any arguments
The test is saying that during the call to #play
, we expected the program to
call gets
, but it didn't. Let's fix this simply by updating the definition of
#play
.
def play(board)
input = gets
end
./lib/tic_tac_toe.rb
#play
asks for players input on a turn of the game
Finished in 0.0058 seconds (files took 0.1434 seconds to load)
1 example, 0 failures
And it works! We don't want to just use that, the #play
method is way more
complicated and probably shouldn't be calling gets
itself but rather having
some other method, like #turn
call gets. As long as something #play
does
meets the expectations set, the test will pass.
The test may just appear to freeze in the middle or you might run into this error:
SystemStackError:
stack level too deep
# ./lib/tic_tac_toe.rb:60:in `puts'
# ./lib/tic_tac_toe.rb:60:in `turn'
# ./lib/tic_tac_toe.rb:67:in `turn'
# ./lib/tic_tac_toe.rb:67:in `turn'
# ./lib/tic_tac_toe.rb:67:in `turn'
# ./lib/tic_tac_toe.rb:67:in `turn'
...
..
.
This means that we're calling a method that gets stuck in an infinite loop. A
common location where this occurs is testing the play
method without using the
over?
method to determine if the game is over. This is because our test stubs
the over?
method to manually return true even when the game isn't over. So if
you are not basing your decision in your code to stop taking turns when the game
is over?
then we end up in an infinite loop!
Your bin/tictactoe
CLI should:
- Greet the players with "Welcome to Tic Tac Toe!"
- Define a
board
array. - Display the starting board.
- Begin the game by calling
#play
.
When building a large application like this one, it's useful to be able to
explore your code through an interactive console that has all your code loaded.
For example, imagine wanting to test your #valid_move?
method. You might have
built the method in lib/tic_tac_toe.rb
, but besides finishing the entire lab
and trying it in the context of your CLI or running the test suite and seeing
output, it's very hard to know how the method you built behaves. So you might
start irb
in your terminal and copy the source of your #valid_move
method
into a new IRB session so you can play with it. But as the methods become more
complex and dependent, that breaks down. So we've given you a console that will
load your code for you into a sort of interactive playground.
First, run bundle
to make sure all your gems are installed. Then, you can
start the console with rake console
in your terminal.
Imagine your lib/tic_tac_toe.rb
containing:
def move(board, index, player = "X")
board[index] = player
end
If you run rake console
, you'll see.
$ rake console
Loading your application environment...
Console started:
[1] pry(main)>
At your prompt, you can execute ruby just like you could in an IRB session. The only difference is that your console has already read all the code in your application, so you have access to it in this Pry session.
$ rake console
Loading your application environment...
Console started:
[1] pry(main)> board = [" ", " ", " ", " ", " ", " ", " ", " ", " "]
=> [" ", " ", " ", " ", " ", " ", " ", " ", " "]
[2] pry(main)> valid_move?(board, 0)
=> true
[3] pry(main)> board = ["X", " ", " ", " ", " ", " ", " ", " ", " "]
=> ["X", " ", " ", " ", " ", " ", " ", " ", " "]
[4] pry(main)> valid_move?(board, 0)
=> false
As you can see, it's easy to test the method with data we created.
Once you get all the tests to pass, play your game!
$ bin/tictactoe
Feel proud. You can't imagine how far we've come already.
Included with this lesson is a tool that will generate a Coverage Report that
shows how much of our code is tested when we run our tests. After a test run,
you can open the file generated in coverage/index.html
from terminal with
open coverage/index.html
(If you're using the Learn IDE, don't run the
open
command here. Instead run httpserver
and then open the IP address noted
in browser and select the link to coverage/
). It should pop up in your
browser and look like:
When you click on lib/tic_tac_toe.rb
, you'll see something like:
The lines in green have been tested, the lines in red haven't. When all tests
pass, you should see 100% test coverage. You can exit httpserver
using
control + c
.