diff --git a/.gitignore b/.gitignore index 3417075..1f514cf 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,4 @@ local.properties .cache-main .scala_dependencies .worksheet +/scores.ser diff --git a/src/dev/kske/minesweeper/Board.java b/src/dev/kske/minesweeper/Board.java index 18355bb..a2fb517 100644 --- a/src/dev/kske/minesweeper/Board.java +++ b/src/dev/kske/minesweeper/Board.java @@ -10,6 +10,8 @@ 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; @@ -35,7 +37,9 @@ public class Board extends JPanel { private GameState gameState; private int mines, activeTiles, flaggedTiles; private Tile[][] board; - private BoardConfig initialConfig; + private BoardConfig boardConfig; + + private Instant start, finish; private List listeners; @@ -71,7 +75,7 @@ public class Board extends JPanel { } public void init(BoardConfig config) { - initialConfig = config; + boardConfig = config; boardWidth = config.width; boardHeight = config.height; @@ -91,10 +95,12 @@ public class Board extends JPanel { initMines(); repaint(); revalidate(); + + start = Instant.now(); } public void reset() { - init(initialConfig); + init(boardConfig); } @Override @@ -147,8 +153,8 @@ public class Board extends JPanel { listeners.add(listener); } - private void notifyGameStateEvent(GameStateEvent evt) { - listeners.forEach(listener -> listener.onGameStateEvent(evt)); + private void notifyGameStateEvent(GameOverEvent evt) { + listeners.forEach(listener -> listener.onGameOverEvent(evt)); } private void notifyFlaggedTilesEvent(FlaggedTilesEvent evt) { @@ -200,14 +206,10 @@ public class Board extends JPanel { // Test if the game is won or lost if (tile.isMine()) { gameState = GameState.LOST; - repaint(); - GameStateEvent evt = new GameStateEvent(this, gameState); - notifyGameStateEvent(evt); + onGameOver(); } else if (mines == activeTiles) { gameState = GameState.WON; - repaint(); - GameStateEvent evt = new GameStateEvent(this, gameState); - notifyGameStateEvent(evt); + onGameOver(); } // Touch surrounding tiles when there are zero surrounding mines @@ -235,6 +237,15 @@ public class Board extends JPanel { } } + 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; } diff --git a/src/dev/kske/minesweeper/BoardConfig.java b/src/dev/kske/minesweeper/BoardConfig.java index 708316e..bdb864a 100644 --- a/src/dev/kske/minesweeper/BoardConfig.java +++ b/src/dev/kske/minesweeper/BoardConfig.java @@ -1,13 +1,17 @@ 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 { +public class BoardConfig implements Serializable { + private static final long serialVersionUID = -6083006887427383946L; + public final int width, height, mines; public BoardConfig(int width, int height, int mines) { @@ -15,4 +19,9 @@ public class BoardConfig { this.height = height; this.mines = mines; } + + @Override + public String toString() { + return String.format("%d %d %d", width, height, mines); + } } diff --git a/src/dev/kske/minesweeper/GameListener.java b/src/dev/kske/minesweeper/GameListener.java index 3aef1dd..b18c464 100644 --- a/src/dev/kske/minesweeper/GameListener.java +++ b/src/dev/kske/minesweeper/GameListener.java @@ -8,7 +8,7 @@ package dev.kske.minesweeper; */ public interface GameListener { - void onGameStateEvent(GameStateEvent evt); + 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/GameStateEvent.java b/src/dev/kske/minesweeper/GameStateEvent.java deleted file mode 100644 index f61b8ca..0000000 --- a/src/dev/kske/minesweeper/GameStateEvent.java +++ /dev/null @@ -1,27 +0,0 @@ -package dev.kske.minesweeper; - -import java.util.EventObject; - -/** - * Project: Minesweeper
- * File: GameStateEvent.java
- * Created: 03.04.2019
- * Author: Kai S. K. Engelbart - */ -public class GameStateEvent extends EventObject { - - private static final long serialVersionUID = -966111253980213845L; - - private final Board board; - private final GameState gameState; - - public GameStateEvent(Object source, GameState gameState) { - super(source); - board = (Board) source; - this.gameState = gameState; - } - - public Board getBoard() { return board; } - - public GameState getGameState() { return gameState; } -} diff --git a/src/dev/kske/minesweeper/Minesweeper.java b/src/dev/kske/minesweeper/Minesweeper.java index a1529ca..554a49f 100644 --- a/src/dev/kske/minesweeper/Minesweeper.java +++ b/src/dev/kske/minesweeper/Minesweeper.java @@ -4,6 +4,16 @@ import java.awt.BorderLayout; import java.awt.EventQueue; import java.awt.FlowLayout; import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +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.Date; +import java.util.TreeSet; import javax.swing.JButton; import javax.swing.JFrame; @@ -24,11 +34,13 @@ import javax.swing.UIManager; */ public class Minesweeper { - private static final String VERSION = "1.0 JE"; + private static final String VERSION = "1.1 JE"; private JFrame mframe; private Board board; + private TreeSet scores; + private final String scoresFile = "scores.ser"; private final BoardConfig easyConfig = new BoardConfig(8, 8, 10), mediumConfig = new BoardConfig(16, 16, 40), hardConfig = new BoardConfig(30, 16, 99); @@ -61,12 +73,19 @@ public class Minesweeper { */ private void initialize() { try { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + UIManager.setLookAndFeel(UIManager.createLookAndFeel("Nimbus")); } catch (Exception ex) { ex.printStackTrace(); } mframe = new JFrame(); + mframe.addWindowListener(new WindowAdapter() { + + @Override + public void windowClosing(WindowEvent e) { + saveScores(); + } + }); mframe.setResizable(false); mframe.setTitle("Minesweeper"); mframe.setBounds(100, 100, 198, 123); @@ -90,13 +109,18 @@ public class Minesweeper { board.registerGameListener(new GameListener() { @Override - public void onGameStateEvent(GameStateEvent evt) { + public void onGameOverEvent(GameOverEvent evt) { switch (evt.getGameState()) { case LOST: JOptionPane.showMessageDialog(mframe, "Game lost!"); break; case WON: JOptionPane.showMessageDialog(mframe, "Game won!"); + if (scores.size() < 10 || scores.last().getDuration() > evt.getDuration()) { + String name = JOptionPane.showInputDialog("Please enter your name"); + Score score = new Score(name, evt.getDuration(), new Date(), evt.getBoardConfig()); + scores.add(score); + } } } @@ -112,13 +136,16 @@ public class Minesweeper { headerPanel.add(btnRestart); btnRestart.addActionListener((evt) -> board.reset()); mframe.pack(); + + loadScores(); } private void createMenuBar() { var menubar = new JMenuBar(); - var gameMenu = new JMenu("Game"); - var aboutMenuItem = new JMenuItem("About"); + var gameMenu = new JMenu("Game"); + var highscoreMenuItem = new JMenuItem("Highscores"); + var aboutMenuItem = new JMenuItem("About"); var easyMenuItem = new JMenuItem("Easy"); var mediumMenuItem = new JMenuItem("Medium"); @@ -138,9 +165,10 @@ public class Minesweeper { BoardConfig cfg = new CustomDialog(mframe).showDialog(); if (cfg != null) initGame(cfg); }); - aboutMenuItem.addActionListener((evt) -> { - JOptionPane.showMessageDialog(board, "Minesweeper version " + VERSION + "\nby Kai S. K. Engelbart"); - }); + + highscoreMenuItem.addActionListener((evt) -> new ScoreDialog(scores).setVisible(true)); + aboutMenuItem.addActionListener((evt) -> JOptionPane.showMessageDialog(board, + "Minesweeper version " + VERSION + "\nby Kai S. K. Engelbart")); gameMenu.add(easyMenuItem); gameMenu.add(mediumMenuItem); @@ -148,11 +176,32 @@ public class Minesweeper { gameMenu.addSeparator(); gameMenu.add(customMenuItem); menubar.add(gameMenu); + menubar.add(highscoreMenuItem); menubar.add(aboutMenuItem); mframe.setJMenuBar(menubar); } + @SuppressWarnings("unchecked") + private void loadScores() { + try (var in = new ObjectInputStream(new FileInputStream(scoresFile))) { + scores = (TreeSet) in.readObject(); + } catch (FileNotFoundException ex) { + scores = new TreeSet<>(); + } catch (IOException | ClassNotFoundException ex) { + JOptionPane.showMessageDialog(mframe, "The score file seems to be corrupted. It will be replaced when closing the game.", "File error", JOptionPane.ERROR_MESSAGE); + scores = new TreeSet<>(); + } + } + + private void saveScores() { + try (var out = new ObjectOutputStream(new FileOutputStream(scoresFile))) { + out.writeObject(scores); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + private void initGame(BoardConfig config) { board.init(config); mframe.pack(); diff --git a/src/dev/kske/minesweeper/Score.java b/src/dev/kske/minesweeper/Score.java new file mode 100644 index 0000000..089fe83 --- /dev/null +++ b/src/dev/kske/minesweeper/Score.java @@ -0,0 +1,40 @@ +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; + private final BoardConfig boardConfig; + + public Score(String name, int duration, Date date, BoardConfig boardConfig) { + this.name = name; + this.duration = duration; + this.date = date; + this.boardConfig = boardConfig; + } + + public String getName() { return name; } + + public int getDuration() { return duration; } + + public Date getDate() { return date; } + + public BoardConfig getBoardConfig() { return boardConfig; } + + @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..11a4553 --- /dev/null +++ b/src/dev/kske/minesweeper/ScoreDialog.java @@ -0,0 +1,59 @@ +package dev.kske.minesweeper; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.text.SimpleDateFormat; +import java.util.Iterator; +import java.util.Set; + +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(Set scores) { + setBounds(100, 100, 450, 300); + setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + getContentPane().setLayout(new BorderLayout(0, 0)); + + String[] columnNames = {"Name", "Game duration", "Board Config", "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] = s.getName(); + data[i][1] = String.valueOf(s.getDuration()); + data[i][2] = s.getBoardConfig().toString(); + 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"); + panel.add(lblHighscores, BorderLayout.NORTH); + lblHighscores.setFont(new Font("Tahoma", Font.BOLD, 16)); + lblHighscores.setHorizontalAlignment(SwingConstants.CENTER); + } +}