fix minesweeper high score tracking

This commit is contained in:
SowinskiBraeden committed 2025-11-29 14:08:57 -08:00
1 parent 515d4331fa
commit eb16d809cc
5 files changed
+386 -112

No files matched your search

+10
View File
@@ -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
View File
Whitespace-only changes.
+2 -86
View File
@@ -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;
}
}
@@ -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<MinesScore> readScoresFromFile(final String scoreFilePath)
{
final List<MinesScore> 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<MinesScore> 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<MinesScore> 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;
}
}
+101 -26
View File
@@ -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<Integer, String> 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 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;
}
/**
@@ -584,18 +622,54 @@ public class MinesUI
private void handleWin()
{
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();
}
/**