fix minesweeper high score tracking
This commit is contained in:
5 files changed
+386
-112
No files matched your search
@@ -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
|
||||
|
||||
Whitespace-only changes.
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user