CIS 120 Homework 1: Minesweeper

Due Wednesday, September 16, 2009 at 11am. The whole assignment is worth 100 points.

Introduction

Minesweeper is a familiar game to many, since it comes preinstalled on many operating systems. If you do not have experience with it, spend a few minutes reading the Wikipedia article. The version of minesweeper we will be using for this assignment is slightly simplified. We will provide you with the files needed to actually play minesweeper; however, it will be your job to write a solver. In order to help you, we have broken the problem down into separate steps.

We have provided you with javadocs which help to explain the supplied code. In addition, the javadocs for Solver illustrate the methods we expect in your submitted code. (Note: These are just the minimum method requirements; if you feel the need to write helper methods, you can, though you probably won't need any for this assignment.) If you have any questions regarding the assignment, don't hesitate to post on the Bulletin Board: that's what it is there for.

Here are some files you will need:

Sections in this document:

  1. Getting Started
  2. Problem 1: Looking at Cell Neighbors
  3. Problem 2: Reasoning About the Grid
  4. Problem 3: Final Solver Construction
  5. Problem 4: Better Exploration
  6. Testing Your Code

Getting Started

Before you begin, you need to tell DrJava where to find the hw1.jar file. Under Edit -> Preferences, click the Add button under Resource Locations -> Extra Classpath and type the full path of the jar file.

Setting the jar location

We will put problems with DrJava and their solutions on a separate page as we encounter them. Check there first if you have a problem.

After loading the jar file, you have two options. One is to copy and paste each interaction shown below, while the other is to download the .hist file and run the interactions automatically. To do the latter, use Tools -> History -> Load Interactions History as Script. The output below shows each interaction (marked with a >) followed by the result from that interaction.

Sample Interactions (Mine1.hist)

Welcome to DrJava.
> Minesweeper game = new Minesweeper(10,10,10);  // Create a game instance of (10 [mines], 10 [rows], 10 [columns])
> game.explore(1,1)                              // Look for mines at location (1 [row], 1 [column])
> game.printBoard()                              // Show current state of board
..........                                       // Row 0
.1........                                       // The cell at (1,1) has 1 mine nearby.
..........
..........
..........
..........
..........
..........
..........
..........					 // Row 9
> game.mark (2,1);                       	 // Mark a potential mine at (2 [row], 1 [column])
> game.hint()                            	 // Ask the game for a hint
> game.printBoard()
..........
.1........                              	 // What we knew before
.*........                               	 // Our guessed mine
..........
..........
..........
..........
.1........                              	 // The hint!
..........
..........
> game.explore(2,2)     	                 // Look for mines at location (2 [row], 2 [column]) (Causes you to step on a mine)
> game.printBoard()     	                 // Show current state of board (Mine has been triggered)
00001@101@                              	 // The complete board is displayed.
0111111011
01@1011100					 // The mine we triggered is at (2,2)
011101@100
2210011100
@@21111000
23@11@2100
011112@100
0111011100
01@1000000

You should think of each cell as being in one of three different states:

  1. An unexplored cell has not yet been explored, nor has it been marked as a bomb. All cells start out as unexplored.
  2. An explored cell will accurately report whether it is a bomb and how many of its neighbors are bombs.
  3. A marked cell is a cell that the user has manually asserted is a bomb. Note that if the user made a mistake, this assertion might be wrong!

Note that these three states are mutually exclusive - for example, if a cell is "marked" then it is neither explored nor unexplored.

Now that you have a basic understanding of the Minesweeper class's functionality, use the javadocs to explore the different methods that we have provided. After looking at the javadocs you should be ready to think about how to write a solver. As shown below, there is a very simple solution that makes exclusive use of the hint method; we'll use it as a starting point. You should copy this code and save it in a file called Solver.java. (Alternatively, you can simply download the template Solver.java.)


class Solver {
    public Minesweeper game;
    public Solver () {
        game = new Minesweeper(10,10,10);
    }

    public int solve() {
        while (!game.isSolved()) {
            game.hint();
        }
        return game.getNumHints();
    }
}

Sample Interactions (Mine2.hist)

Make sure that you reset the interactions pane before beginning.
Welcome to DrJava.
> Solver s = new Solver();
> s.solve()
90

Of course, this "solution" consists entirely of cheating! Let's try to find a way to minimize the number of hints we are required to use.

The idea is to make as much progress as possible using logical deduction, and to only ask for a hint when we get stuck. Consequently, the supplied Solver.java actually looks like this:


class Solver {
    public Minesweeper game;
    public Solver () {
        game = new Minesweeper(10,10,10);
    }

    public int solve() {
        while (!game.isSolved()) {
            game.hint();
            makeAllDeductions();
        }
        return game.getNumHints();
    }

    /** Make as much progress as possible towards solving
      * the board, until we win or get stuck.
      */
    public void makeAllDeductions() {
    }
}

(If you are creating your own copy of Solver.java from scratch, you should modify it to look like this as well.)

Of course, at this point we haven't changed anything: makeAllDeductions() doesn't actually make any deductions yet! Later in the assignment you will be able to remedy this. We'll begin by building some of the tools needed to be able to reason about cells in the board and whether or not they contain bombs.

Problem 1: Looking at Cell Neighbors (28 points; file to submit: Solver.java)

The first step is to find out how many neighbors to a particular cell have been marked as bombs. Here is some code to accomplish this:

/** Determine the number of neighbors of cell (r,c) that have been
  *  marked as bombs. */
public int markedNeighbors(int r, int c) {
    int marked = 0;
    for (int i = Math.max(r-1, 0); i <= Math.min(r+1,game.getHeight()-1); i++) {
        for (int j = Math.max(c-1,0); j <= Math.min(c+1,game.getWidth()-1); j++) {
            if (!(i==r && j==c)) {
                if (game.isMarked(i,j)) {
                    marked++;
                }
            }
        }
    }
    return marked;
}
markedNeighbors iterates through the indices adjacent to the given location, being careful to stay within the boundaries of the board and to skip counting the center location itself, and counts the number of marked cells encountered. markedNeighbors has been provided for you in Solver.java, or you can copy and paste it into your own Solver.java.

For the first part of the assignment, your job is to write a method similar to markedNeighbors that determines how many of the neighboring cells have not been explored.

Your method should look like:

/** Return the number of neighbors of cell (r,c) that have not yet been explored. */
public int unexploredNeighbors(int r, int c) {
 ...
}
Your implementation of unexploredNeighbors should be very similar to the implementation of markedNeighbors: only the innermost counting logic needs to be changed. Remember, the number of unexplored neighbors does NOT include marked cells.

Sample Interactions (Mine4.hist)

> Solver s = new Solver()
> s.unexploredNeighbors(1,1)
8
> s.unexploredNeighbors(0,0)
3
> s.game.explore(1,2)
> s.unexploredNeighbors(1,1)
7

Problem 2: Reasoning About the Grid (28 points; file to submit: Solver.java)

Once we can count the number of marked neighbors, we can make deductions about them and explore the rest of the board. There are two kinds of deductions we can make: we can conclude that some cells are safe to explore, or we can conclude that some cells certainly contain bombs.

First, if a cell has the same number of marked neighbors and neighboring bombs, we know that it is safe to explore the rest of the neighbors of that cell. The following method checks to see if this condition is true, and if it is, explores all neighbors that are not marked as bombs. It returns whether any unexplored neighbors were found.

/** The neighbors of a cell can be explored if
 * the cell has been explored and *all* of the nearby bombs have been marked.
 * @return whether any neighbors were explored.
 */
boolean exploreNeighbors(int r, int c) {
    boolean changed = false;
    if (game.isExplored(r, c)) {
        // # of marked neighbors is equal to # of neighbors containing bombs
        if (markedNeighbors(r,c) == game.getNeighboringBombs(r,c)) {
            for (int i = Math.max(r-1, 0); i <= Math.min(r+1, game.getHeight()-1); i++) {
                for (int j = Math.max(c-1,0); j <= Math.min(c+1, game.getWidth()-1); j++) {
                    if (!(i==r && j==c)) {
                        if (!game.isMarked(i,j) && !game.isExplored(i,j)) {
                            changed = true;
                            game.explore(i,j);
                        }
                    }
                }
            }
        }
    }
    return changed;
}
exploreNeighbors has been provided for you in Solver.java, or you can copy and paste it into your own.

Second, we can use the information about the neighboring cells to conclude that some of those cells are bombs. The unexplored neighbors of a cell can all be marked as bombs if the cell has been explored and the number of nearby bombs is equal to the sum of the number of unexplored neighbors and the number of marked neighbors.

For the second part of the assignment, write and test this method, in the same Solver.java file:


/** The neighbors of a cell can *all* be marked as bombs if
  * the cell has been explored and the number of nearby bombs is equal to the
  * sum of the number of unexplored neighbors and the number of marked
  * neighbors.
  *
  * @return whether any neighbors were marked as bombs.
  */
boolean markNeighborsAsBombs(int r, int c) {
 ...
}

Sample Interactions (Mine5.hist)

> Solver s = new Solver();
> s.markNeighborsAsBombs(5,5)       // Unexplored cell, doesn't do anything
false
> s.game.printBoard()
..........
..........
..........
..........
..........
..........
..........
..........
..........
..........
> s.game.explore(2,9)
> s.markNeighborsAsBombs(2,9)       // Can't mark any neighbors here
false
> s.game.printBoard()
..........
..........
.........0
..........
..........
..........
..........
..........
..........
..........
> s.game.explore(4,0)              // Explore (4,0) and several neighbors
> s.game.explore(3,0)
> s.game.explore(3,1)
> s.game.explore(4,1)
> s.game.printBoard()
..........
..........
.........0
01........
22........
..........
..........
..........
..........
..........
> s.markNeighborsAsBombs(4,0)      // We found some bombs!
true
> s.game.printBoard()
..........
..........
.........0
01........
22........
**........
..........
..........
..........
..........

Problem 3: Final Solver Construction (28 points; file to submit: Solver.java)

We now have the pieces to make a complete solver.

The first step is to use exploreNeighbors and markNeighbors on all of the explored cells. This method should return whether any changes have been made to the board.

/** Examine all cells of the board using markNeighborsAsBombs and exploreNeighbors in row/column order.
 * @returns whether any updates were made to the board. */
public boolean checkBoard() {
 ...
}
You can now also fill in the makeAllDeductions() method which is called from solve(): it should repeatedly check the board for updates until no more updates can be made.

Sample Interactions (Mine6.hist)

> Solver s = new Solver();
> s.checkBoard()
false
> s.game.printBoard()
..........
..........
..........
..........
..........
..........
..........
..........
..........
..........
> s.game.explore(5,5)
> s.game.explore(5,6)
> s.game.explore(5,7)
> s.game.explore(5,8)
> s.checkBoard()
true
> s.game.printBoard()
..........
..........
..........
..........
......1100
.....11000
......2100
.......100
......1100
......0000
> s.makeAllDeductions()
> s.game.printBoard()
00001@101@
0111111011
01@1011100
011101@100
2210011100
@@21111000
23@11@2100
011112@100
0111011100
01@1000000

Problem 4: Better Exploration (16 points; file to submit: Solver.java)

The behavior of explore in the Minesweeper game that we have supplied is different from the standard one. To make things simple, explore just looks at a single cell. However, when there are no neighboring bombs, we know that all of the neighboring cells are safe for exploration. If those cells in turn also have no neighboring bombs, then all of those cells' neighbors are also safe for exploration, etc. A smarter approach is to continue exploring outward as long as we encounter cells with no neighboring bombs.

Sample Interactions (Mine7.hist)

Welcome to DrJava.
> Solver s = new Solver()
> s.deepExplore(4,4)
> s.game.printBoard()
..........
...111....
...101....
..1101....
..1001....
..2111....
..........
..........
..........
..........
>
Your task is to implement a deepExplore method for the Solver class.
  public void deepExplore (int r, int c) {
    ...
  }

This problem requires more thought than the previous problems - don't be alarmed if you can't think of how to do it immediately.

Testing Your Code

There is a suite of tests available to you from any of the SEAS Linux-based computers on campus. To take advantage of them, you must get your code onto a SEAS computer (for example, eniac.seas.upenn.edu), then log on yourself and run the test script.

The first step is to get your code onto the space you are given on the SEAS computers. (If you are developing your code in one of the computer labs on-campus, this step is not needed; open up a terminal and skip to the next step.) Otherwise, you should follow the instructions (found under "Resources" on the main CIS120 web page) for transferring files to eniac and logging in to eniac remotely. Under KDE, the terminals can be found under Applications -> System -> Terminal in the chameleon menu.

Now that the code is on a SEAS computer, you can run the tests. Navigate to the directory that has Solver.class and run ~cis120/pub/bin/test01. For example, my username is wagnerdm and I built my solution in ~/classes/120/minesweeper, so my session would look something like this:

wagnerdm@plus:~> cd classes/120/minesweeper
wagnerdm@plus:~> ~cis120/pub/bin/test01
CIS120 Homework 01
Test Results for null:
-----------------------------------
Question 1: Cell Neighbors
----------------------------------
 Test - Sample Interactions:  Passed - 4.0 points
 Test - Checking unexplored neighbors:  Passed - 5.0 points
(etc.)
If you want to see more details about any tests that fail, take a look at Tester.java, which defines the actual tests that are run. You don't need to understand Tester.java at all. Just search for the name of the test that failed and see what it does. For example, suppose that the test script reports that the test "Unexplored neighbors: Corner II" has failed. Searching through Tester.java for this test, we find this:
q1.addTest("Unexplored neighbors: Corner II", 5, new Tester("q1_test5"));
This means we must look for the test called q1_test5. Searching again, we find:
public void q1_test5() {
    Solver s = new Solver();
    s.game.reset();
    s.game.explore (9, 1);

    assertEquals (error("unexploredNeighbors"), 2, s.unexploredNeighbors (9, 0));
}
This test creates a new solver, resets the board, and explores the cell at (9,1). The last line is the most important: it asserts that after performing these operations, the result of s.unexploredNeighbors(9,0) ought to be 2, and if it isn't, some error message about "unexploredNeighbors" ought to be shown. So in this case, you need to figure out why unexploredNeighbors is giving the wrong result after creating a Solver and exploring (9,1).