package dev.kske.minesweeper; import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.time.Duration; import java.time.Instant; import java.util.*; import java.util.List; import javax.swing.JPanel; /** * Project: Minesweeper
* File: Board.java
* Created: 22.03.2019
* Author: Kai S. K. Engelbart */ public class Board extends JPanel { private static final long serialVersionUID = -279269871397851420L; private static final int tileSize = 32; private static Map icons; private int boardWidth, boardHeight; private GameState gameState; private int mines, activeTiles, flaggedTiles; private Tile[][] board; private BoardConfig boardConfig; private boolean minesPlaced; private Instant start, finish; private List listeners; static { icons = new HashMap<>(); for (String name : new String[] { "mine2", "mine4", "tile", "tile3" }) { icons.put(name, TextureLoader.loadScaledImage(name, tileSize)); } } /** * Creates an instance of {@link Board}. */ public Board() { // Not using a layout manager super(null); listeners = new ArrayList<>(); addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent evt) { int n = evt.getX() / tileSize, m = evt.getY() / tileSize; Tile tile = board[n][m]; if (tile.isTouched() || gameState != GameState.ACTIVE) return; switch (evt.getButton()) { case MouseEvent.BUTTON1: touchTile(n, m); break; case MouseEvent.BUTTON3: flagTile(n, m); } } }); } /** * Initializes the board with a given configuration. This does not include mine placement. * * @param config the configuration used */ public void init(BoardConfig config) { boardConfig = config; boardWidth = config.width; boardHeight = config.height; setPreferredSize(new Dimension(config.width * tileSize, config.height * tileSize)); gameState = GameState.ACTIVE; mines = config.mines; activeTiles = boardWidth * boardHeight; flaggedTiles = 0; minesPlaced = false; notifyFlaggedTilesEvent(new FlaggedTilesEvent(this, flaggedTiles)); // Initialize board board = new Tile[boardWidth][boardHeight]; for (int i = 0; i < boardWidth; i++) for (int j = 0; j < boardHeight; j++) board[i][j] = new Tile(); repaint(); revalidate(); start = Instant.now(); } /** * Re-initializes the board with the cached configuration, thereby resetting it. */ public void reset() { init(boardConfig); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); for (int i = 0; i < boardWidth; i++) for (int j = 0; j < boardHeight; j++) { Tile tile = board[i][j]; int x = i * tileSize, y = j * tileSize; // Draw background g.setColor(Color.gray); g.fillRect(x, y, x + tileSize, y + tileSize); // Draw tile with normal mine if (gameState == GameState.LOST && tile.isMine()) g.drawImage(icons.get("mine2"), x, y, this); // Draw tile with diffused mine else if (gameState == GameState.WON && tile.isMine()) g.drawImage(icons.get("mine4"), x, y, this); else if (tile.isTouched()) { // Draw tile with mine if (tile.isMine()) g.drawImage(icons.get("mine2"), x, y, this); // Draw flagged tile else if ( tile.isDrawSurroundingMines() && tile.getSurroundingMines() > 0 ) { // Draw number of surrounding mines String numStr = String.valueOf(tile.getSurroundingMines()); g.setFont(new Font("Arial", Font.BOLD, 18)); g.setColor(Color.red); FontMetrics fm = g.getFontMetrics(); int w = fm.stringWidth(numStr), h = fm.getHeight(); g.drawString( numStr, x + (tileSize - w) / 2, y + (tileSize - h) / 2 + fm.getAscent() ); } } // Draw flagged tile else if (tile.isFlagged()) g.drawImage(icons.get("tile3"), x, y, this); // Draw normal tile else g.drawImage(icons.get("tile"), x, y, this); // Draw grid ((Graphics2D) g).setStroke(new BasicStroke(2.0f)); g.setColor(Color.black); g.drawRect(x, y, x + tileSize, y + tileSize); } } /** * Registers a game listener that is notified when game events occur. * * @param listener the game listener to register */ public void registerGameListener(GameListener listener) { listeners.add(listener); } private void notifyGameStateEvent(GameOverEvent evt) { listeners.forEach(listener -> listener.onGameOverEvent(evt)); } private void notifyFlaggedTilesEvent(FlaggedTilesEvent evt) { listeners.forEach(listener -> listener.onFlaggedTilesEvent(evt)); } private void repaintTile(int n, int m) { repaint(n * tileSize, m * tileSize, (n + 1) * tileSize, (n + 1) * tileSize); } private void initMines() { int remaining = mines; Random random = new Random(); while (remaining > 0) { // Randomly select a tile int n = random.nextInt(boardWidth); int m = random.nextInt(boardHeight); // Check if the selected tile already is a mine and is not touched if (!board[n][m].isTouched() && !board[n][m].isMine()) { // Decrement the counter remaining--; // Place the mine board[n][m].setMine(true); // Adjust surrounding mine counters for (int i = Math.max(0, n - 1); i < Math.min(n + 2, board.length); i++) for (int j = Math.max(0, m - 1); j < Math.min(m + 2, board[i].length); j++) board[i][j].setSurroundingMines(board[i][j].getSurroundingMines() + 1); } } minesPlaced = true; } private void touchTile(int n, int m) { Tile tile = board[n][m]; if (!tile.isTouched()) { tile.setTouched(true); activeTiles--; tile.setDrawSurroundingMines(true); // Adjust the number of flagged tiles if the tile was flagged if (tile.isFlagged()) { tile.setFlagged(false); flaggedTiles--; notifyFlaggedTilesEvent(new FlaggedTilesEvent(this, flaggedTiles)); } // Test if the game is won or lost if (tile.isMine()) { gameState = GameState.LOST; onGameOver(); } else if (mines == activeTiles) { gameState = GameState.WON; onGameOver(); } // Place the mines if this was the first touch if (!minesPlaced) initMines(); // Touch surrounding tiles when there are zero surrounding mines if (tile.getSurroundingMines() == 0) for (int i = Math.max(0, n - 1); i < Math.min(n + 2, board.length); i++) for (int j = Math.max(0, m - 1); j < Math.min(m + 2, board[i].length); j++) if (i != n || j != m) touchTile(i, j); repaintTile(n, m); } } private void flagTile(int n, int m) { Tile tile = board[n][m]; if (!tile.isTouched()) { if (tile.isFlagged()) { tile.setFlagged(false); flaggedTiles--; } else { tile.setFlagged(true); flaggedTiles++; } notifyFlaggedTilesEvent(new FlaggedTilesEvent(this, flaggedTiles)); repaintTile(n, m); } } private void onGameOver() { finish = Instant.now(); int duration = (int) Duration.between(start, finish).toMillis(); repaint(); GameOverEvent evt = new GameOverEvent(this, gameState, boardConfig, duration); notifyGameStateEvent(evt); } /** * @return the total number of mines */ public int getMines() { return mines; } /** * @return the number of active tiles */ public int getActiveTiles() { return activeTiles; } /** * @return the number of flagged files */ public int getFlaggedTiles() { return flaggedTiles; } /** * @return the current configuration */ public BoardConfig getBoardConfig() { return boardConfig; } }