Madison CS 3-4: Tic-Tac-Toe

In this project, you’ll write a program that lets you play a game of tic-tac-toe against a computer opponent.

The goal of this project is to get you comfortable with writing functions.

Functions are like LEGO blocks.

lego.jpg

A LEGO block is a small, simple thing—but if you put a bunch of those simple blocks together in the right way, you can make the Milennium Falcon, or the Eiffel Tower, or a goofy-looking dog.

Functions are just like that. The functions you write should each be small, simple, and easy to understand—but if you put a bunch of your functions together in the right way, you can make a website, or control a robot, or fly a spaceship.

The best part about functions is that they’re even better than LEGO blocks, because you can make your own! When you buy a LEGO set, you’re stuck with whatever kinds of blocks come with the set; when you use a programming language, you can use its built-in functions to make your own way cooler functions that do whatever you want, and then you can put those functions together to make a program that does something awesome. Functions are basically my favorite thing about programming.

Let’s write some simple functions and put them together to make a tic-tac-toe game!

Demo

The game we’re making will look like this:

(The idea for this project was taken from Al Sweigart’s excellent book “Invent Your Own Computer Games With Python”.)

Representing The Board

A game of tic-tac-toe takes place on a 3x3 board. A space on the board is either empty, or it has an X in it, or it has an O in it.

At this point I’d like you to think back to the blackjack assignment. In that project, we talked about representing stuff. We wanted to teach our computer program about the concept of a playing card like “the five of diamonds”, and we ended up using a two-item list like [5, 'diamonds'] to do that. You’ll also remember that in order to represent a deck of cards, we made a list that had a bunch of items in it, and each of those items was a two-item list like ['4', 'hearts'].

In this project, we now have to figure out: how do we represent the concept of a tic-tac-toe game board? Think about it for a second before you scroll down.

How would you represent a 3x3 tic-tac-toe board in a Python program?

Seriously, think about it!

OK, here's what I recommend doing. Is it the same as what you were thinking?

I think we should use a list of lists of strings (also called a “two-dimensional list of strings”, or a “2D list of strings”). It’ll look like this:

[['X', ' ', 'O'],
 ['O', 'O', ' '],
 ['X', 'O', 'X']]

Let’s play around with this list to make sure that we completely understand it.


board = [['X', ' ', 'O'],
		 ['O', ' ', 'O'],
		 ['X', 'O', 'X']]

print('The board is a list with {0} elements.'.format(len(board)))
print('Each of those elements is a list.')

second_element = board[1]

print('The second element in the board is {0}'.format(repr(second_element)))
print("That's a list with three strings in it.")
print('The last string in that list is {0}.'.format(second_element[2]))
print("Here's the same string: {0}.".format(board[1][2]))

So, that’s our board—we’ll be using a 2D list of strings, and those strings will either be 'X', 'O', or ' '. The board will start off empty (all the strings will be ' ' initally ) and it will change over time as the player and computer make their moves.

A Note On Grading

Throughout this assignment, I’ll be telling you to write specific functions that behave a certain way. I’ll tell you what the functions should be named; I’ll tell you what inputs the functions should take; and I’ll tell you what outputs the function should return.

That’s because now that you’re writing functions, I’ll be able to test them directly. Just like you might, say, import Python’s built-in random module and call the random.choice() function in order to decide who goes first, I’ll be importing the tictactoe_your_name module from your program, and in my automated tests I’ll be writing code like tictactoe_your_name.a_function() in order to make sure each one of your functions works the way it’s supposed to.

What this means is that it’s really important that your functions have the exact names specified in the assignment. If I tell you to write a function named make_pizza(), but you write a function named make_hamburger() instead, my tests won’t be able to find your function and so you won’t pass the tests for this assignment.

That might sound restrictive and lame, but there’s some good news: now that my tests can use your functions directly, the stuff your program prints out can look however you want! You don’t have to use the exact same text from the demo videos any more, your program can be as weird and creative as you like. Enjoy!

Starter Code

I’ve written some starter code that you can use for this project.

The starter code defines five empty functions. Your job is to fill in those empty functions using the instructions below. When you’re done, you’ll have a working tic-tac-toe game!

Feel free to add more functions of your own if you want! Just be sure not to change the names of the functions provided in the starter code, because my automated tests will be looking for functions with those names.

OK, here’s what each of those functions should do!

Function: make_board()

Write a function called make_board.

It should take no inputs.

It should return as output an empty 3x3 tic-tac-toe board like the one described above.

When you call make_board(), it should return a list that looks just like this:

[[' ', ' ', ' '],
 [' ', ' ', ' '],
 [' ', ' ', ' ']]

That’s it for this one!

Function: print_board(board)

Write a function called print_board.

It should take as input a game board, represented by a 2D list of strings.

It should return as output None, which is a special value that we haven't really talked about yet.

In Python, a function will return None by default if the function doesn't have a return statement in it, so you don't really need to worry about this—just don't put a return statement in your function and you're all set.

This function should take a game board as input, and it should print that board out to the screen. Don’t just do print(board)—make it look nice!

You’ll want to use a nested for loop for this (one for loop inside another for loop). You’ll probably be doing range(len(SOMETHING)) once or twice, too.

Check out the demo video from earlier if you’d like an example of what your board might look like when it’s printed out. I didn’t make my board look particularly good, so try to make yours better-looking than mine!

