package mines; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import javax.swing.JPanel; /** * Represents a "mine field", a grid of squares where each square might * or might not hold a mine. The user has to find a safe path from the * upper left square to the lower right square. The user can only click * square that are next to squares that have already been revealed. If * the user clicks a square that contains a mine, the mine explodes and * the game ends. If the user clicks a safe square, its color changes * and the number of mines in neighboring square (if any) is shown in * the square. *
The user can right-click a square to indicate that the user thinks * it contains a mine. Right-clicking a square that has been marked in * this way will unmark it. */ public class MineField extends JPanel { private final static int ROWS = 20; // number of rows on the board private final static int COLS = 20; // number of columns on the board private final static Color LIGHT_GREEN = new Color(180,255,180); // color for unvisited squares private final static int STATE_UNVISITED = 0; // constant for use in state array private final static int STATE_FLAGGED = 1; // constant for use in state array private final static int STATE_REVEALED = 2; // constant for use in state array private int[][] state; // This array holds one of the values STATE_UNVISITED, STATE_FLAGGED, // or STATE_REVEALED for each square on the board. The initial value // is STATE_UNVISITED. If the user drops a "flag" on the square, indicating // that the user thinks there is a bomb there, the state is changed to // STATE_FLAGGED. When the user visits a square by clicking on it (and // doesn't get blown up), the state is changed to STATE_REVEALED. // THIS VERY IMPORTANT ARRAY IS USED THROUGHOUT THIS CLASS. private boolean[][] mined; // mined[r][c] is true if there is a mine in square (r,c) private int mineCount; // number of mines on the board, set when game is started private boolean gameInProgress; // set to true while a game is in progress private boolean userWon; // when gameInprogress == false, this tells whether user won the game /** * Create a board that has 50 mines scattered in it at random positions, * and start the first game. This constructor just calls {@link #MineField(int)} * with a parameter value of 50. */ public MineField() { this(50); } /** * Create a board that has a specified number of mines scattered in it at * random positions, and start the first game. * @param mineCount the number of mines that are to be placed on the board. * The value should be "reasonable" (not too large or too small), but no * error checking is done. Values that are too large or too small might * cause infinite loops as the program tries to find a legal board with * the specified number of mines. */ public MineField(int mineCount) { setPreferredSize( new Dimension(2 + 24*COLS, 2 + 24*ROWS )); startGame(mineCount); addMouseListener( new MouseAdapter() { public void mousePressed(MouseEvent evt) { doMouseClick(evt); } }); } /** * Returns the number of mines on the board. This number is set when a * game begins, and does not change until a new game is started. */ public int getMineCount() { return mineCount; } /** * This method processes the event that occurs when the user clicks the board. */ private void doMouseClick(MouseEvent evt) { int row = (evt.getY() - 1) / 24; // row where the user clicked int col = (evt.getX() - 1) / 24; // column where the user clicked if ( row < 0 || row >= ROWS || col < 0 || col >= COLS) return; // ?? click is not actually on the board. if (state[row][col] == STATE_REVEALED) return; // The user has already revealed the square, so click has no effect. if (evt.isMetaDown() || evt.isShiftDown()) { // When the user right-clicks or shift-clicks, it means that the user wants // to flag the square as containing a bomb, or, if the square is already flagged, // to remove the flag. (Note that the user might be wrong!) The user can only // place a marker next to a revealed square; this is to avoid letting the user // make a trail of bomb markers all the way from the start to the goal -- that // was a bug in a previous version of the program. if (state[row][col] == STATE_UNVISITED) { if (! hasRevealededNeighbor(row,col)) return; state[row][col] = STATE_FLAGGED; } else // The state must be STATE_FLAGGED, since STATE_REVEALED has already been ruled out. state[row][col] = STATE_UNVISITED; } else { // The user wants to reveal the square at (row,col). This is only legal if there // is eitehr a neighboring square that is already revealed, or one that contains // a bomb marker. if (! hasRevealedOrMarkedNeighbor(row,col)) return; if (state[row][col] == STATE_FLAGGED) return; // The user is protected from accidently visiting a flagged square. visit(row, col); // Mark the square (and maybe some other squares) as visited. } repaint(); // Redraw the board to show its changed status. } /** * Draw the board, based on the current state of all the squares and on whether or * not the game is in progress. This method is called by the system and is not meant * to be called directly. */ protected void paintComponent(Graphics g) { g.setColor(Color.GRAY); g.fillRect(0, 0, getWidth(), getHeight()); for (int r = 0; r < ROWS; r++) for (int c = 0; c < COLS; c++) { if (mined[r][c] && !gameInProgress) g.setColor(Color.RED); else if (state[r][c] == STATE_FLAGGED) g.setColor(Color.PINK); else g.setColor(LIGHT_GREEN); if (state[r][c] == STATE_REVEALED) g.fill3DRect( 2+24*c, 2+24*r, 23, 23, false ); else g.fill3DRect( 2+24*c, 2+24*r, 23, 23, true ); g.setColor(Color.BLACK); if (state[r][c] == STATE_FLAGGED) g.drawString("B", 6+24*c, 15+24*r); else if (state[r][c] == STATE_REVEALED) { int bombs = bombCount(r,c); if (bombs > 0) g.drawString("" + bombs, 6+24*c, 15+24*r); } } if (!gameInProgress) { g.setColor(Color.BLUE); g.setFont(new Font("SERIF", Font.BOLD, 36)); g.drawString("Game Over.", 30, 60); if (userWon) g.drawString("YOU WIN!", 30, 120); else g.drawString("YOU LOSE!", 30, 120); } } /** * Begin a new game with a specified number of mines. If a game is * in progress, that game is aborted and no warning or error message is * given. * @param mineCount The number of mines to place on the board. The value * should be "reasonable"; see the comments on the constructor {@link #MineField(int)}. */ public void startGame(int mineCount) { System.out.println("Start a game with " + mineCount + " mines."); this.mineCount = mineCount; while (true) { // This loop ends when a valid board is created. gameInProgress = true; mined = new boolean[ROWS][COLS]; // All values are initially false. state = new int[ROWS][COLS]; // All values are initially STATE_UNVISITED. for (int i = 0; i < mineCount; i++) { // Place a mine at a random position. int r,c; while (true) { r = (int)(ROWS * Math.random()); // randomly selected row number c = (int)(COLS * Math.random()); // randomly selected column number if ( (r + c > 2) && (r < ROWS-1 || c < COLS-1) && ! mined[r][c] ) { // End the loop if the randomly selected position is OK, otherwise, // try again with a different position. The test "r + c > 2" ensures // that positions (0,0), (1,0), (0,1), (1,1), (0,2), and (2,0) are // not mined, and in particular that the upper-left corner is not // mined and has no neighbors that are mined. The other two tests // ensure that the lower-right "home" position is not mined and // that there is not already a mine in the selected square. break; } } mined[r][c] = true; } visit(0,0); // Get the user started by automatically visiting the upper-left square. if (configOK()) // Checks whether the board is valid. break; } repaint(); } /** * The user can only place bomb markers in squares that are next to already revealed * squares. This method checks that a given square is in fact next to a revealed square. */ private boolean hasRevealededNeighbor(int row, int col) { if (row > 0 && state[row-1][col] == STATE_REVEALED) return true; if (row < ROWS-1 && state[row+1][col] == STATE_REVEALED) return true; if (col > 0 && state[row][col-1] == STATE_REVEALED) return true; if (col < COLS-1 && state[row][col+1] == STATE_REVEALED) return true; return false; } /** * The user can only reveal squares that are next to already revealed squares or * next to bomb markers. This method checks that a given square is in fact next * to a such a square */ private boolean hasRevealedOrMarkedNeighbor(int row, int col) { if (row > 0 && state[row-1][col] > STATE_UNVISITED) return true; if (row < ROWS-1 && state[row+1][col] > STATE_UNVISITED) return true; if (col > 0 && state[row][col-1] > STATE_UNVISITED) return true; if (col < COLS-1 && state[row][col+1]> STATE_UNVISITED) return true; return false; } /** * This method is called when a square is visited by the user. */ private void visit( int row, int col ) { if (mined[row][col]) { // The user has stepped on a mine and gets blown up. gameInProgress = false; userWon = false; } else { // It's OK for the user to step on this square. Mark it as revealed. reveal(row,col); if (state[ROWS - 1][COLS -1] == STATE_REVEALED) { // User has reached the home square! gameInProgress = false; userWon = true; } } } /** * Counts the bombs in the squares that neighbor position (row,col). */ private int bombCount(int row, int col) { int ct = 0; if (row > 0) { if (col > 0 && mined[row-1][col-1]) ct++; if (mined[row-1][col]) ct++; if (col < COLS-1 && mined[row-1][col+1]) ct++; } if (col > 0 && mined[row][col-1]) ct++; if (col < COLS-1 && mined[row][col+1]) ct++; if (row < ROWS-1) { if (col > 0 && mined[row+1][col-1]) ct++; if (mined[row+1][col]) ct++; if (col < COLS-1 && mined[row+1][col+1]) ct++; } return ct; } /** * Tests whether a new game board is valid. This is called by startGame() to * decide whether to use a random board that it has created, or to discard * the board and try again. */ private boolean configOK() { if ( state[ROWS-1][COLS-1] == STATE_REVEALED ) { System.out.println("Already solved."); return false; } return true; } /** * Marks the square in position (row, col) as visited. * (Don't do anything if the position is not on the board!) */ private void reveal(int row, int col) { if (row < 0 || row >= ROWS || col < 0 || col >= COLS) return; state[row][col] = STATE_REVEALED; } }