diff --git a/data/minesweeper-score.txt b/data/minesweeper-score.txt new file mode 100644 index 0000000..128c675 --- /dev/null +++ b/data/minesweeper-score.txt @@ -0,0 +1,10 @@ +Date and Time: 2025-11-29 13:49:23 +Seconds: 50 +Difficulty: easy +Random Mode: false + +Date and Time: 2025-11-29 14:07:41 +Seconds: 17 +Difficulty: easy +Random Mode: false + diff --git a/scores.txt b/scores.txt deleted file mode 100644 index e69de29..0000000 diff --git a/src/code/ca/bcit/comp2522/project/Mines.java b/src/code/ca/bcit/comp2522/project/Mines.java index a638c4f..9ec57c9 100644 --- a/src/code/ca/bcit/comp2522/project/Mines.java +++ b/src/code/ca/bcit/comp2522/project/Mines.java @@ -6,6 +6,7 @@ import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; +import java.time.format.DateTimeFormatter; import java.util.Random; import java.util.Scanner; import java.util.function.Consumer; @@ -53,8 +54,6 @@ public class Mines extends GameBoard private boolean[] revealed; private int[] flagged; - private int bestScoreSeconds; - /** * Mines constructor generates minefield board * with a given width, height and number of mines. @@ -72,9 +71,7 @@ public class Mines extends GameBoard super(width, height); this.totalMines = mines; this.randomMode = randomMode; - this.bestScoreSeconds = INITIAL_BEST_SCORE; reset(); - loadBestScore(); } /** @@ -120,7 +117,7 @@ public class Mines extends GameBoard placedMines = NO_MINE; totalCells = this.width * this.height; - while (placedMines < totalMines) + while (placedMines < this.totalMines) { final int index; @@ -432,85 +429,4 @@ public class Mines extends GameBoard } } - - /** - * saveScore to file, then store seconds if - * less than last score - * @param seconds of game time - */ - public void saveScore(final int seconds) - { - try - { - final BufferedWriter writer; - - writer = new BufferedWriter(new FileWriter(SCORE_FILE_NAME)); - - writer.write(Integer.toString(seconds)); - writer.newLine(); - } - catch (final IOException e) - { - // ignore failure - } - - if (seconds < this.bestScoreSeconds) - { - this.bestScoreSeconds = seconds; - } - } - - /** - * loadBestScore from score file, gets - * the best score (the least number of - * seconds) from the score file. - */ - private void loadBestScore() - { - final File scoreFile; - - scoreFile = new File(SCORE_FILE_NAME); - - if (!scoreFile.exists()) - { - return; - } - - int best; - best = INITIAL_BEST_SCORE; - - try - { - final Scanner scanner; - scanner = new Scanner(new BufferedReader(new FileReader(scoreFile))); - - while (scanner.hasNextInt()) - { - final int candidate; - - candidate = scanner.nextInt(); - - if (candidate < best) - { - best = candidate; - } - } - } - catch (final IOException e) - { - // ignore failure - } - - this.bestScoreSeconds = best; - } - - /** - * getBestScoreSeconds returns the best - * score in seconds - * @return best score in seconds - */ - public int getBestScoreSeconds() - { - return this.bestScoreSeconds; - } } diff --git a/src/code/ca/bcit/comp2522/project/MinesScore.java b/src/code/ca/bcit/comp2522/project/MinesScore.java new file mode 100644 index 0000000..ebc9fee --- /dev/null +++ b/src/code/ca/bcit/comp2522/project/MinesScore.java @@ -0,0 +1,273 @@ +package ca.bcit.comp2522.project; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Score manager for WordGame, writes and reads + * scores from file, get and validate high-scores, + * calculate score averages and provides useful + * toString methods to summarize scores. + * + * @author Braeden Sowinski + * @version 1.0.0 + */ +public class MinesScore +{ + private static final String DATE_PATTERN = "yyyy-MM-dd HH:mm:ss"; + private static final int SPLIT_VALUE = 1; + + public static final String DIFFICULTY_EASY = "easy"; + public static final String DIFFICULTY_MEDIUM = "medium"; + public static final String DIFFICULTY_HARD = "hard"; + + private final String dateTimePlayed; + private final String difficulty; + private final boolean randomMode; + private final int seconds; + + private static void validateDifficulty(final String difficulty) + { + if (difficulty == null) + { + throw new IllegalArgumentException("Difficulty cannot be null"); + } + + if (!difficulty.equalsIgnoreCase(DIFFICULTY_EASY) && + !difficulty.equalsIgnoreCase(DIFFICULTY_MEDIUM) && + !difficulty.equalsIgnoreCase(DIFFICULTY_HARD) + ) { + throw new IllegalArgumentException("Invalid difficulty"); + } + + } + + /** + * appendScoreToFile takes in a Score object + * and filepath, appends Score values to file + * accordingly. + * @param score to append to filepath + * @param scoreFilePath filepath to append Score to + */ + public static void appendScoreToFile( + final MinesScore score, + final String scoreFilePath + ) { + try + { + final BufferedWriter writer; + + writer = new BufferedWriter(new FileWriter(scoreFilePath, true)); + + writer.write(score.toString()); + writer.newLine(); + + writer.close(); + } + catch (final IOException e) + { + System.err.println("Failed to write " + scoreFilePath); + } + } + + /** + * readScoresFromFile returns a history of all Scores + * read from the given filepath. + * @param scoreFilePath to read scores from + * @return a list of all Scores read from given filepath + */ + public static List readScoresFromFile(final String scoreFilePath) + { + final List scores; + + scores = new ArrayList<>(); + + try + { + final BufferedReader reader; + + reader = new BufferedReader(new FileReader(scoreFilePath)); + + String line; + while ((line = reader.readLine()) != null) + { + + if (!line.isBlank() && line.contains("Date and Time: ")) + { + final DateTimeFormatter formatter; + final String dateTime; + final MinesScore score; + final String seconds; + final String difficulty; + final String randomMode; + + formatter = DateTimeFormatter.ofPattern(DATE_PATTERN); + + dateTime = line.split(": ")[SPLIT_VALUE]; + seconds = reader.readLine().split(": ")[SPLIT_VALUE]; + difficulty = reader.readLine().split(": ")[SPLIT_VALUE]; + randomMode = reader.readLine().split(": ")[SPLIT_VALUE]; + + reader.readLine(); // Skip over score line + + score = new MinesScore( + LocalDateTime.parse(dateTime, formatter), + Integer.parseInt(seconds), + difficulty, + Boolean.parseBoolean(randomMode) + ); + + scores.add(score); + } + } + + reader.close(); + } + catch (final IOException e) + { + System.err.println("Failed to open score log file."); + } + + return scores; + } + + /** + * getHighScore from a given List of Scores + * @param scores list to find high-score from + * @return high Score from list of scores + */ + public static MinesScore getHighScore( + final List scores, + final String difficulty, + final boolean randomMode + ) { + return scores.stream() + .filter(s -> s.getDifficulty().equalsIgnoreCase(difficulty)) + .filter(s -> s.getRandomMode() == randomMode) + .min(Comparator.comparingInt(MinesScore::getSeconds)) + .orElse(null); + } + + /** + * isHighScore takes a List of Scores and + * new Score to check if the new Score is + * a high-score in the given scores list. + * @param score to check if is high score + * @param scores list to compare Score to + * @return if score is a new high-score in list + */ + public static boolean isHighScore( + final MinesScore score, + final List scores + ) { + final MinesScore highScore; + + highScore = MinesScore.getHighScore( + scores, + score.getDifficulty(), + score.getRandomMode() + ); + + return (highScore == null || + highScore.getSeconds() > score.getSeconds()); + } + + /** + * Score constructor saves score dateTime, + * number of games played for this score, + * and relevant scores based on number of + * guesses. + * @param dateTime the score was recorded + * @param seconds is the number seconds for that round + * @param difficulty the game was played in + * @param randomMode was on or off + */ + public MinesScore( + final LocalDateTime dateTime, + final int seconds, + final String difficulty, + final boolean randomMode + ) { + final DateTimeFormatter formatter; + + formatter = DateTimeFormatter.ofPattern(DATE_PATTERN); + + this.dateTimePlayed = dateTime.format(formatter); + + this.seconds = seconds; + this.difficulty = difficulty; + this.randomMode = randomMode; + } + + /** + * getDateTimePlayed of Score + * @return dateTimePlayed as a String + */ + public String getDateTimePlayed() + { + return this.dateTimePlayed; + } + + /** + * toString neatly presents this Score + * in a String format + * @return formatted String of Score details + */ + @Override + public String toString() + { + final StringBuilder log; + + log = new StringBuilder(); + + log.append("Date and Time: "); + log.append(this.dateTimePlayed); + log.append("\n"); + log.append("Seconds: "); + log.append(this.seconds); + log.append("\n"); + log.append("Difficulty: "); + log.append(this.difficulty); + log.append("\n"); + log.append("Random Mode: "); + log.append(this.randomMode); + log.append("\n"); + + return log.toString(); + } + + /** + * getScore returns total seconds of this Score + * @return total seconds score + */ + public int getSeconds() + { + return this.seconds; + } + + /** + * getDifficulty of this score + * @return difficulty of this score + */ + public String getDifficulty() + { + return this.difficulty; + } + + /** + * getRandomMode status of this score + * @return if this score was in random mode + */ + public boolean getRandomMode() + { + return this.randomMode; + } +} diff --git a/src/code/ca/bcit/comp2522/project/MinesUI.java b/src/code/ca/bcit/comp2522/project/MinesUI.java index a71ffda..dbfc5cb 100644 --- a/src/code/ca/bcit/comp2522/project/MinesUI.java +++ b/src/code/ca/bcit/comp2522/project/MinesUI.java @@ -9,7 +9,6 @@ import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.Label; -import javafx.scene.input.KeyCombination; import javafx.scene.input.MouseButton; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; @@ -20,6 +19,7 @@ import javafx.scene.text.FontWeight; import javafx.stage.Stage; import javafx.util.Duration; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -83,6 +83,7 @@ public class MinesUI private static final int ZERO_KEY = 0; private static final String TITLE_TEXT = "Random Mines - A Minesweeper Game"; + private static final String SCORE_FILE = "./data/minesweeper-score.txt"; private static final Map BUTTON_THEMES; @@ -106,9 +107,9 @@ public class MinesUI private Mines game; + private Stage gameStage; private Label flagLabel; private Label timerLabel; - private Label bestLabel; private boolean randomMode; private int flagsPlaced; @@ -279,11 +280,11 @@ public class MinesUI final int windowWidth, final int windowHeight ) { - final Stage gameStage; - final VBox root; - final VBox topBar; - final GridPane grid; - final Scene scene; + final VBox root; + final VBox topBar; + final GridPane grid; + final Scene scene; + final Label bestLabel; this.flagsPlaced = STARTING_FLAGS; this.seconds = STARTING_SECONDS; @@ -296,27 +297,30 @@ public class MinesUI this.flagLabel = new Label("Flags: " + this.flagsPlaced + " / " + this.game.getTotalMines()); this.timerLabel = new Label("Time: " + this.seconds); - final int bestScore; - bestScore = this.game.getBestScoreSeconds(); + final MinesScore bestScore; + final String difficulty; - if (bestScore == Integer.MAX_VALUE) - { - this.bestLabel = new Label("Best: -"); - } - else - { - this.bestLabel = new Label("Best: " + bestScore + "s"); - } + difficulty = mines == EASY_MINES ? MinesScore.DIFFICULTY_EASY : + mines == MEDIUM_MINES ? MinesScore.DIFFICULTY_MEDIUM : + MinesScore.DIFFICULTY_HARD; + + bestScore = MinesScore.getHighScore( + MinesScore.readScoresFromFile(SCORE_FILE), + difficulty, + this.randomMode + ); + + bestLabel = getBestLabel(bestScore); this.flagLabel.setFont(FONT); this.timerLabel.setFont(FONT); - this.bestLabel.setFont(FONT); + bestLabel.setFont(FONT); topBar = new VBox( TOPBAR_SPACING, this.flagLabel, this.timerLabel, - this.bestLabel + bestLabel ); topBar.setAlignment(Pos.CENTER); @@ -329,14 +333,48 @@ public class MinesUI scene = new Scene(root, WINDOW_WIDTH, WINDOW_HEIGHT); - gameStage = new Stage(); - gameStage.setResizable(true); - gameStage.setMinWidth(windowWidth); - gameStage.setMinHeight(windowHeight); - gameStage.initOwner(ownerStage); - gameStage.setTitle("Random Mines " + width + "x" + height + " - A Minesweeper Game"); - gameStage.setScene(scene); - gameStage.show(); + this.gameStage = new Stage(); + this.gameStage.setResizable(true); + this.gameStage.setMinWidth(windowWidth); + this.gameStage.setMinHeight(windowHeight); + this.gameStage.initOwner(ownerStage); + this.gameStage.setTitle("Random Mines " + width + "x" + height + " - A Minesweeper Game"); + this.gameStage.setScene(scene); + this.gameStage.show(); + } + + /** + * getBestLabel generates the label with + * best score from score file + * @param bestScore to create label from + * @return Label of best score + */ + private static Label getBestLabel(final MinesScore bestScore) + { + final Label bestLabel; + if (bestScore == null) + { + bestLabel = new Label("Best: -"); + } + else + { + final StringBuilder best; + + best = new StringBuilder(); + + best.append("Best: "); + best.append(bestScore.getSeconds()); + best.append("s Difficulty: "); + best.append(bestScore.getDifficulty()); + best.append(" Random Mode: "); + best.append(bestScore.getRandomMode()); + best.append(" Date: "); + best.append(bestScore.getDateTimePlayed()); + + bestLabel = new Label(best.toString()); + } + + return bestLabel; } /** @@ -583,19 +621,55 @@ public class MinesUI */ private void handleWin() { - final Alert winAlert; + final Alert winAlert; + final MinesScore score; + final String difficulty; + final int mines; stopTimer(); - this.game.saveScore(this.seconds); - this.bestLabel.setText("Best: " + this.game.getBestScoreSeconds() + "s"); + + mines = this.game.getTotalMines(); + + difficulty = mines == EASY_MINES ? MinesScore.DIFFICULTY_EASY : + mines == MEDIUM_MINES ? MinesScore.DIFFICULTY_MEDIUM : + MinesScore.DIFFICULTY_HARD; + + score = new MinesScore( + LocalDateTime.now(), + this.seconds, + difficulty, + this.randomMode + ); + + System.out.println(this.seconds + difficulty + this.randomMode); showAllMines(); disableAllButtons(); + final StringBuilder winMessage; + + winMessage = new StringBuilder(); + + winMessage.append("You successfully cleared all safe squares!"); + + if (MinesScore.isHighScore(score, MinesScore.readScoresFromFile(SCORE_FILE))) + { + winMessage.append("\nNew High Score! "); + winMessage.append("Time: "); + winMessage.append(score.getSeconds()); + winMessage.append("s\nDifficulty: "); + winMessage.append(score.getDifficulty()); + winMessage.append("\nRandom Mode: "); + winMessage.append(score.getRandomMode()); + } + + MinesScore.appendScoreToFile(score, SCORE_FILE); + winAlert = new Alert(Alert.AlertType.INFORMATION); winAlert.setHeaderText("You Win!"); - winAlert.setContentText("You successfully cleared all safe squares!"); + winAlert.setContentText(winMessage.toString()); winAlert.showAndWait(); + this.gameStage.close(); } /** @@ -614,6 +688,7 @@ public class MinesUI lossAlert.setHeaderText("You lost..."); lossAlert.setContentText("You dug up a mine and lost your legs."); lossAlert.showAndWait(); + this.gameStage.close(); } /**