Hint: When you’re working on this function (and all the other ones in this assignment!), try it out as you work on it. Add a line of code to your program that calls your function with a particular input (an empty board, a half-full board, a full board—you can write all of these boards by hand, they’re just lists of lists of strings), and see what your function returns when it’s given that input.

I used colors in my printed-out board, but you don’t have to. If you’re interested in using colors, my advice is to start simple and then add colors later. When you finish the no-colors version of this function, there’s a note at the end of this assignment that’ll tell you how to add colors if you want.

Next, let’s think about how we’ll handle the player’s move.

Function: get_player_move(board)

Write a function called get_player_move.

It should take as input a game board, represented by a 2D list of strings.

It should return as output a two-item list like [0, 2].

When you call get_player_move(board), the function should ask the player where they’d like to make their next move (see the demo video from earlier for an example).

If the player chooses a spot that’s off the board or isn’t empty, tell them to try again.

Note: this function takes a game board as input. This function should not modify that board (e.g. the function shouldn’t do something like board[1][2] = 'X'). I’ve written a test that checks for this.

(In general, functions shouldn’t modify their inputs. If a program has functions that modify their inputs, that program quickly becomes hard to understand and make changes to. When you’re using a function, you want to just figure out what data it takes as input and what data it returns—you don’t want to also have to ask questions like: “Will this function mangle the list I’m passing to it?”)

Note: When you prompt the player for their move, you should let them put in a number betwen 1 and 3 for their move’s X position, and another number between 1 and 3 for their move’s Y position. You should then convert those X/Y coordinates so that they’re between 0 and 2 instead of being between 1 and 3. Here’s why you need to convert those numbers:


board = [['X', ' ', 'O'],
		 ['O', ' ', 'O'],
		 ['X', 'O', 'X']]

print(board[0][0])

# This line crashes!
print(board[1][3])

Our board is a 3x3 grid. If you try to do a_three_by_three_grid[1][3], Python crashes, because you’ve asked for the fourth item in the second list, and the second list only has three items. Does that make sense? If not, reread the past few paragraphs one more time, you’ll get it.

2D lists take a little getting used to, but really they’re just like regular lists.

Anyway, what I’m getting at here is that if your get_player_move(board) function asks a user for their move’s X/Y coordinates and the user enters 1 and 3, then your function should return [0, 2]. If it returns [1, 3] in that situation, then that’s a bug, and I’ll find it! :)

OK, now let’s implement our AI opponent!

Function: get_computer_move(board)

Write a function called get_computer_move.

It should take as input a game board, represented by a 2D list of strings.

It should return as output a two-item list like [0, 2].

This function should look at the board and choose an empty space where the computer should make its next move.

This function should only choose an empty space. If it chooses a space that already has an 'X' or an 'O' in it, then that’s a bug.

This function is your game’s AI opponent! Your goal here is to write some code that looks at the passed-in board, thinks really hard, and then picks the best possible space where the computer should make its next move. Make this as crazy as you want—the goal is for this function to crush the human player (or at least force a tie)!

You don’t have to make a crazy-smart AI right now, though: if you’d like, it might be easier to just make a super-simple AI at first (“pick the first empty space you find on the board!”), finish the basic version of this assignment, and then come back to this function and make a crazy cool AI once you’re confident that the rest of your program works.

Hint: you don’t have to do this, but a good trick is to check to see if the player’s about to win on their next move. If that’s the case, the computer should make a move on the spot the player needs so that the player isn’t able to use it!

Note: Just like get_player_move(board), this function should not modify its input board. This function’s job is to take a board, look at it and figure out the space where the computer should move, and return that space. It should not change the board in the process.

We’re almost done with our program now—just one more function to go!

Function: check_for_winner(board)

Write a function called check_for_winner.

It should take as input a game board, represented by a 2D list of strings.

It should return as output one of the following values:'X', 'O', 'tie', or False.

This function’s job is to take a board as input and return as output a value that indicates whether or not the game’s over. If the function returns 'X', that means X wins; if the function returns 'O', that means O wins; if the function returns 'tie', that means the game’s over and there’s a tie; and if the function returns False, that means that the game isn’t over yet.

There are eight possible ways to win at tic-tac-toe: there are three possible horizontal lines, three possible vertical lines, and two possible diagonal lines. Be sure to check for all of them!

(A note for advanced students: this function returns one of three special strings or False. That’s just kind of clunky. It’d be much better if our program defined an Enum with a name like WinStatus; then we could have this function return something like WinStatus.NO_WINNER_YET, WinStatus.X, WinStatus.O, or WinStatus.TIE. We’re not covering Enums in this class, though, so for now let’s just live with the fact that this function has a weird return value.)

One Last Note: Colors

The tic-tac-toe program from my demo video had colorful Xs and Os. If you’d like to do this in your program too, then check out the crayons library; you can install it by running pip install --user crayons at the command line.

The colors won’t work in IDLE, so you’ll need to run your program from the command line if you use the crayons library. Let me know if you’d like help figuring out how to do that.

UPDATE 3/30/2018: it looks like the crayons library might not work with windows. Sorry! :(

Submitting your project

Submit a file called tictactoe_<YOUR_NAME>.py.

For instance, I’d submit a file called tictactoe_jr_heard.py.

On the first line of that file, write a comment with your name on it, like this:

# JR Heard

Remember to follow this class’s style guide.