Welcome
Welcome to people of all ages, all colors, all cultures, abilities, sexual orientations and gender identities.
Welcome to those who identify as biracial, multi-racial, or multi-ethnic.
Welcome especially to those of you who added the course recently.
Announcements
Per the announcement on Sunday, February 7th, campus is in the “High Risk” operational posture for at least two weeks. So we’re back to online-only instruction. 😔
This week, I will be posting asynchronous lecture recordings to Echo360 – there is no synchronous class this week! So I strongly encourage you to get help from someone synchronously if you need it.
In particular you could come to office hours – all six members of the staff all have office hours, and they’re posted on Campuswire. Or, you could use some of the many other help options available to you – there are both supplemental instruction and tutoring available from the Learning Resource Center, and CICS also offers tutorings. Links for all of that are on Campuswire too. Take advantage of them.
Finally, are you a new add to the course? You need to get in touch with me immediately! Send me an email and I’ll get you into Gradescope and CampusWire.
Developing with Visual Studio Code
So, you’ve used Visual Studio Code (or just “Code” for short) in a very hand-holding way to complete HamSpam, but it hasn’t really required you to understand the use of unit tests, or how you can use the debugger to help find errors in your code.
This week, we’re going to do a little practice run through Code, demonstrating some of its features that you’ll find useful for the rest of the semester.
In particular, we’ll show you how you might write and use your own “unit tests” to check your progress toward completing an assignment. We provide tests for the projects early on, but you’ll need to start writing your own at some point this semester, and sooner is probably better than later.
We’ll also show you how you might use the debugger to figure out what’s going on when things aren’t working the way you’d expect. Sometimes “caveman debugging” (liberal use of “println”) is good enough, but sometimes it’s helpful to examine a program’s state while it’s running to figure out what’s going on.
We’ll use everyone’s favorite game, tic-tac-toe (also known as “naughts and crosses”) as our running example.
(demo the game on the board)
Creating a project
For this class we typically give you a project template to work with. One way to create such a template is to just use Code to make a new project. From the command palette, choose “Java: Create Java Project”. In a moment, you’ll be prompted to “Select Project Type”; choose “No Build Tools”. Then you’ll be prompted to pick a location; this is the folder where the project folder will go. Finally, choose the project name; the project will be stored in a folder of this name inside the location you chose in the previous step.
By default Code creates a README.md
, a src/
directory (and a very short “Hello World” sort of thing called App.java
) to store source files, and a lib/
directory to store libraries.“Libraries” are bundles (usually in the form of “.jar” files) of Java code written by others that this project will leverage. For example, this week’s programming assignment uses the “Processing” jar to draw the bus simulator graphics and handle mouse and keyboard input. We won’t use any libraries for graphics in our example, instead using simple text-based input and output. But we will use a pair of jar files to enable unit testing, as I’ll show shortly.
OK, so now we have an new programming project project. A new project! What should you do?
a. immediately start coding whatever comes to mind, like a hyperactive gerbil that just had a quadruple espresso on an empty stomach!
b. maybe stop and think for a bit about what the program should do, and how it should do it.
Blank slate
Now, really, what should we do? Think about about the data our program is going to represent, and the actions we’re going to perform upon that data.
What kinds of data is this program going to need to keep track of? (If we were meeting in person, I’d ask you in lecture). Think in terms of nouns here.
- a board / spaces
- which spaces have been claimed by which player
- winner/loser/draw (or game still in progress?)
And what kinds of actions are we going to be performing? This time, verbs.
- creating a new board
- claiming spaces
- checking winner
- checking end-of-game
- printing the board (actually, converting it to a string)
If we squint at this for a bit, we get a sense of what types of data we’re going to have and the methods we might use to. We need something to represent the state of a space, and an array of these somethings to represent the board. And…that’s about it, actually – all of the other things can be computed by looking at the board – we don’t need to keep track of them, though we might choose to do so to keep the code simple or clear.
Keeping track of things we don’t need to can be detrimental: we might make a mistake (and desynchronize between our board and, say, an xHasWon
instance variable) which would lead to hard-to-track down bugs.
Whether to keep track of values, or to compute them from other values we are tracking, is a judgment call. If computing one of these properties was actually hard – that is, used a lot of time or space, or the code to do so was complicated and error prone, we might just track the thing in a variable. But if the code to update that variable was complicated and needed to be considered in many methods, we might compute the value, instead.
You’ll develop a sense for this as you gain in programming experience (and as you learn how to accurately determine which things are actually costly in terms of time and space).
Getting started
Let’s start by creating an empty board class, and put our state (instance variables) and behavior (methods) there. Notice I put the classes into a package; this is generally expected in “real” Java programs. Also notice I have to make a directory structure that corresponds to the package structure (that is, classes in a tictactoe
package have to be in a tictactoe
directory inside my source directory).
package tictactoe;
public class TicTacToeBoard {
}
Spaces and the board
How should we represent spaces? Two-state variables are naturally represented as booleans, but spaces can be in one of three states: empty, ‘X’ or ‘O’. How can we deal with this?
One option is to use Strings (“empty” “x” “o”); another might be to use integers (0 = empty, 1 = x, 2 = 0), where we restrict our attention to the values of interest. These aren’t bad choices per se, though they do come with some risks.
private String[] board;
// or we might use
// private int[] board;
Suppose we define our board as an array of strings, where each entry is X
, O
or (empty).
board[0] = "X";
// ... later ...
board[1] = "0";
Is this code correct? And, will the compiler catch the error?
For strings, if we make a typo in the string in our code somewhere (“X” vs “x”) then comparisons will fail and some conditionals might not execute.
For ints, they aren’t very readable, and again, we might slip with our fingers and accidentally assign the value 3 to a board space.
One way to mitigate these issues somewhat by using “named constants”. We have to use two keywords to signal some things to the compiler:
final
: This variable’s value is not variable at all, but instead constant. It’s a compile-time error to attempt to change its value.static
: There is only one copy of this variable, shared between all instances of the class. We don’t need a separate copy per-instance. (Strictly speaking, this is optional, but since the values of the spaces won’t vary between games, we might as well make that clear.)public static final String X = "X"; public static final String O = "O"; public static final String EMPTY = "empty";
“Marc,” you may be thinking, “this seems like a lot of extra work.” Well, “a lot” is relative. But the goal here is to let the computer (in this case, the compiler) help you. If you only use these constant names in your code; it prevents you from making typos and is a good idea if you are working with constants. It also tells other people what the otherwise “magic numbers” or “magic strings” in your code mean. The vast majority of programming jobs in the real world don’t let you write everything from scratch – you’re often reading other people’s code, too, so it pays to make your code legible.
We might also consider using an “enumeration type.” Enumeration types are kinda like very short classes, which can only hold a small set of values, that have specific support in the Java language. Enums are reference types, and like all reference types, their default value is null
– watch out for that!
An enum provides additional benefits over constants. Namely, it defines its own type so you can’t accidentally compare it with another String. (Maybe someday take a deeper dive into a programming language that supports algebraic data types and pattern matching to see how you can really leverage the compiler to help you avoid mistakes.)
private enum Space {
X, O, EMPTY;
}
Back to the job at hand! Now that we’ve defined the types of values that can be stored in spaces, we need a place to put them. We’ll create a Space array:
private Space[] board;
Now, let’s see if we can see the board. Switch over to our driver and add this to the main()
method:
public static void main(String[] args) {
TicTacToeBoard board = new TicTacToeBoard();
System.out.println(board);
}
And…you’ll see something like tictactoe.TicTacToeBoard@677327b6
! (It might look different each time you run the program.) What’s this?
We’re attempting to print board
, which is an instance of TicTacToeBoard
. What does println
do when we pass it an object? It attempts to coerce it to a string, using the object’s builtin toString
method. But we didn’t define one. Where is this string coming from?
Java classes use inheritance, and if a method isn’t defined in the current class, the Java language says we should look up the inheritance hierarchy to find its definition. What’s the superclass of TicTacToeBoard? We didn’t define one (using extends
), but there is a default for all objects: Object
. What does it have to say about toString
?
https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Object.html#toString()
So if we don’t define a toString, we get the typename and the hash code. (And lookie there, the default value of the hashcode is the object’s address! So they are, kinda, sorta, sometimes, available.)
Anyway, one of the important rules for development and debugging is that you can’t fix what you can’t see. So let’s write our own toString method now. It should convert the current instance of the board into a string:
public String toString() {
String string = "";
for (Space s : board) {
string = string + s;
}
return string;
}
What’s the output?
And… Boom! Null pointer exception! We didn’t initialize the board! Let’s make a constructor and do that.
public TicTacToeBoard() {
board = new Space[9];
}
And… not quite Boom! But still lotsa null. Eff null, seriously, what were language designers thinking? The guy who invented null has since claimed it was the billion dollar mistake, and that’s before adjusting for inflation.
How do we fix it? By fully initializing the board:
public TicTacToeBoard() {
board = new Space[9];
for (int i = 0; i < board.length; i++) {
board[i] = Space.EMPTY;
}
}
Unfortunately we still get something ugly: EMPTYEMPTYEMPTYEMPTYEMPTYEMPTYEMPTYEMPTYEMPTY
.
Let’s fix this in two parts. First, let’s teach the enumeration toString()
itself:
public String toString() {
if (this == X) {
return "X";
} else if (this == O) {
return "O";
} else if (this == EMPTY) {
return ".";
}
return "WTFBBQ";
}
Annoyingly we can’t prove to Java that we’ve covered all the cases here (again, languages with better type systems can handle this) so we need a default, obviously wrong value in case of programmer error. Choose your own if you like.
Then, while we don’t need to, it might be nice to structure the board’s toString
output to be a little more human-readable. We want spaces between each item, and a newline at the end. And we want to trim the last newline. OK, here we go:
public String toString() {
String string = "";
for (int i = 0; i < board.length; i++) {
string = string + board[i];
if (i % 3 == 2) {
string = string + "\n";
}
else {
string = string + " ";
}
}
return string.substring(0, string.length() - 1);
}
Well, not beautiful but it gets the job done. It might be marginally more efficient to use a StringBuilder
, but enough premature optimization for now.
Tests
So we’ve been manually running our driver after each change to see if the output matches what we want. You know what else is really good at running things repeatedly and checking for matches against expected results? A computer.
It turns out this is the basis of automated testing, and there’s a system we can use in Java called JUnit to do this for us. We can still use our driver, of course, but we can also run automated tests to make sure that output is what we expect it to be.
In most class projects testing is already set up for you, and you can just edit the classes yourself if you like. Here I’ll cheat by copy/pasting in the scaffolding we need for tests. I’ll also drop the two “.jar” files we need here – junit and hamcrest. You generally won’t need to worry about doing this for class projects, but if you do your own Java development you might need to set up these (and other) jars.
Now, let’s add a test to capture the expected behavior of toString
on an empty board:
@Test
public void testEmptyBoard() {
assertEquals(". . .\n. . .\n. . .", new TicTacToeBoard().toString());
}
First, note that the method is prefixed with an @Test
decorator. This tells Code (all tools, that understand unit tests, really) to run this method as a test – not all methods in a “Test” class will be run as tests, only @Test
methods.
Next, note we had to deal with the “newlines” or line breaks. Strings in Java source code can’t contain explicit line breaks. So you “encode” them using an “escape character”. Escape characters are short sequences of characters prefixed by a “backslash” \
. \n
means “don’t actually treat this as backslash n, treat it as a newline.” There are others (and they’re used in other contexts, too) but this is enough for now.
Next, note the test returns void. We don’t care about it’s return value; instead, we use an assertion inside it.
Speaking of which, assertEquals
checks that the first argument (the expected value, which we hardcode to the “right” answer) and the second argument (the actual value, which should be computed by the code being tested) are equal; if so, the test passes, if not, it fails. The test can also have an error, if it (for example) throws an unexpected exception.
This is a pretty common pattern, actually, where you write and test code in a driver class, then encode your test as a test case. This way, if you break something by accident later, you’ll discover it immediately before you move on.
More on tests
Good tests are generally small – testing just one thing.
They usually use assertEquals()
, where the first argument is what’s expected and the second is what’s being tested. (They might use assertTrue
or assertFalse
when checking boolean results, and there are a few others, but we’ll come back to them later). It’s important to write tests this way, so that tools (like, for example Code) show the right things to you in their UI.
TicTacToeSoFar
So what do we have so far?
package tictactoe;
public class TicTacToeBoard {
private enum Space {
X, O, EMPTY;
public String toString() {
if (this == X) {
return "X";
} else if (this == O) {
return "O";
} else if (this == EMPTY) {
return ".";
}
return "WTFBBQ"; // this should never happen
}
}
private Space[] board;
public String toString() {
String string = "";
for (int i = 0; i < board.length; i++) {
string = string + board[i];
if (i % 3 == 2) {
string = string + '\n';
}
else {
string = string + ' ';
}
}
return string.substring(0, string.length() - 1);
}
public TicTacToeBoard() {
board = new Space[9];
for (int i = 0; i < board.length; i++) {
board[i] = Space.EMPTY;
}
}
}
package tictactoe;
public class TicTacToDriver {
public static void main(String[] args) {
}
}
and a test:
package tictactoe;
import static org.junit.Assert.*;
import org.junit.Test;
public class TestTicTacToeBoard {
@Test
public void testEmptyBoardPrinting() {
// do some setup
TicTacToeBoard board = new TicTacToeBoard();
// test that a value is what you expect it to be
// two arguments:
// first: what you expect
// second: what you're testing ("actual")
assertEquals(". . .\n" +
". . .\n" +
". . .", board.toString());
}
}
This is a pretty common pattern, actually, where you write and test code in a driver class or method, then encode your test as a test case. This way, if you break something by accident later, you’ll discover it immediately before you move on.
Output legibility
Remember I made EMPTY
toString()
itself as a single .
Usually this sort of thing is a good idea – anything to make your life easier when reading your code (and its output) will pay dividends in the future, either to you or the programmer who’s reading your code in six months.
Schools, especially intro classes, are very misleading about the code lifecycle. You spend most of your time in school writing your own code. But in the real world, you spend far more time reading, modifying, and fixing others’ code. And once in a while, when you’re reading and debugging some particularly gnarly code, you’ll come to an awful realization: The programmer who wrote this mess was you, six months ago. Don’t let this happen to you!
Back to the programming
Let’s sketch out the driver so we know what we’ll need to do. How is a typical game played?
- The board is empty
- Players take turns until the game is over, that is
- one player wins
- or it’s a draw (tie game)
- Taking a turn means choosing a valid move (and updating the game state accordingly)
- The game ends, the result is announced
What might this look like in code? Here’s a first cut.
public static void main(String[] args) {
TicTacToeBoard board = new TicTacToeBoard();
try (Scanner scanner = new Scanner(System.in)) {
while (!board.isGameOver()) {
System.out.println(board);
System.out.println("Space " + board.getCurrentPlayer() + ", choose a move (0-8):");
int move = scanner.nextInt();
board.move(move);
}
System.out.println("Game result: " + board.getResult());
}
}
Notice that we don’t worry about various error conditions (like if the player chooses an invalid move); we’ll get to that later. Also notice we haven’t yet defined any of these methods – we’ll do that soon.
Also notice that we’ve implicitly divided the methods into observers, which return a fact about the underlying object but which don’t change the object’s state, and mutators, which change something about the object. It’s good practice to put methods into one or the other of these groups, to help us reason about our code.
Let’s leave isGameOver()
alone for now, and work on getCurrentPlayer()
and move().
Ways to implement an observer
getCurrentPlayer()
returns the current player. We can either explicitly keep track of whose turn it is (in an instance variable), or compute it by looking at the board (if there are an equal number of Xs and Os, it’s X’s turn; if there are more Xs, it’s O’s turn). Let’s keep track explicitly for now by adding an instance variable and initializing it in the constructor.
private Space currentPlayer;
//...later, in the constructor
currentPlayer = Space.X;
Refactoring
As we program, sometimes it becomes clear we’ve named something poorly, and a better name would be more readable.
For example, what about renaming Space
to Player
, since that’s kinda a better name? Code lets you “refactor” code (what “refactoring” means is kinda complicated, but…). One simple refactoring is to rename a variable or type. You could just find-and-replace, but Code is smart enough to change only the variable you “mean” (for example, if you rename x
to count
in a method, it won’t change any variable named x
in other methods). Select the variable or type, then choose “Rename” in the command palette, or right click, to do this task.
Another observer
Let’s make sure we return it in our getter:
public Player getCurrentPlayer() {
return currentPlayer;
}
A mutator
And, at the end of every move, it should be the other player’s turn, so let’s encode that, too:
public void move(int move) {
if (currentPlayer == Player.X) {
currentPlayer = Player.O;
} else { // currentPlayer == O
currentPlayer = Player.X;
}
}
Note that this method, which changes the object’s state, does not return a value (it’s void
). Often mutators are void, since you generally don’t want to mix mutation and observation.
Checking in the driver, it works, inasmuch as the players alternate.
Testing our mutator
We showed in the console that the player’s turn will swap back and forth after each move(). Let’s start by adding a test that captures this behavior:
@Test
public void testPlayerChangeAfterMove() {
TicTacToeBoard board = new TicTacToeBoard();
assertEquals("X", board.getCurrentPlayer().toString());
board.move(0);
assertEquals("O", board.getCurrentPlayer().toString());
}
And while we’re at it, why not a bunch of moves?
@Test
public void testPlayerChangeAfterMultipleMove() {
TicTacToeBoard board = new TicTacToeBoard();
assertEquals("X", board.getCurrentPlayer().toString());
board.move(0);
assertEquals("O", board.getCurrentPlayer().toString());
board.move(1);
assertEquals("X", board.getCurrentPlayer().toString());
board.move(2);
assertEquals("O", board.getCurrentPlayer().toString());
board.move(3);
assertEquals("X", board.getCurrentPlayer().toString());
// ..etc
}
OK, and since we’re getting crazy, how about we get move() working now, too? It’s only a one-liner:
board[move] = Player.X;
And test it in the console. Huh, there’s a problem. Now, I get it, most of you see the problem immediately. But that’s kinda on purpose here; some problems are easier than others to spot (and then fix). Sometimes they’re simple problems you just can’t see yourself; sometimes they’re more subtle. Instead of caveman debugging this one (you know, adding “printlns” everywhere, let’s try the debugger) so we can see what’s going on.
First, we must set a breakpoint! Click the “gutter” and create a small red dot.
Then run under the debugger. You can do this in the command palette, or just click “Debug” right above the main method. Code executes until it reaches the breakpoint, and then pauses there. The yellow arrow shows you what line you’re paused on.
The “debug” pane on the left is immediately useful – you can see all your breakpoints (and set other kinds), and you can examine the value of variables in the running program. You can also hover over things in the source to see their current value in the program.
We can “step” forward line-by-line. “over” means stay in this method; “into” means follow any method call in that line into another method; “out” means return to the calling method.
The second time through, we see that the wrong value is assigned to the board entry – whoops, we hardcoded it! Let’s fix that:
board[move] = currentPlayer;
And test it in the console with our driver.
And maybe some tests now too:
@Test
public void testOneMove() {
TicTacToeBoard board = new TicTacToeBoard();
board.move(0);
assertEquals("X . .\n. . .\n. . .", board.toString());
}
@Test
public void testMultipleMoves() {
TicTacToeBoard board = new TicTacToeBoard();
board.move(0);
assertEquals("X . .\n. . .\n. . .", board.toString());
board.move(1);
assertEquals("X O .\n. . .\n. . .", board.toString());
board.move(2);
assertEquals("X O X\n. . .\n. . .", board.toString());
// and so on...
}
Again, (one of) the reason(s) we encode this stuff into tests is so that if we break something later, we’ll spot it immediately and have a working test case to debug with.
Finding the end of the game
How about checking for the end of the game? In English, in isGameOver()
we check to see if either player has won or if it’s a draw. If any of those are true, the game is over, otherwise it’s a draw. Similarly, our getResult()
method needs to determine which player won, or if it’s a draw.
Stopping and thinking for a second: we can express our draw check in terms of our winner check. If neither player has won, and the board is full, then it’s a draw.
What does this look like?
public boolean isGameOver() {
return (isWinner(Player.X) || isWinner(Player.O) || isDraw());
}
How do we check if a player has won? If they have three in a row. We could hardcode this, for example:
private String getWinner() {
if (board[0][0].equals(X) && board[0][1].equals(X) && board[0][2].equals(X)) {
return X;
}
else if //...
}
but we’d have to do 2 * (3 + 3 + 2) if clauses, mostly copy-pasted, making sure we didn’t screw up the board index each time. That’s OK, I guess, but it fails to capture the patterns we’re looking for: rows, columns, diagonals. Maybe something like this would be better:
private boolean isWinner(Player player) {
// check rows
for (int i = 0; i < 7; i += 3) { // pick a row (starting at i)
boolean won = true;
for (int j = 0; j < 3; j++) { // check each square in that row
if (board[i + j] != player) {
won = false; // any one is not this player
}
}
if (won) return true;
}
// check cols
for (int i = 0; i < 3; i++) { // pick a column (starting at i)
boolean won = true;
for (int j = 0; j < 7; j += 3) { // check each square in that column
if (board[i + j] != player) {
won = false; // any one is not this player
}
}
if (won) return true;
}
// check diagonals
if (board[0] == player && board[4] == player && board[8] == player) {
return true;
}
if (board[2] == player && board[4] == player && board[6] == player) {
return true;
}
// maybe it would have been shorter just to do the eight cases individually!
// context matters.
return false;
}
Two notes: First, we could further condense the row/column checks with another inner for loop (though it’s not much cleaner); second, notice this is a version of the for loop pattern we talked about in a previous class, where we check a bunch of stuff: if it ever becomes true, then the method returns true, and if we make it to the end, the method returns false.
How about a draw? If either player has won, it’s not a draw. Or, if there are still empty spaces, it’s not a draw. Otherwise, it’s a draw:
private boolean isDraw() {
// has someone won?
if (isWinner(Player.X) || isWinner (Player.O)) {
return false;
}
// otherwise, is the board full?
for (Player s : board) {
// if any of them are empty, no, it's not full
if (s == Player.EMPTY) {
return false;
}
}
return true;
}
Notice I can freely call these “observer” methods (isDraw, isWinner, etc.): they only observe values, but they don’t change anything. Having methods like this means I don’t have to worry about “breaking” an object. A common beginner style error is to have a method that both answers a question about an object and changes its state (Imagine a method that, say, both makes a move and returns the current winner, if any. How do you then check winner without making a move, too? You can’t, unless you add another method. But then there are two methods that check winner, which means you need to keep them in sync, which means it’s easier to get later things wrong…).
Now we can also pretty easily return the game result, too:
public String getResult() {
if (isWinner(Player.X)) {
return "Player X wins!";
}
else if (isWinner(Player.O)) {
return "Player O wins!";
}
else if (isDraw()) {
return "It's a draw.";
}
else {
return "Game ain't over yet.";
}
}
It appears to work (demo in console). One possible test might be:
@Test
public void testDiagonalXWinner() {
TicTacToeBoard board = new TicTacToeBoard();
board.move(0);
board.move(1);
board.move(4);
board.move(3);
board.move(8);
assertEquals("Player X wins!", board.getResult());
}
OK, enough tic-tac-toe. There’s more you could do (like make sure moves are in the right range, and not over an existing player’s move) but I’m sure you could figure it out (and test it!) yourself.
Code and testing note!
There’s one last thing to tell you. Tests interact poorly with infinite loops. So at the top of most test files in this course’s assignments, you’ll see something like this:
@Rule
public Timeout globalTimeout = Timeout.seconds(10); // 10 seconds
This tells JUnit that if a test takes more than 10 seconds, it should just fail. If you don’t do this and your code contains an infinite loop, the tests will “hang” on any test that calls that code. And your laptop will heat up and the fan will spin and you’ll drain your battery, etc.
So the timeout stops this from happening. But it also stops the test if you’re running it in the debugger. If you plan to use the debugger, you need to disable the timeout. Do this by commenting out both lines (the @Rule
and the public Timeout...
lines), and the debugger will no longer exit after 10 seconds – but if you have an infinite loop, tests will hang. (Gradescope takes 10 minutes to time out.)