CS200 Lab 4
Overview
This lab will explore induction and program debugging with assertions. Specifically, we will be covering:
- The induction worksheet and related exercises
- A review of PA2 and finding your partner
- A discussion on debugging and basic assertion usage
- Get to know your partner by debugging minesweeper
Lab Assignment
You will need to following files to complete the induction portion of this lab:
Meet your partner!
After reviewing PA2, you'll be working with your partner on the last portion of this lab:
Create an R4 in your workspace, you will need the following files:
Debugging Overview
This week's assignment involves very little coding on your
part. Instead, you will be reading, understanding, and
debugging an application.
Minesweeper is a simple, but popular, game available on virtually
every platform. Given a square board (n rows by n columns),
the player must identify board spaces that contain a mine (or
bomb). At the start of the game, all squares obscure whether or not
they contain a mine. However, the user is given hints about the
locations of mines by other board spaces. Specifically, board spaces
adjacent to one or more mines tell how many mines they neighbor. This
enables the player to detect patterns and deduce where a mine is most
likely to appear.
Once the player has guessed that a space contains a
mine, they may "flag" the board space. The user wins the game by
successfully flagging every mine or by exploring every space that does
not contain a mine. The player is initially given a number of flags
equal to the number of mines so that they may not simply guess that
every space is a mine. Finally, the player immediately loses the game
if they explore a board space that contains a mine. Upon winning or
losing the game, the player is shown the true board layout (all
unexplored spaces are shown).
The provided code is a broken implementation of a text-based
minesweeper game. Here is an example of a game in progress:
0 1 2 3 4 5 6 7 8 9
0| | | | | | | | | | |
1| | | | | | | | | | |
2| | | |1|1|1| | | | |
3| | | |1|?|1| | | | |
4| | | |1|1|1| | | | |
5| | | | | | | | | | |
6| | | | |1|1|1| | | |
7| | | | |1|?|2|2|1|1|
8| | | | |1|3|?|?|?|?|
9| | | | | |2|?|?|?|?|
Flags Remaining: 5
The top and left side of the board describe the column and row number, respectively. Board spaces are delimited by the "|" character. The top left board square is identified as (0, 0). This example contains several types of board spaces. Initially, all board spaces are marked as "hidden", that is, it is uncertain whether or not the space contains a mine. Hidden spaces are denoted by "?". Explored spaces that do not contain a mine and are not adjacent to a mine are referred to as "cleared". Cleared spaces are represented by " ". Board spaces adjacent to a mine contain an integer [1, 8] that describes how many mines the space neighbors. For example, the board space at row 3, column 3 contains the value 1. In other words, (3, 3) is adjacent to only 1 mine. Adjacency is counted in terms of left, right, up, down, as well as diagonal directions. Consider the board space (8, 5) that is adjacent to a total of 3 mines.
Given this board state, the player may wish to immediately "flag" a mine. Here, (3, 4) is a fairly obvious choice. Once the user enters the "flag" command, the board state would change to
0 1 2 3 4 5 6 7 8 9
0| | | | | | | | | | |
1| | | | | | | | | | |
2| | | |1|1|1| | | | |
3| | | |1|F|1| | | | |
4| | | |1|1|1| | | | |
5| | | | | | | | | | |
6| | | | |1|1|1| | | |
7| | | | |1|?|2|2|1|1|
8| | | | |1|3|?|?|?|?|
9| | | | | |2|?|?|?|?|
Flags Remaining: 4
where the board space (3, 4) is now represented by "F". Note that the "Flags Remaining" status was also updated from 5 to 4. That is, the player has consumed a flag by placing it on the board. The player is initially given a sufficient number of flags to cover all placed mines.
Suppose the player chose to reveal a board space that contained a mine. For example, (7, 5):
0 1 2 3 4 5 6 7 8 9
0| | | | | | | | | | |
1| | | | | | | | | | |
2| | | | | | | | | | |
3| | | | |X| | | | | |
4| | | | | | | | | | |
5| | | | | | | | | | |
6| | | | | | | | | | |
7| | | | | |M| | | | |
8| | | | | | |M| |M| |
9| | | | | | |M| | | |
GAME OVER
Here, we see an "X" at position (3, 4) denoting a mine that was correctly flagged.
The remaining mines, including the one revealed by the player, are displayed as "M".
Finally, note that a GAME OVER message is displayed and the program terminates.
Similarly, the full board is displayed if the player wins the game along with an appropriate message.
Thus far, we have mentioned two distinct commands the player may enter to play the game: reveal and flag. These commands are entered at the displayed ">" prompt.
- r a b : "reveals" what is present in the board space located at (row a, column b). If this space contains a mine, the player loses. The player may win the game by revealing all board spaces that are not mines.
- f a b : "flag" board space (row a, column b) as a mine. The player wins the game if they manage to flag all mines.
- q : "quit" the game.
Your Task
The provided implementation of minesweeper contains numerous bugs. Talk with your partner about how to read and interpret the provided code documentation to understand what each method should accomplish. You will then use this understanding to generate assertions using the assert
keyword that reflect the method's goals. These assertions will enable you quickly identify bugs and reason about what code modifications should be made (and then make them).
In summary, your workflow for each method will roughly consist of:
- Understand how the method should work by reading its documentation.
- Convert your knowledge into
assert
statements in the code.
- Observe the program's behavior by running it and seeing what assertions are violated and/or exceptions are thrown.
- Read the program's code to understand why the observed behavior happens (or when it happens).
- Fix the problem.
- Re-run the program to confirm the bug is fixed.
This assignment does NOT involve writing a substantial amount of code. If you find yourself writing much more than a single if-statement here and there, something is wrong. Most of the required fixes are minor tweaks to the existing code.
Furthermore, you need only modify the code in BoardModel.java. There are no intentional bugs in BoardSpace.java and BoardController.java to worry about. BoardController is responsible for running the minesweeper game and interacting with the user. BoardSpace objects are used by the BoardModel to represent game spaces. You may encounter bugs in how BoardModel uses BoardSpace objects.
You should assume that the provided documentation is correct. There are no intentional bugs in the documentation.
Overview of the Program
The provided minesweeper game consists of three classes: BoardController, BoardModel, and BoardSpace. BoardController handles all I/O and interaction with the player. The BoardModel represents the current state of the game. More specifically, BoardModel describes the rules of the game (win/loss conditions, move validity, etc.) and the state of the game in progress (revealed spaces, flag/mine locations, etc.).
Internally, the game board is represented as a two dimensional array of BoardSpace objects in (row, column) order. For example:
int row = 5;
int col = 2;
BoardSpace space = board[row][col];
refers to BoardSpace stored in row 5, column 2. Recall that row 0, column 0 is the top left corner of the board. Each BoardSpace describes whether or not the space has been revealed (directly or indirectly), if the space contains a mine, if the space has been flagged, and the number of mines that are adjacent to the space. In general, a BoardSpace object is responsible for storing information related to how that particular space is view by the user over the course of the game.
Board spaces are initially not "visible" to the player. That is, the player does not know whether or not a space contains a mine until it has been revealed. This information is displayed once the player reveals a space either directly (via the reveal command) or indirectly (triggered exploration). The BoardModel is the intermediary for all interaction with BoardSpace objects. The BoardController has no knowledge of the details of the BoardSpace class.
BoardModel contains numerous instance variables that help it keep track of the game's state. Aside from the previously discussed 2D board, BoardModel also keeps track of the unflagged mines (remainingMines), the number of mines that should be laid, whether or not the user has made their first move, and whether the game has been lost or won. BoardModel retains state of the user's first move in order to improve the game's playability. Mines are only laid after the player has made their first move. This prevents the player from picking a mine on their first move and immediately losing the game. We will discuss mine laying in more detail shortly.
Constructing BoardModels
Minesweeper games are created with respect to an initial board size and a difficulty setting. For the purposes of our game, the board should be a square with at least 3 rows. Anything smaller would not be a very fun game. The difficulty level determines the number of mines that should be laid after the player's first move. We allow three difficulty settings: easy, normal, and hard. Let R be the number of rows in the game board. The number of mines laid, M is set according to the following equations:
- Easy: M = R/2
- Normal: M = R
- Hard: M = R + R/2
Game instances must also have at least 1 mine. Otherwise, it is not much of a game.
Finally, the constructor must ensure other pieces of the game's state are properly initialized. However, this task is delegated to the resetBoard() method. We will discuss this method soon.
Debugging the Constructor (Warm Up)
The initial BoardModel(int row, int col, int difficulty) constructor contains:
public BoardModel(int rows, int cols, int difficulty)
{
board = new BoardSpace[rows][cols];
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
board[r][c] = new BoardSpace();
}
}
switch (difficulty) {
case HARD:
numMines = rows + (rows/2);
break;
case NORMAL:
numMines = rows;
break;
case EASY:
default:
numMines = rows/2;
break;
}
resetBoard();
}
We begin by adding assertions to the constructor that enforce the original design. More specifically, a game must be at have at least 3 rows and columns and must form a square board. A board must also contain at least one mine.
public BoardModel(int rows, int cols, int difficulty)
{
//Smallest board allowed is a 3x3
assert rows >= 3 : "Expected rows >= 3, got " + rows;
assert cols >= 3 : "Expected cols >= 3, got " + cols;
//We only use a square board (rows == cols)
assert rows == cols: "Expected rows == cols, got rows = " + rows + " cols= " + cols;
board = new BoardSpace[rows][cols];
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
board[r][c] = new BoardSpace();
}
}
switch (difficulty) {
case HARD:
numMines = rows + (rows/2);
break;
case NORMAL:
numMines = rows;
break;
case EASY:
default:
numMines = rows/2;
break;
}
//Assert that there is at least 1 mine, otherwise it's not much of a game
assert numMines >= 1 : "Expected numMines >= 1, got " + numMines;
resetBoard();
}
These requirements lead to four assertions. Note that Java assertions can take two distinct forms. We use a variation on the form used in class that enables us to print a special error message if the assertion is violated. The portion after the ":" is the message that is displayed is similar to what you would pass to System.out.println().
Examining the code reveals that the constructor has several bugs. First, we are trusting that the caller will pass rows and cols a values greater than or equal to 3. Similarly, we have no guarantee that rows will have the same value as columns and could potentially end up with a board that is not a square. If we assume that we have at least 3 rows and columns, then we can be certain that we will always have at least 1 mine since 3/2 is 1 (integer division). If we were to run this code, as you will for the later methods, the first three assertions could be tripped. As such, add some small fixes to ensure that we always have appropriate values.
public BoardModel(int rows, int cols, int difficulty)
{
//Fix having less than 3 rows
if (rows < 3) {
rows = 3;
}
//Fix having less than 3 columns
if (cols < 3) {
cols = 3;
}
//Smallest board allowed is a 3x3
assert rows >= 3 : "Expected rows >= 3, got " + rows;
assert cols >= 3 : "Expected cols >= 3, got " + cols;
//Fix non-square board
if (rows != cols) {
cols = rows;
}
//We only use a square board (rows == cols)
assert rows == cols: "Expected rows == cols, got rows = " + rows + " cols= " + cols;
board = new BoardSpace[rows][cols];
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
board[r][c] = new BoardSpace();
}
}
switch (difficulty) {
case HARD:
numMines = rows + (rows/2);
break;
case NORMAL:
numMines = rows;
break;
case EASY:
default:
numMines = rows/2;
break;
}
if (numMines == 0) {
numMines = 1;
}
//Assert that there is at least 1 mine, otherwise it's not much of a game
assert numMines >= 1 : "Expected numMines >= 1, got " + numMines;
resetBoard();
}
Running the program should now successful run the BoardModel constructor without failing any assertions. The remainder of this lab page will serve as the documentation for the remaining methods that you must debug. You should follow the above workflow to help you fix the problems.
The Rest
Your lab assignment is to fix bugs in the following methods:
- resetBoard(): responsible initializing the remaining pieces of game state. resetBoard() assumes that the 2D board array has been initialized so that it may "reset" each individual BoardSpace via their reset() methods. Then, since a new game has been started, isFirstTurn should be set to true. Similarly, since the player has not yet taken a turn, the game has not yet been won or lost.
resetBoard() must initialize the remainingMines ArrayList to keep track of mines that have not yet been flagged by the player. Mines are only laid when the player takes their first turn. As such, remainingMines should be empty at this stage.
- layMines(int row, int col): places exactly numMines mines within the boundaries of the board. Each board space may contain at most 1 mine, so this method should not place a mine where there already is one. In order to enhance playability, the layMines method does not place a mine at the board space specified by (row, col).
layMines also computes the number of mines adjacent to each board space. This information is displayed to user when the space is actually revealed/explored. We are only concerned with adjacency counts for spaces that are not mines since mined spaces cannot be explored without losing the game.
- isValidMove(int row, int col): determines if the board space specified by (row, col) is a valid move for the player to make. The player may only choose board spaces that currently hidden (unexplored) and (row, column) must be within the board's boundaries.
- reveal(int row, int col): exposes the board space specified by (row, col) if it would be a valid move. If it is the player's first turn, the reveal method is also reponsible for calling layMines using row and col. The player immediately loses the game if they select a board space that contains a mine (the lose instance variable is set to true and later detected by BoardController).
If the player's move does not result in losing the game, the reveal method invokes explore(row, col) to perform the work of actually exposing board spaces. Afterwards, the reveal method checks if every board space that does not contain a mine has been explored. If so, the game immediately ends in victory for the player (won is set to true and detected by BoardController). The onlyMinesRemain() method should be used to determine if this event has occurred.
- explore(int row, int col): the work horse of BoardModel. The explore method makes the board space specified by (row, col) visible and recursively explores each of its eight neighbors that are not adjacent to a mine. In other words, a neighboring space is only explored if adjacentMineCount(r, c) is 0. Otherwise, the neighbor is only made visible to prevent accidentally exploring a space containing a mine. In general, the explore method does not visit board spaces that have been marked as visible.
(Hint: As we have only just begun to cover recursion in class, there are no intentional bugs in the explore method's recursion logic in terms of start or stopping the process. However, you should pay special attention to how it chooses neighboring spaces to visit. In other words, you may need to win or lose a game to see the problem.)
- flag(int row, int col): "flags" a board space as a potential mine if (row, col) represents a valid move. The player wins if they are able to flag every mine. Note that because there are exactly numMines flags, there is no room for error on the player's part. Players may also undo a flag placement by invoking the flag method on a position that already contains a flag.
- unflag(int row, int col): removes a flag from the board position (row, col). Note that unflag must essentially restore the state that was modified by the flag method and return that BoardSapce to the remainingMines ArrayList if that space held a mine.