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