- Build a method composed of the use of many methods ("helper methods") previously defined.
- Use method return values to control the logic of a composed method.
- Use an input validation loop or use recursion to create a loop.
- Build a CLI that uses a single method call to execute.
You're going to be building a CLI for a single turn of Tic Tac Toe.
A turn of Tic Tac Toe is composed of the following routine:
- Asking the user for their move by position 1-9.
- Receiving the user input.
- If the move is valid, make the move.
- If the move is invalid, ask for a new move until a valid move is received.
- Display the board after the valid move has been made.
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 (some of which you may have built previously).
You can imagine the pseudocode for the #turn
method:
ask for input
get input
if input is valid
make the move for input
else
ask for input again until you get a valid input
end
show the board
Before defining #turn
, you should define the following methods:
Should accept a board as an argument and print out the current state of the board for the user.
Should accept a board and a position from the user (remember that the user will input a number 1-9 but your board is really indexed 0-8) and return true if the position is within the correct range of 1-9 and is currently unoccupied by an X or O token.
Hint: While not explicitly required by this lab, you might want to encapsulate the logic to check if a position is occupied in its own method, perhaps #position_taken?
This method should accept a board, a position from the user (which will be in the 1-9 format), and a token to mark that position with (you can give that argument a default value of 'X'––we're not worrying about whose turn it is yet). The method should set the correct index value of that position within the board equal to the token.
Start with building those methods (or copying code you might have written before) and making the first few tests in lib/turn_spec.rb
pass. You can use the learn --fail-fast
or rspec --fail-fast
mode to only see 1 failure at a time and allow you to work through those method definitions.
You'll then need to build your #turn
method. Before building a full turn method according to the failing tests, let's setup a quick CLI so that you can watch your turn method perform as you build, visually confirming it behaves as expected.
Open bin/turn
, you'll see that it is already setup with #!/usr/bin/env ruby
so you can execute it by running ./bin/turn
or ruby bin/turn
from your terminal. It currently does nothing (because it has no code), but try it out just for fun.
The purpose of this file is to execute a turn of tic tac toe. The first thing it needs to do is load our library of methods defined in lib/turn.rb
.
Edit bin/turn
:
#!/usr/bin/env ruby
require_relative '../lib/turn'
By adding require_relative '../lib/turn'
we are telling ruby to load a file from a relative path to the current file. Since we're in bin
we have to go up a directory and into lib to find turn.rb
, thus the path ../lib/turn
. You never need to give the .rb extension to a path for require_relative, ruby assumes you mean a .rb file.
Next, the CLI needs to setup the data required to play a game of Tic Tac Toe, namely, the board
variable to store the array we use to keep track of the state of the board.
Edit bin/turn
:
#!/usr/bin/env ruby
require_relative '../lib/turn'
board = [" ", " ", " ", " ", " ", " ", " ", " ", " "]
We're now ready to start the game. Let's welcome the user and show them the board at the start of the game.
Edit bin/turn
:
#!/usr/bin/env ruby
require_relative '../lib/turn'
board = [" ", " ", " ", " ", " ", " ", " ", " ", " "]
puts "Welcome to Tic Tac Toe!"
display_board(board)
Notice how we evoke the #display_board
method defined in lib/turn, passing the local board data into it via an argument on the last line.
Now let's run this CLI: bin/turn
or ruby bin/turn.rb
from your terminal. You should see:
$ ./bin/turn
Welcome to Tic Tac Toe!
| |
-----------
| |
-----------
| |
Great! Now the next thing the CLI needs to do is kick off a turn of the game. We know we're going to build a #turn
method to encapsulate that procedure, so even though we haven't defined it yet, let's add the call to the soon-to-be-coded #turn
method to our CLI right now.
Edit bin/turn
:
#!/usr/bin/env ruby
require_relative '../lib/turn'
board = [" ", " ", " ", " ", " ", " ", " ", " ", " "]
puts "Welcome to Tic Tac Toe!"
display_board(board)
turn(board)
If we ran the CLI right now, without defining #turn
in lib/turn.rb
, we'd get a NameError
complaining about an undefined local variable or method turn
.
Let's quickly jump to lib/turn.rb
and just stub out the most simple version of the turn method.
Add to lib/turn.rb
def turn(board)
puts "Please enter 1-9:"
end
Let's run the CLI now: bin/turn
or ruby bin/turn.rb
from your terminal. You should see:
$ ./bin/turn
Welcome to Tic Tac Toe!
| |
-----------
| |
-----------
| |
Please enter 1-9:
Great! Now as we add logic to #turn
, we can use our CLI to see how it behaves.
The hard part of the turn method is figuring out how to handle invalid input. We know that when a user enters invalid input, we want to ask them for input again. Imagine the pseudo-code again:
get input
if input is valid
make the move for input
else
ask for input again until you get a valid input
end
Asking for input again is the hard part. We either need a mechanism to repeat the entire logic again until input satisfies the valid requirement, like a loop of some sort, or we need to be able to execute a procedure that asks for a user's input again. It's almost like what we might want to do in the event of invalid user input is just replay the entire turn, no move was made, so why not just run #turn
again?
Calling a method from within itself is totally okay in programming, in fact, it is an elegant solution to some complex problems. Recursion is the repeated application of the same procedure. Google it there's an easter egg from Google developers on that page, can you find it?
If you are familiar with loops, that is a totally acceptable solution to the input validation problem as well.
As you try to get it working, keep playing with bin/turn
until it works as expected, endlessly asking you for a valid turn input. If you ever need to exit the CLI without giving an input, just hit CTRL+C
(sometimes ALT+C
or COMMAND+C
).
The tests will pass once you have your CLI working right, but don't be scared of running the tests to see the failures for #turn
and seeing if they point you in the right direction.
Once you define #turn
as specific by the tests in spec/turn_spec.rb
, your CLI should work for a turn of Tic Tac Toe and running it should yield:
$ ./bin/turn
Welcome to Tic Tac Toe!
| |
-----------
| |
-----------
| |
Please enter 1-9:
1 # I entered 1 in response to the gets prompt.
X | |
-----------
| |
-----------
| |
A subsequent run might yield:
$ ./bin/turn
Welcome to Tic Tac Toe!
| |
-----------
| |
-----------
| |
Please enter 1-9:
5
| |
-----------
| X |
-----------
| |
Currently our program only allows us to run 1 turn, the first turn. If you wanted to see how #turn
would behave say on the third turn of the game, make the following edits to bin/turn
:
#!/usr/bin/env ruby
require_relative '../lib/turn'
board = [" ", " ", " ", " ", " ", " ", " ", " ", " "]
puts "Welcome to Tic Tac Toe!"
move(board, "5", "X")
move(board, "1", "O")
display_board(board)
turn(board)
Notice that before the game even really starts, we hard code an execution of two moves, X to the middle position ("5"), and O to the top left ("1").
When we run the CLI, we'd see:
$ ./bin/turn
Welcome to Tic Tac Toe!
O | |
-----------
| X |
-----------
| |
Please enter 1-9:
The board is pre-filled and the turn will now add a 3rd token to the board.
Try this edit to bin/turn
:
#!/usr/bin/env ruby
require_relative '../lib/turn'
board = [" ", " ", " ", " ", " ", " ", " ", " ", " "]
puts "Welcome to Tic Tac Toe!"
move(board, "5", "X")
move(board, "1", "O")
move(board, "2", "X")
display_board(board)
turn(board)
Here we are manually making 3 moves, an X, an O, and then an X, as would proceed normally in a real game. That means that the next turn belongs to the O player, right? Run your CLI and enter a move for O and see what happens. Can you guess?
$ ./bin/turn
Welcome to Tic Tac Toe!
O | X |
-----------
| X |
-----------
| |
Please enter 1-9:
8
O | X |
-----------
| X |
-----------
| X |
It was O's move and when I entered 8 to block X with my O, our program put an X in!!! Why? We'll have to fix that. Can you anticipate what we might need to build to make that work?
The other issue with our current program is that only one turn is being played. We could actually work around that right now. How many turns does our CLI execute? Currently only 1 (even with the explicit calls to move, those aren't user turns, they are only updating the board). Is there anything preventing us from just calling #turn
9 times so it's like a real game with 9 turns? Nope! Let's try it with a final edit to bin/turn
:
#!/usr/bin/env ruby
require_relative '../lib/turn'
board = [" ", " ", " ", " ", " ", " ", " ", " ", " "]
puts "Welcome to Tic Tac Toe!"
display_board(board)
turn(board)
turn(board)
turn(board)
turn(board)
turn(board)
turn(board)
turn(board)
turn(board)
turn(board)
Here's an entire execution of this CLI (remember, when you see a number that means the player put in that input).
./bin/turn
Welcome to Tic Tac Toe!
| |
-----------
| |
-----------
| |
Please enter 1-9:
1
X | |
-----------
| |
-----------
| |
Please enter 1-9:
2
X | X |
-----------
| |
-----------
| |
Please enter 1-9:
3
X | X | X
-----------
| |
-----------
| |
Please enter 1-9:
4
X | X | X
-----------
X | |
-----------
| |
Please enter 1-9:
5
X | X | X
-----------
X | X |
-----------
| |
Please enter 1-9:
6
X | X | X
-----------
X | X | X
-----------
| |
Please enter 1-9:
7
X | X | X
-----------
X | X | X
-----------
X | |
Please enter 1-9:
8
X | X | X
-----------
X | X | X
-----------
X | X |
Please enter 1-9:
9
X | X | X
-----------
X | X | X
-----------
X | X | X
Another issue, besides only marking Xs as described above, is that the game played way too many turns! We need it to know how to quit if someone wins.
Even with these deficiencies, this #turn
method means you are very close to building a complete Tic Tac Toe. Get excited!