import java.awt.*; import java.util.HashSet; import java.awt.event.*; import javax.swing.*; public class Tile extends JComponent { public static final int SIZE = 16; public static final int REGULAR_INDEX = 0; public static final int PRESSED_INDEX = 1; public static final int MINE_INDEX = 2; public static final int FLAGGED_INDEX = 3; public static final int NOT_MINE_INDEX = 4; public static final int EXPLODED_MINE_INDEX = 5; private final Canvas GAME_CANVAS; private final Image[] NUMBER_TILES; private final Image[] SPECIAL_TILES; // Store the mouse listener for convenient removal once the game ends private final MouseListener TILE_MOUSE_LISTENER; // A set of all the Tiles that are adjacent to this one, for calculating // numbers and autorevealing zeros and chording private final HashSet ADJACENT_TILES; // Whether or not the tile is a mine private boolean mine; // The number of mines adjacent to the current one private int num; // Whether or not this mine has been revealed already private boolean revealed; // Whether or not this mine has been flagged private boolean flagged; private boolean pressed; private boolean mouseOver; private boolean gameEnded; private void init() { mine = false; num = 0; revealed = false; flagged = false; pressed = false; mouseOver = false; gameEnded = false; addMouseListener(TILE_MOUSE_LISTENER); } public Tile(Canvas gameCanvas, Image[] numberTiles, Image[] specialTiles) { GAME_CANVAS = gameCanvas; NUMBER_TILES = numberTiles; SPECIAL_TILES = specialTiles; ADJACENT_TILES = new HashSet(); TILE_MOUSE_LISTENER = new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { boolean rightClicked = false; switch (e.getButton()) { case MouseEvent.BUTTON1: GAME_CANVAS.setLeftMouseDown(true); break; case MouseEvent.BUTTON3: rightClicked = true; GAME_CANVAS.setRightMouseDown(true); break; default: return; } Tile tile = GAME_CANVAS.getCurrentTile(); if (tile == null) return; if (rightClicked) tile.rightClickAction(); tile.updatePressedState(); } @Override public void mouseReleased(MouseEvent e) { boolean leftClicked = false; switch (e.getButton()) { case MouseEvent.BUTTON1: leftClicked = true; GAME_CANVAS.setLeftMouseDown(false); break; case MouseEvent.BUTTON3: GAME_CANVAS.setRightMouseDown(false); break; default: return; } Tile tile = GAME_CANVAS.getCurrentTile(); if (tile == null) return; if (leftClicked) tile.leftClickAction(); tile.updatePressedState(); } @Override public void mouseEntered(MouseEvent e) { GAME_CANVAS.setCurrentTile(Tile.this); mouseOver = true; updatePressedState(); } @Override public void mouseExited(MouseEvent e) { if (GAME_CANVAS.getCurrentTile() == Tile.this) GAME_CANVAS.setCurrentTile(null); mouseOver = false; updatePressedState(); } }; setPreferredSize(new Dimension(SIZE, SIZE)); init(); } public void restart() { removeMouseListener(TILE_MOUSE_LISTENER); init(); } public void addAdjacentTile(Tile tile) { ADJACENT_TILES.add(tile); } // At the start of the game, place a mine at this tile, unless it is // adjacent to or is the starting tile, or is already a mine. Returns // whether or not a mine was actually placed. public boolean makeMine(Tile startTile) { if (mine || this == startTile || Options.isProtectedStart() && ADJACENT_TILES.contains(startTile)) return false; mine = true; // Increment the number on the adjacent tiles when a new mine is decided for (Tile tile : ADJACENT_TILES) tile.num++; return true; } public void gameEnd() { removeMouseListener(TILE_MOUSE_LISTENER); gameEnded = true; // Only repaint the tiles that actually change if (mine || flagged) repaint(); } public void updatePressedState() { if (pressed != (GAME_CANVAS.isLeftMouseDown() && mouseOver)) { pressed = !pressed; repaint(); } boolean pressAdjacent = pressed && GAME_CANVAS.isChording(); for (Tile tile : ADJACENT_TILES) { if (tile.pressed != pressAdjacent) { tile.pressed = pressAdjacent; tile.repaint(); } } } private void rightClickAction() { if (revealed) return; flagged = !flagged; GAME_CANVAS.modifyFlagCount(flagged); repaint(); } private void leftClickAction() { if (GAME_CANVAS.isChording()) { if (!revealed) return; int adjacentFlags = 0; for (Tile tile : ADJACENT_TILES) if (tile.flagged) adjacentFlags++; if (adjacentFlags != num) return; for (Tile tile : ADJACENT_TILES) if (!tile.flagged) tile.reveal(); } else { if (flagged || revealed) return; // Ensure that mines are placed before revealing anything GAME_CANVAS.tryStartGame(this); reveal(); } GAME_CANVAS.postRevealCheck(); } private void reveal() { if (revealed) return; revealed = true; repaint(); if (mine) { GAME_CANVAS.setLoseFlag(); } else { GAME_CANVAS.revealedSingleTile(); if (num == 0) for (Tile tile : ADJACENT_TILES) if (!tile.flagged) tile.reveal(); } } // Determine what image should be drawn at this tile private Image getImage() { if (gameEnded && mine) { // Automatically flag mines at wins, losses keep correct flags if (!GAME_CANVAS.isGameLost() || flagged) return SPECIAL_TILES[FLAGGED_INDEX]; if (revealed) return SPECIAL_TILES[EXPLODED_MINE_INDEX]; return SPECIAL_TILES[MINE_INDEX]; } if (revealed) return NUMBER_TILES[num]; if (flagged) { // Correctly flagged mines would have been caught earlier if (gameEnded) return SPECIAL_TILES[NOT_MINE_INDEX]; return SPECIAL_TILES[FLAGGED_INDEX]; } // Tiles are only interactable if the game is ongoing if (pressed && !gameEnded) return SPECIAL_TILES[PRESSED_INDEX]; return SPECIAL_TILES[REGULAR_INDEX]; } @Override public void paintComponent(Graphics gr) { super.paintComponent(gr); Graphics2D g = (Graphics2D) gr; g.drawImage(getImage(), 0, 0, this); } }