diff --git a/.classpath b/.classpath
new file mode 100644
index 0000000..07b86b3
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..48dd617
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,57 @@
+.metadata
+bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.settings/
+.loadpath
+.recommenders
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+*.launch
+
+# PyDev specific (Python IDE for Eclipse)
+*.pydevproject
+
+# CDT-specific (C/C++ Development Tooling)
+.cproject
+
+# CDT- autotools
+.autotools
+
+# Java annotation processor (APT)
+.factorypath
+
+# PDT-specific (PHP Development Tools)
+.buildpath
+
+# sbteclipse plugin
+.target
+
+# Tern plugin
+.tern-project
+
+# TeXlipse plugin
+.texlipse
+
+# STS (Spring Tool Suite)
+.springBeans
+
+# Code Recommenders
+.recommenders/
+
+# Annotation Processing
+.apt_generated/
+
+# Scala IDE specific (Scala & Java development for Eclipse)
+.cache-main
+.scala_dependencies
+.worksheet
+/scores.ser
+/scores old.ser
diff --git a/.project b/.project
new file mode 100644
index 0000000..137b252
--- /dev/null
+++ b/.project
@@ -0,0 +1,17 @@
+
+
+ Minesweeper
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/res/flag.png b/res/flag.png
new file mode 100644
index 0000000..70bde50
Binary files /dev/null and b/res/flag.png differ
diff --git a/res/mine.png b/res/mine.png
new file mode 100644
index 0000000..658d93b
Binary files /dev/null and b/res/mine.png differ
diff --git a/res/mine2.png b/res/mine2.png
new file mode 100644
index 0000000..47fe196
Binary files /dev/null and b/res/mine2.png differ
diff --git a/res/mine3.png b/res/mine3.png
new file mode 100644
index 0000000..b707ffd
Binary files /dev/null and b/res/mine3.png differ
diff --git a/res/mine4.png b/res/mine4.png
new file mode 100644
index 0000000..c529c82
Binary files /dev/null and b/res/mine4.png differ
diff --git a/res/smiley.png b/res/smiley.png
new file mode 100644
index 0000000..f3dac13
Binary files /dev/null and b/res/smiley.png differ
diff --git a/res/smiley1.png b/res/smiley1.png
new file mode 100644
index 0000000..40132d3
Binary files /dev/null and b/res/smiley1.png differ
diff --git a/res/smiley2.png b/res/smiley2.png
new file mode 100644
index 0000000..6487c0f
Binary files /dev/null and b/res/smiley2.png differ
diff --git a/res/smiley3.png b/res/smiley3.png
new file mode 100644
index 0000000..f736de4
Binary files /dev/null and b/res/smiley3.png differ
diff --git a/res/tile.png b/res/tile.png
new file mode 100644
index 0000000..d66d495
Binary files /dev/null and b/res/tile.png differ
diff --git a/res/tile2.png b/res/tile2.png
new file mode 100644
index 0000000..9e9ceb1
Binary files /dev/null and b/res/tile2.png differ
diff --git a/res/tile3.png b/res/tile3.png
new file mode 100644
index 0000000..706eb4d
Binary files /dev/null and b/res/tile3.png differ
diff --git a/res/tile4.png b/res/tile4.png
new file mode 100644
index 0000000..164a82a
Binary files /dev/null and b/res/tile4.png differ
diff --git a/src/dev/kske/minesweeper/Board.java b/src/dev/kske/minesweeper/Board.java
new file mode 100644
index 0000000..ec8ee9b
--- /dev/null
+++ b/src/dev/kske/minesweeper/Board.java
@@ -0,0 +1,258 @@
+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;
+
+ 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();
+ 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; }
+
+ public BoardConfig getBoardConfig() { return boardConfig; }
+}
diff --git a/src/dev/kske/minesweeper/BoardConfig.java b/src/dev/kske/minesweeper/BoardConfig.java
new file mode 100644
index 0000000..4779f5a
--- /dev/null
+++ b/src/dev/kske/minesweeper/BoardConfig.java
@@ -0,0 +1,25 @@
+package dev.kske.minesweeper;
+
+import java.io.Serializable;
+
+/**
+ * Project: Minesweeper
+ * File: BoardConfig.java
+ * Created: 01.04.2019
+ * Author: Kai S. K. Engelbart
+ */
+public class BoardConfig implements Serializable {
+
+ private static final long serialVersionUID = -6083006887427383946L;
+
+ public static final BoardConfig EASY = new BoardConfig(8, 8, 10), MEDIUM = new BoardConfig(16, 16, 40),
+ HARD = new BoardConfig(30, 16, 99);
+
+ public final int width, height, mines;
+
+ public BoardConfig(int width, int height, int mines) {
+ this.width = width;
+ this.height = height;
+ this.mines = mines;
+ }
+}
diff --git a/src/dev/kske/minesweeper/CustomDialog.java b/src/dev/kske/minesweeper/CustomDialog.java
new file mode 100644
index 0000000..69b3e27
--- /dev/null
+++ b/src/dev/kske/minesweeper/CustomDialog.java
@@ -0,0 +1,112 @@
+package dev.kske.minesweeper;
+
+import java.awt.BorderLayout;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.Frame;
+import java.awt.GridLayout;
+
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSlider;
+import javax.swing.border.EmptyBorder;
+
+/**
+ * Project: Minesweeper
+ * File: CustomDialog.java
+ * Created: 03.04.2019
+ * Author: Kai S. K. Engelbart
+ */
+public class CustomDialog extends JDialog {
+
+ private static final long serialVersionUID = -4019516811065781434L;
+ private final JPanel mcontentPanel = new JPanel();
+
+ private BoardConfig result;
+
+ /**
+ * Create the dialog.
+ */
+ public CustomDialog(Frame owner) {
+ super(owner, ModalityType.APPLICATION_MODAL);
+ setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+
+ setBounds(100, 100, 450, 300);
+ getContentPane().setLayout(new BorderLayout());
+ mcontentPanel.setBorder(new EmptyBorder(5, 5, 5, 5));
+ getContentPane().add(mcontentPanel, BorderLayout.CENTER);
+ mcontentPanel.setLayout(new GridLayout(0, 3, 0, 0));
+ {
+ JLabel lblBoardWidthText = new JLabel("Board Width:");
+ lblBoardWidthText.setFont(new Font("Tahoma", Font.PLAIN, 14));
+ mcontentPanel.add(lblBoardWidthText);
+ }
+ JLabel lblBoardWidth = new JLabel("");
+ lblBoardWidth.setFont(new Font("Tahoma", Font.PLAIN, 14));
+ mcontentPanel.add(lblBoardWidth);
+ JSlider sliderBoardWidth = new JSlider();
+ sliderBoardWidth.addChangeListener((evt) -> lblBoardWidth.setText(String.valueOf(sliderBoardWidth.getValue())));
+ sliderBoardWidth.setValue(16);
+ sliderBoardWidth.setMinimum(2);
+ sliderBoardWidth.setMaximum(30);
+ mcontentPanel.add(sliderBoardWidth);
+ {
+ JLabel lblBoardHeightText = new JLabel("Board Height:");
+ lblBoardHeightText.setFont(new Font("Tahoma", Font.PLAIN, 14));
+ mcontentPanel.add(lblBoardHeightText);
+ }
+ JLabel lblBoardHeight = new JLabel("");
+ lblBoardHeight.setFont(new Font("Tahoma", Font.PLAIN, 14));
+ mcontentPanel.add(lblBoardHeight);
+ JSlider sliderBoardHeight = new JSlider();
+ sliderBoardHeight
+ .addChangeListener((evt) -> lblBoardHeight.setText(String.valueOf(sliderBoardHeight.getValue())));
+ sliderBoardHeight.setValue(16);
+ sliderBoardHeight.setMaximum(30);
+ sliderBoardHeight.setMinimum(2);
+ mcontentPanel.add(sliderBoardHeight);
+ {
+ JLabel lblNumberOfMinesText = new JLabel("Number of Mines:");
+ lblNumberOfMinesText.setFont(new Font("Tahoma", Font.PLAIN, 14));
+ mcontentPanel.add(lblNumberOfMinesText);
+ }
+ JLabel lblNumMines = new JLabel("");
+ lblNumMines.setFont(new Font("Tahoma", Font.PLAIN, 14));
+ mcontentPanel.add(lblNumMines);
+ JSlider sliderNumMines = new JSlider();
+ sliderNumMines.addChangeListener((evt) -> lblNumMines.setText(String.valueOf(sliderNumMines.getValue())));
+ sliderNumMines.setValue(16);
+ sliderNumMines.setMinimum(2);
+ sliderNumMines.setMaximum(200);
+ mcontentPanel.add(sliderNumMines);
+ {
+ JPanel buttonPane = new JPanel();
+ buttonPane.setLayout(new FlowLayout(FlowLayout.RIGHT));
+ getContentPane().add(buttonPane, BorderLayout.SOUTH);
+ {
+ JButton okButton = new JButton("Start Game");
+ okButton.setActionCommand("OK");
+ okButton.addActionListener((evt) -> {
+ result = new BoardConfig(sliderBoardWidth.getValue(), sliderBoardHeight.getValue(),
+ sliderNumMines.getValue());
+ dispose();
+ });
+ buttonPane.add(okButton);
+ getRootPane().setDefaultButton(okButton);
+ }
+ {
+ JButton cancelButton = new JButton("Cancel");
+ cancelButton.setActionCommand("Cancel");
+ cancelButton.addActionListener((evt) -> dispose());
+ buttonPane.add(cancelButton);
+ }
+ }
+ }
+
+ public BoardConfig showDialog() {
+ setVisible(true);
+ return result;
+ }
+}
diff --git a/src/dev/kske/minesweeper/FlaggedTilesEvent.java b/src/dev/kske/minesweeper/FlaggedTilesEvent.java
new file mode 100644
index 0000000..ecc42fe
--- /dev/null
+++ b/src/dev/kske/minesweeper/FlaggedTilesEvent.java
@@ -0,0 +1,27 @@
+package dev.kske.minesweeper;
+
+import java.util.EventObject;
+
+/**
+ * Project: Minesweeper
+ * File: FlaggedTilesEvent.java
+ * Created: 03.04.2019
+ * Author: Kai S. K. Engelbart
+ */
+public class FlaggedTilesEvent extends EventObject {
+
+ private static final long serialVersionUID = -5809857531886339312L;
+
+ private final Board board;
+ private final int flagged;
+
+ public FlaggedTilesEvent(Object source, int flagged) {
+ super(source);
+ board = (Board) source;
+ this.flagged = flagged;
+ }
+
+ public Board getBoard() { return board; }
+
+ public int getFlagged() { return flagged; }
+}
diff --git a/src/dev/kske/minesweeper/GameListener.java b/src/dev/kske/minesweeper/GameListener.java
new file mode 100644
index 0000000..b18c464
--- /dev/null
+++ b/src/dev/kske/minesweeper/GameListener.java
@@ -0,0 +1,14 @@
+package dev.kske.minesweeper;
+
+/**
+ * Project: Minesweeper
+ * File: GameStateListener.java
+ * Created: 03.04.2019
+ * Author: Kai S. K. Engelbart
+ */
+public interface GameListener {
+
+ void onGameOverEvent(GameOverEvent evt);
+
+ void onFlaggedTilesEvent(FlaggedTilesEvent evt);
+}
diff --git a/src/dev/kske/minesweeper/GameOverEvent.java b/src/dev/kske/minesweeper/GameOverEvent.java
new file mode 100644
index 0000000..dc62e24
--- /dev/null
+++ b/src/dev/kske/minesweeper/GameOverEvent.java
@@ -0,0 +1,35 @@
+package dev.kske.minesweeper;
+
+import java.util.EventObject;
+
+/**
+ * Project: Minesweeper
+ * File: GameOverEvent.java
+ * Created: 03.04.2019
+ * Author: Kai S. K. Engelbart
+ */
+public class GameOverEvent extends EventObject {
+
+ private static final long serialVersionUID = -966111253980213845L;
+
+ private final Board board;
+ private final GameState gameState;
+ private final BoardConfig boardConfig;
+ private final int duration;
+
+ public GameOverEvent(Object source, GameState gameState, BoardConfig boardConfig, int duration) {
+ super(source);
+ board = (Board) source;
+ this.gameState = gameState;
+ this.boardConfig = boardConfig;
+ this.duration = duration;
+ }
+
+ public Board getBoard() { return board; }
+
+ public GameState getGameState() { return gameState; }
+
+ public BoardConfig getBoardConfig() { return boardConfig; }
+
+ public int getDuration() { return duration; }
+}
diff --git a/src/dev/kske/minesweeper/GameState.java b/src/dev/kske/minesweeper/GameState.java
new file mode 100644
index 0000000..81d667f
--- /dev/null
+++ b/src/dev/kske/minesweeper/GameState.java
@@ -0,0 +1,12 @@
+package dev.kske.minesweeper;
+
+/**
+ * Project: Minesweeper
+ * File: GameState.java
+ * Created: 22.03.2019
+ * Author: Kai S. K. Engelbart
+ */
+public enum GameState {
+
+ ACTIVE, WON, LOST;
+}
diff --git a/src/dev/kske/minesweeper/Minesweeper.java b/src/dev/kske/minesweeper/Minesweeper.java
new file mode 100644
index 0000000..cda6f17
--- /dev/null
+++ b/src/dev/kske/minesweeper/Minesweeper.java
@@ -0,0 +1,221 @@
+package dev.kske.minesweeper;
+
+import static dev.kske.minesweeper.BoardConfig.EASY;
+import static dev.kske.minesweeper.BoardConfig.HARD;
+import static dev.kske.minesweeper.BoardConfig.MEDIUM;
+
+import java.awt.BorderLayout;
+import java.awt.EventQueue;
+import java.awt.event.KeyEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.SwingConstants;
+import javax.swing.Timer;
+import javax.swing.UIManager;
+
+/**
+ * Project: Minesweeper
+ * File: Minesweeper.java
+ * Created: 21.03.2019
+ * Author: Kai S. K. Engelbart
+ */
+public class Minesweeper {
+
+ private static final String VERSION = "1.0";
+
+ private JFrame mframe;
+
+ private Board board;
+ private Timer timer;
+ private int gameTime;
+ private ScoreManager scoreManager;
+
+ /**
+ * Launch the application.
+ */
+ public static void main(String[] args) {
+ EventQueue.invokeLater(() -> {
+ try {
+ Minesweeper window = new Minesweeper();
+ window.mframe.setVisible(true);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+ }
+
+ /**
+ * Create the application.
+ *
+ * @wbp.parser.entryPoint
+ */
+ public Minesweeper() {
+ initialize();
+ }
+
+ /**
+ * Initialize the contents of the frame.
+ */
+ private void initialize() {
+ try {
+ UIManager.setLookAndFeel(UIManager.createLookAndFeel("Nimbus"));
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+
+ mframe = new JFrame();
+ mframe.addWindowListener(new WindowAdapter() {
+
+ @Override
+ public void windowClosing(WindowEvent e) {
+ scoreManager.saveScores();
+ }
+ });
+ mframe.setResizable(false);
+ mframe.setTitle("Minesweeper");
+ mframe.setBounds(100, 100, 359, 86);
+ mframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+
+ createMenuBar();
+
+ board = new Board();
+ board.init(EASY);
+ mframe.getContentPane().setLayout(new BorderLayout(0, 0));
+ mframe.getContentPane().add(board, BorderLayout.CENTER);
+
+ JPanel headerPanel = new JPanel();
+ mframe.getContentPane().add(headerPanel, BorderLayout.NORTH);
+ headerPanel.setLayout(new BorderLayout(0, 0));
+
+ JButton btnRestart = new JButton("Restart");
+ btnRestart.setHorizontalAlignment(SwingConstants.RIGHT);
+ headerPanel.add(btnRestart, BorderLayout.EAST);
+
+ JPanel panel = new JPanel();
+ headerPanel.add(panel, BorderLayout.WEST);
+ panel.setLayout(new BorderLayout(0, 0));
+
+ JLabel lblTime = new JLabel("Time:");
+ panel.add(lblTime, BorderLayout.NORTH);
+
+ timer = new Timer(1000, e -> lblTime.setText("Time: " + gameTime++ + "s"));
+ timer.setRepeats(true);
+ timer.setInitialDelay(0);
+ timer.setCoalesce(true);
+
+ JLabel lblRemainingMines = new JLabel("Remaining Mines: " + EASY.mines);
+ panel.add(lblRemainingMines, BorderLayout.SOUTH);
+ lblRemainingMines.setHorizontalAlignment(SwingConstants.LEFT);
+ btnRestart.addActionListener((evt) -> { board.reset(); gameTime = 0; timer.restart(); });
+ mframe.pack();
+
+ board.registerGameListener(new GameListener() {
+
+ @Override
+ public void onGameOverEvent(GameOverEvent evt) {
+ timer.stop();
+ switch (evt.getGameState()) {
+ case LOST:
+ JOptionPane.showMessageDialog(mframe, "Game lost!");
+ break;
+ case WON:
+ JOptionPane.showMessageDialog(mframe, "Game won!");
+ scoreManager.addScore(evt);
+ }
+ }
+
+ @Override
+ public void onFlaggedTilesEvent(FlaggedTilesEvent evt) {
+ lblRemainingMines.setText("Remaining Mines: " + (evt.getBoard().getMines() - evt.getFlagged()));
+ mframe.pack();
+ }
+ });
+
+ scoreManager = new ScoreManager();
+ scoreManager.loadScores();
+ timer.start();
+ }
+
+ private void createMenuBar() {
+ var menubar = new JMenuBar();
+
+ {
+ var gameMenu = new JMenu("Game");
+
+ var easyMenuItem = new JMenuItem("Easy");
+ var mediumMenuItem = new JMenuItem("Medium");
+ var hardMenuItem = new JMenuItem("Hard");
+ var customMenuItem = new JMenuItem("Custom");
+
+ gameMenu.setMnemonic(KeyEvent.VK_G);
+ easyMenuItem.setMnemonic(KeyEvent.VK_E);
+ mediumMenuItem.setMnemonic(KeyEvent.VK_M);
+ hardMenuItem.setMnemonic(KeyEvent.VK_H);
+ customMenuItem.setMnemonic(KeyEvent.VK_C);
+
+ easyMenuItem.addActionListener((evt) -> initGame(EASY));
+ mediumMenuItem.addActionListener((evt) -> initGame(MEDIUM));
+ hardMenuItem.addActionListener((evt) -> initGame(HARD));
+ customMenuItem.addActionListener((evt) -> {
+ BoardConfig cfg = new CustomDialog(mframe).showDialog();
+ if (cfg != null) initGame(cfg);
+ });
+
+ gameMenu.add(easyMenuItem);
+ gameMenu.add(mediumMenuItem);
+ gameMenu.add(hardMenuItem);
+ gameMenu.addSeparator();
+ gameMenu.add(customMenuItem);
+ menubar.add(gameMenu);
+ }
+
+ {
+ var highscoreMenu = new JMenu("Highscores");
+
+ var easyMenuItem = new JMenuItem("Easy");
+ var mediumMenuItem = new JMenuItem("Medium");
+ var hardMenuItem = new JMenuItem("Hard");
+
+ highscoreMenu.setMnemonic(KeyEvent.VK_H);
+ easyMenuItem.setMnemonic(KeyEvent.VK_E);
+ mediumMenuItem.setMnemonic(KeyEvent.VK_M);
+ hardMenuItem.setMnemonic(KeyEvent.VK_H);
+
+ easyMenuItem.addActionListener((evt) -> scoreManager.displayEasy());
+ mediumMenuItem.addActionListener((evt) -> scoreManager.displayMedium());
+ hardMenuItem.addActionListener((evt) -> scoreManager.displayHard());
+
+ highscoreMenu.add(easyMenuItem);
+ highscoreMenu.add(mediumMenuItem);
+ highscoreMenu.add(hardMenuItem);
+ menubar.add(highscoreMenu);
+ }
+
+ {
+ var aboutMenuItem = new JMenuItem("About");
+
+ aboutMenuItem.addActionListener((evt) -> JOptionPane.showMessageDialog(board,
+ "Minesweeper version " + VERSION + "\nby Kai S. K. Engelbart"));
+
+ menubar.add(aboutMenuItem);
+ }
+
+ mframe.setJMenuBar(menubar);
+ }
+
+ private void initGame(BoardConfig config) {
+ board.init(config);
+ gameTime = 0;
+ timer.restart();
+ mframe.pack();
+ }
+}
diff --git a/src/dev/kske/minesweeper/Score.java b/src/dev/kske/minesweeper/Score.java
new file mode 100644
index 0000000..00aca0c
--- /dev/null
+++ b/src/dev/kske/minesweeper/Score.java
@@ -0,0 +1,36 @@
+package dev.kske.minesweeper;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * Project: Minesweeper
+ * File: Score.java
+ * Created: 16.04.2019
+ * Author: Kai S. K. Engelbart
+ */
+public class Score implements Comparable, Serializable {
+
+ private static final long serialVersionUID = 3384023296639779740L;
+
+ private final String name;
+ private final int duration;
+ private final Date date;
+
+ public Score(String name, int duration, Date date) {
+ this.name = name;
+ this.duration = duration;
+ this.date = date;
+ }
+
+ public String getName() { return name; }
+
+ public int getDuration() { return duration; }
+
+ public Date getDate() { return date; }
+
+ @Override
+ public int compareTo(Score other) {
+ return Integer.compare(duration, other.duration);
+ }
+}
diff --git a/src/dev/kske/minesweeper/ScoreDialog.java b/src/dev/kske/minesweeper/ScoreDialog.java
new file mode 100644
index 0000000..f97ceb5
--- /dev/null
+++ b/src/dev/kske/minesweeper/ScoreDialog.java
@@ -0,0 +1,60 @@
+package dev.kske.minesweeper;
+
+import java.awt.BorderLayout;
+import java.awt.Font;
+import java.text.SimpleDateFormat;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTable;
+import javax.swing.SwingConstants;
+
+/**
+ * Project: Minesweeper
+ * File: ScoreDialog.java
+ * Created: 16.04.2019
+ * Author: Kai S. K. Engelbart
+ */
+public class ScoreDialog extends JDialog {
+
+ private static final long serialVersionUID = 3637727047056147815L;
+ private JTable mtable;
+
+ /**
+ * Create the dialog.
+ */
+ public ScoreDialog(List scores, String boardConfigName) {
+ setModal(true);
+ setBounds(100, 100, 450, 300);
+ setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+ getContentPane().setLayout(new BorderLayout(0, 0));
+
+ String[] columnNames = { "Place", "Name", "Duration", "Date" };
+ String[][] data = new String[scores.size()][4];
+ Iterator iter = scores.iterator();
+ for(int i = 0; i < data.length; i++) {
+ Score s = iter.next();
+ data[i][0] = String.valueOf(i + 1);
+ data[i][1] = s.getName();
+ data[i][2] = String.valueOf(s.getDuration());
+ data[i][3] = new SimpleDateFormat().format(s.getDate());
+ }
+
+ mtable = new JTable(data, columnNames);
+ getContentPane().add(mtable);
+
+ JPanel panel = new JPanel();
+ getContentPane().add(panel, BorderLayout.NORTH);
+ panel.setLayout(new BorderLayout(0, 0));
+
+ panel.add(mtable.getTableHeader(), BorderLayout.CENTER);
+
+ JLabel lblHighscores = new JLabel("Highscores: " + boardConfigName);
+ panel.add(lblHighscores, BorderLayout.NORTH);
+ lblHighscores.setFont(new Font("Tahoma", Font.BOLD, 16));
+ lblHighscores.setHorizontalAlignment(SwingConstants.CENTER);
+ }
+}
diff --git a/src/dev/kske/minesweeper/ScoreManager.java b/src/dev/kske/minesweeper/ScoreManager.java
new file mode 100644
index 0000000..3f70de3
--- /dev/null
+++ b/src/dev/kske/minesweeper/ScoreManager.java
@@ -0,0 +1,101 @@
+package dev.kske.minesweeper;
+
+import static dev.kske.minesweeper.BoardConfig.EASY;
+import static dev.kske.minesweeper.BoardConfig.HARD;
+import static dev.kske.minesweeper.BoardConfig.MEDIUM;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import javax.swing.JOptionPane;
+
+/**
+ * Project: Minesweeper
+ * File: ScoreManager.java
+ * Created: 15.05.2019
+ * Author: Kai S. K. Engelbart
+ */
+public class ScoreManager {
+
+ private List easy, medium, hard;
+ private final String scoresFile = "scores.ser";
+
+ public ScoreManager() {
+ easy = new ArrayList<>();
+ medium = new ArrayList<>();
+ hard = new ArrayList<>();
+ }
+
+ public void addScore(GameOverEvent evt) {
+ // Determine board config
+ BoardConfig config = evt.getBoardConfig();
+ if (config == EASY && (easy.size() < 10 || easy.get(9).getDuration() > evt.getDuration())) {
+ String name = JOptionPane.showInputDialog("Please enter your name");
+ Score score = new Score(name, evt.getDuration(), new Date());
+ sortInsert(score, easy);
+ } else if (config == MEDIUM && (medium.size() < 10 || medium.get(9).getDuration() > evt.getDuration())) {
+ String name = JOptionPane.showInputDialog("Please enter your name");
+ Score score = new Score(name, evt.getDuration(), new Date());
+ sortInsert(score, medium);
+ } else if (config == HARD && (hard.size() < 10 || hard.get(9).getDuration() > evt.getDuration())) {
+ String name = JOptionPane.showInputDialog("Please enter your name");
+ Score score = new Score(name, evt.getDuration(), new Date());
+ sortInsert(score, hard);
+ }
+ }
+
+ private void sortInsert(Score score, List list) {
+ for (int i = 0; i < list.size(); i++)
+ if (list.get(i).getDuration() > score.getDuration()) {
+ list.add(i, score);
+ return;
+ }
+ list.add(score);
+ }
+
+ public void displayEasy() {
+ new ScoreDialog(easy, "Easy").setVisible(true);
+ }
+
+ public void displayMedium() {
+ new ScoreDialog(medium, "Medium").setVisible(true);
+ }
+
+ public void displayHard() {
+ new ScoreDialog(hard, "Hard").setVisible(true);
+ }
+
+ @SuppressWarnings("unchecked")
+ public void loadScores() {
+ try (var in = new ObjectInputStream(new FileInputStream(scoresFile))) {
+ Object obj = in.readObject();
+ if (obj instanceof ArrayList>) easy = (ArrayList) obj;
+ obj = in.readObject();
+ if (obj instanceof ArrayList>) medium = (ArrayList) obj;
+ obj = in.readObject();
+ if (obj instanceof ArrayList>) hard = (ArrayList) obj;
+ else throw new IOException("Serialized object has the wrong class.");
+ } catch (FileNotFoundException ex) {} catch (IOException | ClassNotFoundException ex) {
+ JOptionPane.showMessageDialog(null,
+ "The score file seems to be corrupted. It will be replaced when closing the game.", "File error",
+ JOptionPane.ERROR_MESSAGE);
+ }
+ }
+
+ public void saveScores() {
+ try (var out = new ObjectOutputStream(new FileOutputStream(scoresFile))) {
+ out.writeObject(easy);
+ out.writeObject(medium);
+ out.writeObject(hard);
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ }
+ }
+}
diff --git a/src/dev/kske/minesweeper/TextureLoader.java b/src/dev/kske/minesweeper/TextureLoader.java
new file mode 100644
index 0000000..7059649
--- /dev/null
+++ b/src/dev/kske/minesweeper/TextureLoader.java
@@ -0,0 +1,38 @@
+package dev.kske.minesweeper;
+
+import java.awt.Image;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+
+import javax.imageio.ImageIO;
+
+/**
+ * Project: Minesweeper
+ * File: TextureLoader.java
+ * Created: 25.03.2019
+ * Author: Kai S. K. Engelbart
+ */
+public class TextureLoader {
+
+ private TextureLoader() {}
+
+ /**
+ * Loads an image from the resource folder and scales it to a square.
+ *
+ * @param name The name of the file without the PNG extension in the resource
+ * folder
+ * @param scale The side length of the square to which the image will be scaled
+ * @return The scaled image
+ */
+ public static Image loadScaledImage(String name, int scale) {
+ BufferedImage in = null;
+ try {
+ in = ImageIO.read(new File("res" + File.separator + name + ".png"));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ Image scaled = in.getScaledInstance(scale, scale, Image.SCALE_SMOOTH);
+ return scaled;
+ }
+}
diff --git a/src/dev/kske/minesweeper/Tile.java b/src/dev/kske/minesweeper/Tile.java
new file mode 100644
index 0000000..ccfa7e1
--- /dev/null
+++ b/src/dev/kske/minesweeper/Tile.java
@@ -0,0 +1,43 @@
+package dev.kske.minesweeper;
+
+/**
+ * Project: Minesweeper
+ * File: Tile.java
+ * Created: 22.03.2019
+ * Author: Kai S. K. Engelbart
+ */
+public class Tile {
+
+ private boolean mine, flagged, touched;
+
+ private boolean drawSurroundingMines;
+ private int surroundingMines;
+
+ public Tile() {
+ mine = flagged = touched = drawSurroundingMines = false;
+
+ surroundingMines = 0;
+ }
+
+ public boolean isMine() { return mine; }
+
+ public void setMine(boolean mine) { this.mine = mine; }
+
+ public boolean isFlagged() { return flagged; }
+
+ public void setFlagged(boolean flagged) { this.flagged = flagged; }
+
+ public boolean isTouched() { return touched; }
+
+ public void setTouched(boolean touched) { this.touched = touched; }
+
+ public boolean isDrawSurroundingMines() { return drawSurroundingMines; }
+
+ public void setDrawSurroundingMines(boolean drawSurroundingMines) {
+ this.drawSurroundingMines = drawSurroundingMines;
+ }
+
+ public int getSurroundingMines() { return surroundingMines; }
+
+ public void setSurroundingMines(int surroundingMines) { this.surroundingMines = surroundingMines; }
+}
diff --git a/src/module-info.java b/src/module-info.java
new file mode 100644
index 0000000..1649271
--- /dev/null
+++ b/src/module-info.java
@@ -0,0 +1,3 @@
+module Minesweeper {
+ requires java.desktop;
+}
\ No newline at end of file