Added score management

- Renamed GameStateEvent to GameOverEvent
- Added a game time counter to Board
- Added a game duration property to BoardConfig and GameOverEvent
- Added Score and ScoreDialog classes
- Added a Set for managing scores to Minesweeper
This commit is contained in:
Kai S. K. Engelbart 2019-04-17 14:32:00 +02:00
parent 53e9c7ea26
commit 43d3d94756
Signed by: kske
GPG Key ID: 8BEB13EC5DF7EF13
9 changed files with 225 additions and 48 deletions

1
.gitignore vendored
View File

@ -53,3 +53,4 @@ local.properties
.cache-main .cache-main
.scala_dependencies .scala_dependencies
.worksheet .worksheet
/scores.ser

View File

@ -10,6 +10,8 @@ import java.awt.Graphics2D;
import java.awt.Image; import java.awt.Image;
import java.awt.event.MouseAdapter; import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -35,7 +37,9 @@ public class Board extends JPanel {
private GameState gameState; private GameState gameState;
private int mines, activeTiles, flaggedTiles; private int mines, activeTiles, flaggedTiles;
private Tile[][] board; private Tile[][] board;
private BoardConfig initialConfig; private BoardConfig boardConfig;
private Instant start, finish;
private List<GameListener> listeners; private List<GameListener> listeners;
@ -71,7 +75,7 @@ public class Board extends JPanel {
} }
public void init(BoardConfig config) { public void init(BoardConfig config) {
initialConfig = config; boardConfig = config;
boardWidth = config.width; boardWidth = config.width;
boardHeight = config.height; boardHeight = config.height;
@ -91,10 +95,12 @@ public class Board extends JPanel {
initMines(); initMines();
repaint(); repaint();
revalidate(); revalidate();
start = Instant.now();
} }
public void reset() { public void reset() {
init(initialConfig); init(boardConfig);
} }
@Override @Override
@ -147,8 +153,8 @@ public class Board extends JPanel {
listeners.add(listener); listeners.add(listener);
} }
private void notifyGameStateEvent(GameStateEvent evt) { private void notifyGameStateEvent(GameOverEvent evt) {
listeners.forEach(listener -> listener.onGameStateEvent(evt)); listeners.forEach(listener -> listener.onGameOverEvent(evt));
} }
private void notifyFlaggedTilesEvent(FlaggedTilesEvent evt) { private void notifyFlaggedTilesEvent(FlaggedTilesEvent evt) {
@ -200,14 +206,10 @@ public class Board extends JPanel {
// Test if the game is won or lost // Test if the game is won or lost
if (tile.isMine()) { if (tile.isMine()) {
gameState = GameState.LOST; gameState = GameState.LOST;
repaint(); onGameOver();
GameStateEvent evt = new GameStateEvent(this, gameState);
notifyGameStateEvent(evt);
} else if (mines == activeTiles) { } else if (mines == activeTiles) {
gameState = GameState.WON; gameState = GameState.WON;
repaint(); onGameOver();
GameStateEvent evt = new GameStateEvent(this, gameState);
notifyGameStateEvent(evt);
} }
// Touch surrounding tiles when there are zero surrounding mines // 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 getMines() { return mines; }
public int getActiveTiles() { return activeTiles; } public int getActiveTiles() { return activeTiles; }

View File

@ -1,13 +1,17 @@
package dev.kske.minesweeper; package dev.kske.minesweeper;
import java.io.Serializable;
/** /**
* Project: <strong>Minesweeper</strong><br> * Project: <strong>Minesweeper</strong><br>
* File: <strong>BoardConfig.java</strong><br> * File: <strong>BoardConfig.java</strong><br>
* Created: <strong>01.04.2019</strong><br> * Created: <strong>01.04.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong> * Author: <strong>Kai S. K. Engelbart</strong>
*/ */
public class BoardConfig { public class BoardConfig implements Serializable {
private static final long serialVersionUID = -6083006887427383946L;
public final int width, height, mines; public final int width, height, mines;
public BoardConfig(int width, int height, int mines) { public BoardConfig(int width, int height, int mines) {
@ -15,4 +19,9 @@ public class BoardConfig {
this.height = height; this.height = height;
this.mines = mines; this.mines = mines;
} }
@Override
public String toString() {
return String.format("%d %d %d", width, height, mines);
}
} }

View File

@ -8,7 +8,7 @@ package dev.kske.minesweeper;
*/ */
public interface GameListener { public interface GameListener {
void onGameStateEvent(GameStateEvent evt); void onGameOverEvent(GameOverEvent evt);
void onFlaggedTilesEvent(FlaggedTilesEvent evt); void onFlaggedTilesEvent(FlaggedTilesEvent evt);
} }

View File

@ -0,0 +1,35 @@
package dev.kske.minesweeper;
import java.util.EventObject;
/**
* Project: <strong>Minesweeper</strong><br>
* File: <strong>GameOverEvent.java</strong><br>
* Created: <strong>03.04.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
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; }
}

View File

@ -1,27 +0,0 @@
package dev.kske.minesweeper;
import java.util.EventObject;
/**
* Project: <strong>Minesweeper</strong><br>
* File: <strong>GameStateEvent.java</strong><br>
* Created: <strong>03.04.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
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; }
}

View File

@ -4,6 +4,16 @@ import java.awt.BorderLayout;
import java.awt.EventQueue; import java.awt.EventQueue;
import java.awt.FlowLayout; import java.awt.FlowLayout;
import java.awt.event.KeyEvent; 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.JButton;
import javax.swing.JFrame; import javax.swing.JFrame;
@ -24,11 +34,13 @@ import javax.swing.UIManager;
*/ */
public class Minesweeper { public class Minesweeper {
private static final String VERSION = "1.0 JE"; private static final String VERSION = "1.1 JE";
private JFrame mframe; private JFrame mframe;
private Board board; private Board board;
private TreeSet<Score> scores;
private final String scoresFile = "scores.ser";
private final BoardConfig easyConfig = new BoardConfig(8, 8, 10), mediumConfig = new BoardConfig(16, 16, 40), private final BoardConfig easyConfig = new BoardConfig(8, 8, 10), mediumConfig = new BoardConfig(16, 16, 40),
hardConfig = new BoardConfig(30, 16, 99); hardConfig = new BoardConfig(30, 16, 99);
@ -61,12 +73,19 @@ public class Minesweeper {
*/ */
private void initialize() { private void initialize() {
try { try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); UIManager.setLookAndFeel(UIManager.createLookAndFeel("Nimbus"));
} catch (Exception ex) { } catch (Exception ex) {
ex.printStackTrace(); ex.printStackTrace();
} }
mframe = new JFrame(); mframe = new JFrame();
mframe.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
saveScores();
}
});
mframe.setResizable(false); mframe.setResizable(false);
mframe.setTitle("Minesweeper"); mframe.setTitle("Minesweeper");
mframe.setBounds(100, 100, 198, 123); mframe.setBounds(100, 100, 198, 123);
@ -90,13 +109,18 @@ public class Minesweeper {
board.registerGameListener(new GameListener() { board.registerGameListener(new GameListener() {
@Override @Override
public void onGameStateEvent(GameStateEvent evt) { public void onGameOverEvent(GameOverEvent evt) {
switch (evt.getGameState()) { switch (evt.getGameState()) {
case LOST: case LOST:
JOptionPane.showMessageDialog(mframe, "Game lost!"); JOptionPane.showMessageDialog(mframe, "Game lost!");
break; break;
case WON: case WON:
JOptionPane.showMessageDialog(mframe, "Game 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); headerPanel.add(btnRestart);
btnRestart.addActionListener((evt) -> board.reset()); btnRestart.addActionListener((evt) -> board.reset());
mframe.pack(); mframe.pack();
loadScores();
} }
private void createMenuBar() { private void createMenuBar() {
var menubar = new JMenuBar(); var menubar = new JMenuBar();
var gameMenu = new JMenu("Game"); var gameMenu = new JMenu("Game");
var aboutMenuItem = new JMenuItem("About"); var highscoreMenuItem = new JMenuItem("Highscores");
var aboutMenuItem = new JMenuItem("About");
var easyMenuItem = new JMenuItem("Easy"); var easyMenuItem = new JMenuItem("Easy");
var mediumMenuItem = new JMenuItem("Medium"); var mediumMenuItem = new JMenuItem("Medium");
@ -138,9 +165,10 @@ public class Minesweeper {
BoardConfig cfg = new CustomDialog(mframe).showDialog(); BoardConfig cfg = new CustomDialog(mframe).showDialog();
if (cfg != null) initGame(cfg); 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(easyMenuItem);
gameMenu.add(mediumMenuItem); gameMenu.add(mediumMenuItem);
@ -148,11 +176,32 @@ public class Minesweeper {
gameMenu.addSeparator(); gameMenu.addSeparator();
gameMenu.add(customMenuItem); gameMenu.add(customMenuItem);
menubar.add(gameMenu); menubar.add(gameMenu);
menubar.add(highscoreMenuItem);
menubar.add(aboutMenuItem); menubar.add(aboutMenuItem);
mframe.setJMenuBar(menubar); mframe.setJMenuBar(menubar);
} }
@SuppressWarnings("unchecked")
private void loadScores() {
try (var in = new ObjectInputStream(new FileInputStream(scoresFile))) {
scores = (TreeSet<Score>) 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) { private void initGame(BoardConfig config) {
board.init(config); board.init(config);
mframe.pack(); mframe.pack();

View File

@ -0,0 +1,40 @@
package dev.kske.minesweeper;
import java.io.Serializable;
import java.util.Date;
/**
* Project: <strong>Minesweeper</strong><br>
* File: <strong>Score.java</strong><br>
* Created: <strong>16.04.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class Score implements Comparable<Score>, 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);
}
}

View File

@ -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: <strong>Minesweeper</strong><br>
* File: <strong>ScoreDialog.java</strong><br>
* Created: <strong>16.04.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class ScoreDialog extends JDialog {
private static final long serialVersionUID = 3637727047056147815L;
private JTable mtable;
/**
* Create the dialog.
*/
public ScoreDialog(Set<Score> 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<Score> 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);
}
}