minesweeper/src/dev/kske/minesweeper/Board.java

300 lines
7.9 KiB
Java

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: <strong>Minesweeper</strong><br>
* File: <strong>Board.java</strong><br>
* Created: <strong>22.03.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class Board extends JPanel {
private static final long serialVersionUID = -279269871397851420L;
private static final int tileSize = 32;
private static Map<String, Image> 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<GameListener> 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; }
}