import java.awt.*; import java.awt.event.*; import java.awt.image.*; import java.io.*; import javax.imageio.*; import javax.swing.*; // Canvas class, which draws the board and also acts as a central location of // game state that tiles and the face can refer back to public class Canvas extends JComponent { // Width of the tileset public static final int TS_WIDTH = 144; // Height of the tileset public static final int TS_HEIGHT = 122; // Y-position of the digits in the tileset public static final int TS_DIGITS_Y = 33; // Y-position of the faces in the tilest public static final int TS_FACES_Y = 55; // Y-position of the miscellaneous stuff in the tileset public static final int TS_MISC_Y = 82; // X-position of the digit backdrop in the tileset public static final int TS_DIGIT_BACKDROP_X = 28; // X-position of the color pixel in the tileset public static final int TS_COLOR_X = 70; // Width of the borders public static final int BORDER_WIDTH = 12; // Height of most of the borders public static final int BORDER_HEIGHT = 11; // Height of the bottom border public static final int BOTTOM_HEIGHT = 12; // Height of the box at the top, without the borders public static final int TOP_BOX_HEIGHT = 33; // The distance of the top items from the top of the screen public static final int TOP_PADDING = 15; // The distance of the flags display from the left of the screen public static final int FLAGS_PADDING = 16; // The distance of the left side of the timer display from the right of the // screen public static final int TIMER_PADDING = 59; // The important game attributes as final ints public final int ROWS; public final int COLS; public final int MINES; public final int WIDTH; public final int HEIGHT; // JPanel containing all the tiles private final JPanel BOARD_PANEL; // Number displays for the flags and the time private final NumberDisplay FLAGS_DISPLAY; private final NumberDisplay TIMER_DISPLAY; // The face private final Face FACE; // A KeyListener listening for shift to help chord private final KeyListener SHIFT_KEY_LISTENER; // Timer to tick up the time every second private final Timer TIMER; // The virtual representation of the board if we want to actually reference // the tiles instead of casting BOARD_PANEL.getComponents() becuase that's // weird private final Tile[][] BOARD; // The image to draw in the background private Image backgroundImage; // The tile that the mouse is currently over private Tile currentTile; // Whether the game has started yet (before the first click, no mines exist // and the timer doesn't start) private boolean gameStarted; // Whether the game has ended private boolean gameEnded; // Whether the game was lost private boolean gameLost; // The number of tiles left to reveal, to know when to win private int numTilesLeft; // The number of flags left to place, for the digit display private int numFlagsLeft; // The time, also for the digit display private int time; // Input state; not used direclty, but getters for these exist in Tile, and // this is where we can store the data for the entire game private boolean holdingShift; private boolean leftMouseDown; private boolean rightMouseDown; // Set initial values of all the variables private void init() { currentTile = null; gameStarted = false; gameEnded = false; gameLost = false; numTilesLeft = ROWS * COLS - MINES; numFlagsLeft = MINES; time = 0; holdingShift = false; leftMouseDown = false; rightMouseDown = false; addKeyListener(SHIFT_KEY_LISTENER); FLAGS_DISPLAY.setNum(numFlagsLeft); TIMER_DISPLAY.setNum(time); } // Get the tileset from Options and use it to set the images of all the // components inside the canvas and the background image private void setImages() { // Read the tileset image BufferedImage tileset; try { tileset = ImageIO.read(new File(Options.SKINS_DIR, Options.getSkinName())); // Reject images that are too small to properly form the tileset if (tileset.getWidth() < TS_WIDTH || tileset.getHeight() < TS_HEIGHT) throw new IOException(); } catch (IOException e) { // Just use an empty tileset if it didn't work out tileset = new BufferedImage(TS_WIDTH, TS_HEIGHT, BufferedImage.TYPE_INT_RGB); } // Set the images of the tiles Image[] numberTiles = new Image[9]; Image[] specialTiles = new Image[Tile.EXPLODED_MINE_INDEX + 1]; for (int i = 0; i < 9; i++) numberTiles[i] = getTileFromSet(tileset, i, 0); for (int i = 0; i <= Tile.EXPLODED_MINE_INDEX; i++) specialTiles[i] = getTileFromSet(tileset, i, Tile.SIZE); for (Tile[] row : BOARD) for (Tile tile : row) tile.setImages(numberTiles, specialTiles); // Set the images of the the number displays Image[] digits = getImageSet(tileset, NumberDisplay.MINUS_INDEX + 1, TS_DIGITS_Y, NumberDisplay.DIGIT_WIDTH, NumberDisplay.DIGIT_HEIGHT); Image numberDisplayBackdrop = tileset.getSubimage(TS_DIGIT_BACKDROP_X, TS_MISC_Y, NumberDisplay.BACKDROP_WIDTH, NumberDisplay.BACKDROP_HEIGHT); FLAGS_DISPLAY.setImages(digits, numberDisplayBackdrop); TIMER_DISPLAY.setImages(digits, numberDisplayBackdrop); // Set the images of the face FACE.setImages(getImageSet(tileset, Face.PRESSED_INDEX + 1, TS_FACES_Y, Face.SIZE, Face.SIZE)); // Set the background image backgroundImage = getBackgroundImage(tileset); } public Canvas() { // Set game attributes based on the difficulty Difficulty difficulty = Options.getDifficulty(); ROWS = difficulty.getRows(); COLS = difficulty.getCols(); MINES = difficulty.getMines(); WIDTH = Tile.SIZE * COLS + 2 * BORDER_WIDTH; HEIGHT = Tile.SIZE * ROWS + 2 * BORDER_HEIGHT + BOTTOM_HEIGHT + TOP_BOX_HEIGHT; // Initialize the tiles BOARD = new Tile[ROWS][COLS]; BOARD_PANEL = new JPanel(new GridLayout(ROWS, COLS)); for (int r = 0; r < ROWS; r++) { for (int c = 0; c < COLS; c++) { BOARD[r][c] = new Tile(this); BOARD_PANEL.add(BOARD[r][c]); } } // Before doing anything else, we want each tile to know what tiles are // adjacent to them for (int r = 0; r < ROWS; r++) { for (int c = 0; c < COLS; c++) { // Liberal use of min/max to ensure we don't go out of bounds int rStart = Math.max(r - 1, 0); int rEnd = Math.min(r + 2, ROWS); int cStart = Math.max(c - 1, 0); int cEnd = Math.min(c + 2, COLS); for (int rAdj = rStart; rAdj < rEnd; rAdj++) for (int cAdj = cStart; cAdj < cEnd; cAdj++) if (rAdj != r || cAdj != c) // Ignore yourself BOARD[r][c].addAdjacentTile(BOARD[rAdj][cAdj]); } } // Initialize the other components FLAGS_DISPLAY = new NumberDisplay(); TIMER_DISPLAY = new NumberDisplay(); FACE = new Face(this); // Now that the components are all initialized, set all of their images setImages(); // Shift+LMB chords, so we need some way of determining if the shift key // is being held. SHIFT_KEY_LISTENER = new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { updateShift(e, true); } @Override public void keyReleased(KeyEvent e) { updateShift(e, false); } // Helper method to update holdingShift to the new value if shift // was pressed, and additionally update the pressed state of the // current tile if that exists private void updateShift(KeyEvent e, boolean newValue) { if (e.getKeyCode() == KeyEvent.VK_SHIFT) { holdingShift = newValue; if (currentTile != null) currentTile.updatePressedState(); } } }; // 1 second delay TIMER = new Timer(1000, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // Can't display past 999 anyways if (time == 999) { TIMER.stop(); return; } // Play the tick sound and increment the timer number Sound.TICK.play(); TIMER_DISPLAY.setNum(++time); } }); // Null layout, for fine-tuned placement of all game components setLayout(null); Insets insets = getInsets(); placeComponent(BOARD_PANEL, BORDER_WIDTH, 2 * BORDER_HEIGHT + TOP_BOX_HEIGHT, insets); placeComponent(FLAGS_DISPLAY, FLAGS_PADDING, TOP_PADDING, insets); placeComponent(TIMER_DISPLAY, WIDTH - TIMER_PADDING, TOP_PADDING, insets); placeComponent(FACE, (WIDTH - Face.SIZE) / 2, TOP_PADDING, insets); setPreferredSize(new Dimension(WIDTH, HEIGHT)); setFocusable(true); init(); } // Refresh the skin and repaint public void newSkin() { setImages(); repaint(); } // Stop everything that might be running public void stop() { TIMER.stop(); removeKeyListener(SHIFT_KEY_LISTENER); } // Restart the game public void restart() { if (!gameStarted) return; stop(); init(); for (Tile[] row : BOARD) for (Tile tile : row) tile.restart(); BOARD_PANEL.repaint(); } // Start the game if it's not already started public void tryStartGame(Tile startTile) { if (gameStarted) return; gameStarted = true; // Place all the mines after the the first tile was selected, in such a // way that the first selected tile must be blank for (int mineCount = 0; mineCount < MINES;) { int row = (int) (Math.random() * ROWS); int col = (int) (Math.random() * COLS); if (BOARD[row][col].makeMine(startTile)) mineCount++; } TIMER.start(); } // After every tile revealed, check for game ending public void postRevealCheck() { if (gameLost) { // Loss Sound.EXPLODE.play(); endGame(); FACE.setFace(Face.DEAD_INDEX); } else if (numTilesLeft == 0) { // Win Sound.WIN.play(); // Automatically flag all remaining tiles on win FLAGS_DISPLAY.setNum(0); endGame(); FACE.setFace(Face.COOL_INDEX); } } // Modify the flag counter public void modifyFlagCount(boolean addedFlag) { if (addedFlag) numFlagsLeft--; else numFlagsLeft++; FLAGS_DISPLAY.setNum(numFlagsLeft); } // Called by tile every single time one is revealed public void revealedSingleTile() { numTilesLeft--; } // Called by tile if it is a mine when revealed public void setLoseFlag() { gameLost = true; } // Every tile has a MouseListener, but the canvas must be aware of the mouse // button state because other tiles that weren't the one that was initially // clicked also need to know the mouse button state, so Canvas has a setter // for it public void setLeftMouseDown(boolean b) { if (gameEnded) return; // Face gets nervous when clicking leftMouseDown = b; if (leftMouseDown) FACE.setFace(Face.SHOCKED_INDEX); else FACE.setFace(Face.HAPPY_INDEX); } // Ditto public void setRightMouseDown(boolean b) { if (!gameEnded) rightMouseDown = b; } public boolean isLeftMouseDown() { return leftMouseDown; } // Chording can be done with either the right mouse button or the shift key public boolean isChording() { return holdingShift || rightMouseDown; } // Tiles need to know what tile the mouse is currently over because the // MouseReleased event is triggered on the tile that the mouse was pressed // down on, not the one it was released on, but the tile that it was // released on is the one that actually gets revealed, which we would // otherwise have no way of knowing public Tile getCurrentTile() { return currentTile; } public void setCurrentTile(Tile tile) { currentTile = tile; } public boolean isGameLost() { return gameLost; } // Get the nth tile from the tileset, with the initial y-value of y private Image getTileFromSet(BufferedImage tileset, int n, int y) { return tileset.getSubimage(Tile.SIZE * n, y, Tile.SIZE, Tile.SIZE); } // For the other images, which have a 1-pixel gap between them: return an // array of images by slicing subimages of the given size from the image, // with a 1-pixel gap between them private Image[] getImageSet(BufferedImage tileset, int n, int y, int width, int height) { Image[] img = new Image[n]; int x = 0; for (int i = 0; i < n; i++) { img[i] = tileset.getSubimage(x, y, width, height); x += width + 1; } return img; } // Create the background image, borders and all private BufferedImage getBackgroundImage(BufferedImage tileset) { // Get all the images out of the tileset, going row-by-row, hence tsY // marking the y-index of the current row int tsY = TS_MISC_Y; Image topLeftCorner = tileset.getSubimage(0, tsY, BORDER_WIDTH, BORDER_HEIGHT); Image topBorder = tileset.getSubimage(BORDER_WIDTH + 1, tsY, 1, BORDER_HEIGHT); Image topRightCorner = tileset.getSubimage(BORDER_WIDTH + 3, tsY, BORDER_WIDTH, BORDER_HEIGHT); tsY += BORDER_HEIGHT + 1; Image topLeftBorder = tileset.getSubimage(0, tsY, BORDER_WIDTH, 1); Image topRightBorder = tileset.getSubimage(BORDER_WIDTH + 3, tsY, BORDER_WIDTH, 1); tsY += 2; Image middleLeftCorner = tileset.getSubimage(0, tsY, BORDER_WIDTH, BORDER_HEIGHT); Image middleBorder = tileset.getSubimage(BORDER_WIDTH + 1, tsY, 1, BORDER_HEIGHT); Image middleRightCorner = tileset.getSubimage(BORDER_WIDTH + 3, tsY, BORDER_WIDTH, BORDER_HEIGHT); tsY += BORDER_HEIGHT + 1; Image bottomLeftBorder = tileset.getSubimage(0, tsY, BORDER_WIDTH, 1); Image bottomRightBorder = tileset.getSubimage(BORDER_WIDTH + 3, tsY, BORDER_WIDTH, 1); tsY += 2; Image bottomLeftCorner = tileset.getSubimage(0, tsY, BORDER_WIDTH, BOTTOM_HEIGHT); Image bottomBorder = tileset.getSubimage(BORDER_WIDTH + 1, tsY, 1, BOTTOM_HEIGHT); Image bottomRightCorner = tileset.getSubimage(BORDER_WIDTH + 3, tsY, BORDER_WIDTH, BOTTOM_HEIGHT); BufferedImage img = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); Graphics2D g = (Graphics2D) img.getGraphics(); int rightBorderX = WIDTH - BORDER_WIDTH; int middleBorderY = BORDER_HEIGHT + TOP_BOX_HEIGHT; int bottomBorderY = HEIGHT - BOTTOM_HEIGHT; // Draw all the corners g.drawImage(topLeftCorner, 0, 0, this); g.drawImage(topRightCorner, rightBorderX, 0, this); g.drawImage(middleLeftCorner, 0, middleBorderY, this); g.drawImage(middleRightCorner, rightBorderX, middleBorderY, this); g.drawImage(bottomLeftCorner, 0, bottomBorderY, this); g.drawImage(bottomRightCorner, rightBorderX, bottomBorderY, this); // Draw all the horizontal borders for (int x = BORDER_WIDTH; x < rightBorderX; x++) { g.drawImage(topBorder, x, 0, this); g.drawImage(middleBorder, x, middleBorderY, this); g.drawImage(bottomBorder, x, bottomBorderY, this); } // Draw the top set of vertical borders for (int y = BORDER_HEIGHT; y < middleBorderY; y++) { g.drawImage(topLeftBorder, 0, y, this); g.drawImage(topRightBorder, rightBorderX, y, this); } // Draw the bottom set of vertical borders for (int y = middleBorderY + BORDER_HEIGHT; y < bottomBorderY; y++) { g.drawImage(bottomLeftBorder, 0, y, this); g.drawImage(bottomRightBorder, rightBorderX, y, this); } // Get the background color out of the tileset and fill the box with it g.setColor(new Color(tileset.getRGB(TS_COLOR_X, TS_MISC_Y))); g.fillRect(BORDER_WIDTH, BORDER_HEIGHT, WIDTH - 2 * BORDER_WIDTH, TOP_BOX_HEIGHT); g.dispose(); return img; } // Helper for placing components in the null layout at specific coordinates private void placeComponent(JComponent c, int x, int y, Insets insets) { add(c); c.setLocation(x + insets.left, y + insets.top); c.setSize(c.getPreferredSize()); } // Stop the moving parts after a win or a loss private void endGame() { TIMER.stop(); gameEnded = true; for (Tile[] row : BOARD) for (Tile tile : row) tile.gameEnd(); } // Override the paintComponent method in order to add the background image @Override public void paintComponent(Graphics gr) { super.paintComponent(gr); Graphics2D g = (Graphics2D) gr; g.drawImage(backgroundImage, 0, 0, this); } }