package dev.kske.minesweeper; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; 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 Instant start, finish; private List listeners; static { icons = new HashMap<>(); final String[] names = { "mine2", "mine4", "tile", "tile3" }; for (String name : names) { icons.put(name, TextureLoader.loadScaledImage(name, tileSize)); } } 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); } } }); } 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; // 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(); initMines(); repaint(); revalidate(); start = Instant.now(); } 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); } } 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].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); } } } 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(); } // Touch surrounding tiles when there are zero surrounding mines else 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); } public int getMines() { return mines; } public int getActiveTiles() { return activeTiles; } public int getFlaggedTiles() { return flaggedTiles; } }