import java.awt.*; import java.awt.event.*; import javax.swing.*; /** * A graphical interface driver for MineSweeper.
* Takes advantage of the same (implicit) interface provided by the MineSweeper class (As do all drivers).
* Supports the same command line options as the textual driver (See {@link #printHelp() printHelp()}). * @author Nick Neely */ public class PlayMinesGui { private static final String HELP_SHORT_ARGUMENT = "-h"; private static final String HELP_LONG_ARGUMENT = "--help"; private static final String DEBUG_SHORT_ARGUMENT = "-d"; private static final String DEBUG_LONG_ARGUMENT = "--debug"; private static final int NORMAL_EXIT_CODE = 0; private static final int ERROR_EXIT_CODE = 1; // These should have been public in MineSweeper.java private static final char BLANK_TILE = ' '; private static final char CLOSED_TILE = 'X'; private static final char FLAGGED_TILE = 'F'; private static final char MINE_TILE = '*'; private static final int COMMAND_OPEN = 0; private static final int COMMAND_FLAG = 1; private static final int COMMAND_UNFLAG = 2; private static final String GAME_WINDOW_TITLE = "CS61B MineSweeper"; private static final int INITIAL_WINDOW_WIDTH = 600; private static final int INITIAL_WINDOW_HEIGHT = 600; private static final int INITIAL_WINDOW_X = 0; private static final int INITIAL_WINDOW_Y = 0; private static final Color CLOSED_TILE_BACKGROUND_COLOR = Color.black; private static final Color CLOSED_TILE_FOREGROUND_COLOR = Color.white; private static final Color OPEN_TILE_BACKGROUND_COLOR = Color.white; private static final Color OPEN_TILE_FOREGROUND_COLOR = Color.black; private static final Color FLAGGED_TILE_BACKGROUND_COLOR = Color.blue; private static final Color FLAGGED_TILE_FOREGROUND_COLOR = Color.white; private static final Color MINE_TILE_BACKGROUND_COLOR = Color.red; private static final Color MINE_TILE_FOREGROUND_COLOR = Color.white; private static final Color GRID_COLOR = Color.gray; private static final int CLUE_OFFSET_X = 10; private static final int CLUE_OFFSET_Y = 20; private static final int COLUMN_LABEL_TILE_INDEX = -1; private static final int ROW_LABEL_TILE_INDEX = -1; // Set default values here private boolean debugEnabled = false; private int rowCount = 9; private boolean rowCountAlreadySet = false; private int columnCount = 9; private MineSweeper game; public static void main(String[] arguments) { PlayMinesGui instance = new PlayMinesGui(arguments); } /** * @param arguments Accepts command line arguments (Passed from the constructor). */ public PlayMinesGui(String[] arguments) { processCommandLineArguments(arguments); game = new MineSweeper(rowCount, columnCount); JFrame gameWindow = new JFrame(GAME_WINDOW_TITLE); gameWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); MineSweeperGui gui = new MineSweeperGui(); gameWindow.setContentPane(gui); gameWindow.setSize(INITIAL_WINDOW_WIDTH, INITIAL_WINDOW_HEIGHT); gameWindow.setLocation(INITIAL_WINDOW_X, INITIAL_WINDOW_Y); gameWindow.setVisible(true); // Game window takes control from here, until it is closed. } /** * Processes the command line arguments, setting member fields with the results. * @param arguments The command line arguments. */ private void processCommandLineArguments(String[] arguments) { int argumentCount = arguments.length; for (int currentArgumentIndex = 0; currentArgumentIndex < argumentCount; currentArgumentIndex ++) { String currentArgument = arguments[currentArgumentIndex]; if (currentArgument.equals(HELP_SHORT_ARGUMENT) || currentArgument.equals(HELP_LONG_ARGUMENT)) { printHelp(); System.exit(NORMAL_EXIT_CODE); } else if (currentArgument.equals(DEBUG_SHORT_ARGUMENT) || currentArgument.equals(DEBUG_SHORT_ARGUMENT)) { debugEnabled = true; } else { try { int dimensionSpecification; // If this throws an Exception (because the argument is // a malformed number), then control will jump right // to the catch block and skip setting the dimension. dimensionSpecification = Integer.parseInt(currentArgument); if (rowCountAlreadySet) { columnCount = dimensionSpecification; } else { rowCount = dimensionSpecification; rowCountAlreadySet = true; } } catch (Exception e) { // Do nothing } } } } /** * Prints command line usage information. */ private void printHelp() { //TODO System.out.println("PlayMinesGui - A graphical interface to MineSweeper."); System.out.println("Usage: java PlayMinesGui [options] rows columns"); System.out.println(" In short, this class has the same interface as PlayMines."); System.out.println(" -h, --help - Print this message."); System.out.println(" -d, --debug - Turn on debug."); } /** * The main graphics pane used for displaying the MineSweeper board. */ private class MineSweeperGui extends JComponent { // There will be this many grid spacings between the edge of the window // (on all sides) and the playing board. private static final int WINDOW_PADDING_WEIGHT = 1; private double boundarySpacingX, boundarySpacingY; public MineSweeperGui() { this.setOpaque(true); this.setDoubleBuffered(true); GuiMouseListener mouse = new GuiMouseListener(); this.addMouseListener(mouse); } /** * Our custom paintComponent(). * Called automatically each time the window needs to be repainted. */ public void paintComponent(Graphics surface) { calculateBoundarySpacing(); paintBackground(surface); paintTiles(surface); paintTileIndexLabels(surface); paintGrid(surface); } /** * Calculates the double precision horizontal (x) and vertical (y) spacing * between tile boundaries. */ private void calculateBoundarySpacing() { boundarySpacingX = (((double)this.getWidth()) / ((double)(columnCount + (2 * WINDOW_PADDING_WEIGHT)))); boundarySpacingY = (((double)this.getHeight()) / ((double)(rowCount + (2 * WINDOW_PADDING_WEIGHT)))); } /** * Calculates the x coordinate of the tile boundary (AKA grid line) with * the given number/index. * @param boundaryNumber Generally meant to be a number from 0 to the number * of columns, but will work with all values. * In general, the left boundary of a tile has the same number/index as that tile. * @return The x coordinate of the specified boundary. */ private int calculateBoundaryX(int boundaryNumber) { return ((int) (((double)(boundaryNumber + WINDOW_PADDING_WEIGHT)) * boundarySpacingX)); } /** * Calculates the y coordinate of the tile boundary (AKA grid line) with * the given number/index. * @param boundaryNumber Generally meant to be a number from 0 to the number * of rows, but will work with all values. * In general, the top boundary of a tile has the same number/index as that tile. * @return The y coordinate of the specified boundary. */ private int calculateBoundaryY(int boundaryNumber) { return ((int) (((double)(boundaryNumber + WINDOW_PADDING_WEIGHT)) * boundarySpacingY)); } /** * Calculates the lesser x coordinate for the tile with the given number/index. * Useful for determining the start coordinate for drawing the tile rectangle. * @param tileNumber Generally meant to be a horizontal tile index, but * will work with all values. * @return The lesser x coordinate for the specified tile. */ private int calculateTileMinX(int tileNumber) { return (calculateBoundaryX(tileNumber) + 1); } /** * Calculates the lesser y coordinate for the tile with the given number/index. * Useful for determining the start coordinate for drawing the tile rectangle. * @param tileNumber Generally meant to be a vertical tile index, but * will work with all values. * @return The lesser y coordinate for the specified tile. */ private int calculateTileMinY(int tileNumber) { return (calculateBoundaryY(tileNumber) + 1); } /** * Calculates the width of the tile with the given number/index. * Useful when drawing the tile rectangle. * @param tileNumber Will work with all values. * @return The width of the specified tile. */ private int calculateTileWidth(int tileNumber) { return (calculateBoundaryX(tileNumber + 1) - calculateTileMinX(tileNumber)); } /** * Calculates the height of the tile with the given number/index. * Useful when drawing the tile rectangle. * @param tileNumber Will work with all values. * @return The height of the specified tile. */ private int calculateTileHeight(int tileNumber) { return (calculateBoundaryY(tileNumber + 1) - calculateTileMinY(tileNumber)); } /** * Indicates tile column index that contains the given x coordinate. * @param x The raw x coordinate to be translated. * @return The horizontal tile index. */ private int mapXCoordinateToTileColumn(int x) { return (((int)(((double)x) / boundarySpacingX)) - 1); } /** * Indicates tile row index that contains the given y coordinate. * @param x The raw y coordinate to be translated. * @return The vertical tile index. */ private int mapYCoordinateToTileRow(int y) { return (((int)(((double)y) / boundarySpacingY)) - 1); } /** * Paints the background of the graphics pane. */ private void paintBackground(Graphics surface) { surface.setColor(Color.white); surface.fillRect(0, 0, this.getWidth(), this.getHeight()); } /** * Paints the tiles, including debug information (if enabled), of the game board. */ private void paintTiles(Graphics surface) { for (int currentTileRow = 0; currentTileRow < rowCount; currentTileRow ++) { for (int currentTileColumn = 0; currentTileColumn < columnCount; currentTileColumn ++) { int currentTileMinX = calculateTileMinX(currentTileColumn); int currentTileWidth = calculateTileWidth(currentTileColumn); int currentTileMinY = calculateTileMinY(currentTileRow); int currentTileHeight = calculateTileHeight(currentTileRow); char tileType = game.getBoard(currentTileRow, currentTileColumn); // It might have been possible to rearrange the following block // (and even save some lines in the process). I opted, instead, // for this organization because it allows for future flexibility // and is more clear, I think. // The text drawing code (For drawing the clues) could use some // improvement; Perhaps some font size information and variable // font sizes could be used? if (tileType == CLOSED_TILE) { surface.setColor(CLOSED_TILE_BACKGROUND_COLOR); surface.fillRect(currentTileMinX, currentTileMinY, currentTileWidth, currentTileHeight); if (debugEnabled) { String tileClue = Integer.toString( game.getMines(currentTileRow, currentTileColumn)); surface.setColor(CLOSED_TILE_FOREGROUND_COLOR); surface.drawString(tileClue, currentTileMinX + CLUE_OFFSET_X, currentTileMinY + CLUE_OFFSET_Y); } } else if (tileType == BLANK_TILE) { surface.setColor(Color.white); surface.fillRect(currentTileMinX, currentTileMinY, currentTileWidth, currentTileHeight); } else if (tileType == FLAGGED_TILE) { surface.setColor(FLAGGED_TILE_BACKGROUND_COLOR); surface.fillRect(currentTileMinX, currentTileMinY, currentTileWidth, currentTileHeight); if (debugEnabled) { String tileClue = Integer.toString( game.getMines(currentTileRow, currentTileColumn)); surface.setColor(FLAGGED_TILE_FOREGROUND_COLOR); surface.drawString(tileClue, currentTileMinX + CLUE_OFFSET_X, currentTileMinY + CLUE_OFFSET_Y); } } else if (tileType == MINE_TILE) { surface.setColor(MINE_TILE_BACKGROUND_COLOR); surface.fillRect(currentTileMinX, currentTileMinY, currentTileWidth, currentTileHeight); if (debugEnabled) { String tileClue = Integer.toString( game.getMines(currentTileRow, currentTileColumn)); surface.setColor(MINE_TILE_FOREGROUND_COLOR); surface.drawString(tileClue, currentTileMinX + CLUE_OFFSET_X, currentTileMinY + CLUE_OFFSET_Y); } } // For drawing the clues (1-8) in open tiles. else { String tileClue = new String(new char[] {tileType}); surface.setColor(OPEN_TILE_FOREGROUND_COLOR); surface.drawString(tileClue, currentTileMinX + CLUE_OFFSET_X, currentTileMinY + CLUE_OFFSET_Y); } } } } /** * Paints tile row and column indices. */ private void paintTileIndexLabels(Graphics surface) { int columnLabelMinY = calculateTileMinY(COLUMN_LABEL_TILE_INDEX); for (int currentColumnIndex = 0; currentColumnIndex < columnCount; currentColumnIndex ++) { int currentColumnLabelMinX = calculateTileMinX(currentColumnIndex); String currentColumnIndexLabel = Integer.toString(currentColumnIndex); surface.setColor(OPEN_TILE_FOREGROUND_COLOR); surface.drawString(currentColumnIndexLabel, currentColumnLabelMinX + CLUE_OFFSET_X, columnLabelMinY + CLUE_OFFSET_Y); } int rowLabelMinX = calculateTileMinX(ROW_LABEL_TILE_INDEX); for (int currentRowIndex = 0; currentRowIndex < rowCount; currentRowIndex ++) { int currentRowLabelMinY = calculateTileMinY(currentRowIndex); String currentRowIndexLabel = Integer.toString(currentRowIndex); surface.setColor(OPEN_TILE_FOREGROUND_COLOR); surface.drawString(currentRowIndexLabel, rowLabelMinX + CLUE_OFFSET_X, currentRowLabelMinY + CLUE_OFFSET_Y); } } /** * Paints the gameboard grid. */ private void paintGrid(Graphics surface) { int minBoundaryX = calculateBoundaryX(0); int maxBoundaryX = calculateBoundaryX(columnCount); int minBoundaryY = calculateBoundaryY(0); int maxBoundaryY = calculateBoundaryY(rowCount); for (int currentXBoundaryNumber = 0; currentXBoundaryNumber <= columnCount; currentXBoundaryNumber ++) { int currentBoundaryX = calculateBoundaryX(currentXBoundaryNumber); surface.setColor(GRID_COLOR); surface.drawLine(currentBoundaryX, minBoundaryY, currentBoundaryX, maxBoundaryY); } for (int currentYBoundaryNumber = 0; currentYBoundaryNumber <= columnCount; currentYBoundaryNumber ++) { int currentBoundaryY = calculateBoundaryY(currentYBoundaryNumber); surface.setColor(GRID_COLOR); surface.drawLine(minBoundaryX, currentBoundaryY, maxBoundaryX, currentBoundaryY); } } /** * Our custom mouse handler. */ private class GuiMouseListener implements MouseListener { public void mouseClicked(MouseEvent event) { processMouseInput(event); } public void mousePressed(MouseEvent inputEvent) { } public void mouseReleased(MouseEvent inputEvent) { } public void mouseEntered(MouseEvent inputEvent) { } public void mouseExited(MouseEvent inputEvent) { } } /* * Deals with mouse input. For now, it only handles mouse clicks. * @param event The MouseEvent generated on the mouse action. */ private void processMouseInput(MouseEvent event) { if (game.gameRunning()) { int x = event.getX(); int y = event.getY(); int button = event.getButton(); int tileRow = mapYCoordinateToTileRow(y); int tileColumn = mapXCoordinateToTileColumn(x); if (button == MouseEvent.BUTTON1) { game.markTile(tileRow, tileColumn, COMMAND_OPEN); } else if (button == MouseEvent.BUTTON3) { if (event.isControlDown()) { game.markTile(tileRow, tileColumn, COMMAND_UNFLAG); } else { game.markTile(tileRow, tileColumn, COMMAND_FLAG); } } this.repaint(); // This chunk stolen from PlayMines.java (Written by Sourav) if (debugEnabled) { System.out.println("Tiles array:"); System.out.println(game.toStringTiles()); System.out.println("Mines array:"); System.out.println(game.toStringMines()); } System.out.println(game.toStringBoard()); } else { if (game.didWin()) { System.out.println("You Won! Congratulations!"); } else { System.out.println("Sorry, better luck next time"); } // End of plagiarism } } } }