Files
minesweeper-comp2522/src/code/ca/bcit/comp2522/project/MinesUI.java
T

765 lines
23 KiB
Java

package ca.bcit.comp2522.project;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.stage.Window;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
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;
import java.util.Map;
/**
* MinesUI handles all Minesweeper UI elements,
* main menu, grid generation of the minefield.
* Updating button displays, disabling buttons,
* showing flags, warnings, etc.
*
* @author Braeden Sowinski
* @version 1.0.0
*/
public class MinesUI
{
private static final int MENU_FONT_SIZE = 14;
private static final int FONT_SIZE = 18;
private static final Font FONT = Font.font("Arial", FontWeight.BOLD, FONT_SIZE);
private static final Font MENU_FONT = Font.font("Arial", FontWeight.NORMAL, MENU_FONT_SIZE);
private static final int MENU_PADDING = 5;
private static final int POPUP_WIDTH = 500;
private static final int POPUP_HEIGHT = 250;
private static final int WINDOW_WIDTH = 900;
private static final int WINDOW_HEIGHT = 700;
private static final int BUTTON_WIDTH = 40;
private static final int BUTTON_HEIGHT = 40;
private static final int MENU_BUTTON_WIDTH = 200;
private static final int MENU_BUTTON_HEIGHT = 60;
private static final int PADDING = 20;
private static final int GAME_PADDING = 5;
private static final int VERTICAL_MARGIN = 15;
private static final int TOPBAR_SPACING = 5;
private static final int EASY_WIDTH = 8;
private static final int EASY_HEIGHT = 8;
private static final int EASY_MINES = 10;
private static final int MEDIUM_WIDTH = 16;
private static final int MEDIUM_HEIGHT = 16;
private static final int MEDIUM_MINES = 40;
private static final int HARD_WIDTH = 36;
private static final int HARD_HEIGHT = 16;
private static final int HARD_MINES = 99;
private static final int EASY_WINDOW_WIDTH = 600;
private static final int EASY_WINDOW_HEIGHT = 600;
private static final int MEDIUM_WINDOW_WIDTH = 1000;
private static final int MEDIUM_WINDOW_HEIGHT = 1000;
private static final int HARD_WINDOW_WIDTH = 1800;
private static final int HARD_WINDOW_HEIGHT = 1000;
private static final int STARTING_FLAGS = 0;
private static final int STARTING_SECONDS = 0;
private static final int TIMER_TICK_SECONDS = 1;
private static final int DEFAULT_BUTTON_KEY = -2;
private static final int MINE_KEY = -1;
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;
static
{
BUTTON_THEMES = new java.util.HashMap<>();
BUTTON_THEMES.put(-2, "-fx-background-color: #b3b3b3; -fx-text-fill: black;");
BUTTON_THEMES.put(-1, "-fx-background-color: #d9d9d9; -fx-text-fill: black;");
BUTTON_THEMES.put(0, "-fx-background-color: #d9d9d9; -fx-text-fill: black;");
BUTTON_THEMES.put(1, "-fx-background-color: #c8ffbf; -fx-text-fill: black;");
BUTTON_THEMES.put(2, "-fx-background-color: #edfc9f; -fx-text-fill: black;");
BUTTON_THEMES.put(3, "-fx-background-color: #fad08c; -fx-text-fill: black;");
BUTTON_THEMES.put(4, "-fx-background-color: #f59338; -fx-text-fill: black;");
BUTTON_THEMES.put(5, "-fx-background-color: #ff644d; -fx-text-fill: black;");
BUTTON_THEMES.put(6, "-fx-background-color: #ff644d; -fx-text-fill: black;");
BUTTON_THEMES.put(7, "-fx-background-color: #ff644d; -fx-text-fill: black;");
BUTTON_THEMES.put(8, "-fx-background-color: #ff644d; -fx-text-fill: black;");
}
private final List<Button> buttons;
private Mines game;
private Stage gameStage;
private Label flagLabel;
private Label timerLabel;
private boolean randomMode;
private int flagsPlaced;
private int seconds;
private Timeline timer;
private boolean timerRunning;
/**
* MinesUI constructor creates an ArrayList
* to store buttons and track which mode
* the game is in, normal or random
*/
public MinesUI()
{
this.buttons = new ArrayList<>();
this.randomMode = false;
this.timerRunning = false;
}
/*
* showInfo displays an info popup
* about the random mode
*/
private void showInfo(final Window window)
{
final Stage popup;
final VBox layout;
final Label msgLabel;
final Button okButton;
final Scene popupScene;
popup = new Stage();
popup.setTitle("Random Mines - A Minesweeper Game");
msgLabel = new Label("Random Mode regenerates the field every move. " +
"Any mine that is not correctly flagged will get randomly moved " +
"after each reveal! This may make it easier to start, but riskier " +
"to end. Beware of losing track of mines you haven't flagged.");
msgLabel.setFont(MENU_FONT);
msgLabel.setAlignment(Pos.CENTER);
msgLabel.setWrapText(true);
msgLabel.setPadding(new Insets(PADDING));
okButton = new Button("OK");
okButton.setFont(MENU_FONT);
okButton.setOnAction(e -> {
popup.close();
});
layout = new VBox(PADDING, msgLabel, okButton);
layout.setAlignment(Pos.CENTER);
layout.setPadding(new Insets(MENU_PADDING));
popupScene = new Scene(layout, POPUP_WIDTH, POPUP_HEIGHT);
popup.setScene(popupScene);
popup.setResizable(false);
popup.initOwner(window);
popup.show();
}
/**
* showMainMenu displays main menu to choose
* game mode, and field size.
* @param primaryStage to display main menu to
*/
/**
* showMainMenu displays main menu to choose
* game mode and field size.
*
* @param primaryStage to display main menu on
*/
public void showMainMenu(final Stage primaryStage)
{
final Label titleLabel;
final Button easyButton;
final Button mediumButton;
final Button hardButton;
final Label modeLabel;
final Button toggleModeButton;
final Button modeInfoButton;
final Button quitButton;
final HBox modeButtons;
final VBox root;
final Scene scene;
titleLabel = new Label("Select Game Size");
titleLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold;");
easyButton = new Button("8 x 8 (Beginner)");
mediumButton = new Button("16 x 16 (Intermediate)");
hardButton = new Button("36 x 16 (Expert)");
easyButton.setPrefWidth(MENU_BUTTON_WIDTH);
easyButton.setPrefHeight(MENU_BUTTON_HEIGHT);
mediumButton.setPrefWidth(MENU_BUTTON_WIDTH);
mediumButton.setPrefHeight(MENU_BUTTON_HEIGHT);
hardButton.setPrefWidth(MENU_BUTTON_WIDTH);
hardButton.setPrefHeight(MENU_BUTTON_HEIGHT);
modeLabel = new Label("Random Mode: OFF");
toggleModeButton = new Button("Toggle Random Mode");
modeInfoButton = new Button("?");
quitButton = new Button("Quit");
modeButtons = new HBox(MENU_PADDING, toggleModeButton, modeInfoButton);
toggleModeButton.setOnAction(e -> {
this.randomMode = !this.randomMode;
modeLabel.setText("Random Mode: " + (this.randomMode ? "ON" : "OFF"));
});
modeInfoButton.setOnAction(e -> showInfo(primaryStage));
quitButton.setFont(MENU_FONT);
quitButton.setOnAction(e -> primaryStage.close());
easyButton.setOnAction(e -> startGame(
EASY_WIDTH,
EASY_HEIGHT,
EASY_MINES,
primaryStage,
EASY_WINDOW_WIDTH,
EASY_WINDOW_HEIGHT
)
);
mediumButton.setOnAction(e -> startGame(
MEDIUM_WIDTH,
MEDIUM_HEIGHT,
MEDIUM_MINES,
primaryStage,
MEDIUM_WINDOW_WIDTH,
MEDIUM_WINDOW_HEIGHT
)
);
hardButton.setOnAction(e -> startGame(
HARD_WIDTH,
HARD_HEIGHT,
HARD_MINES,
primaryStage,
HARD_WINDOW_WIDTH,
HARD_WINDOW_HEIGHT
)
);
root = new VBox(VERTICAL_MARGIN);
root.setPadding(new Insets(PADDING));
root.setAlignment(Pos.CENTER);
modeButtons.setAlignment(Pos.CENTER);
root.getChildren().addAll(
titleLabel,
easyButton,
mediumButton,
hardButton,
modeLabel,
modeButtons,
quitButton
);
scene = new Scene(root, WINDOW_WIDTH, WINDOW_HEIGHT);
primaryStage.setTitle(TITLE_TEXT);
primaryStage.setResizable(false);
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* startGame creates new Mine game, to
* generate field, place mines, handle
* randomization if enabled. Creates
* grid of buttons for minefield, displays
* new window of game instance of given size.
* @param width of field to generate
* @param height of field to generates
* @param mines to generate in field
* @param ownerStage to display minefield grid to
*/
private void startGame(
final int width,
final int height,
final int mines,
final Stage ownerStage,
final int windowWidth,
final int windowHeight
) {
final VBox root;
final VBox topBar;
final GridPane grid;
final Scene scene;
final Label bestLabel;
this.flagsPlaced = STARTING_FLAGS;
this.seconds = STARTING_SECONDS;
this.timerRunning = false;
this.game = new Mines(width, height, mines, this.randomMode);
this.buttons.clear();
this.flagLabel = new Label("Flags: " + this.flagsPlaced + " / " + this.game.getTotalMines());
this.timerLabel = new Label("Time: " + this.seconds);
final MinesScore bestScore;
final String difficulty;
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);
bestLabel.setFont(FONT);
topBar = new VBox(
TOPBAR_SPACING,
this.flagLabel,
this.timerLabel,
bestLabel
);
topBar.setAlignment(Pos.CENTER);
grid = createGrid(width, height);
root = new VBox();
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(GAME_PADDING));
root.getChildren().addAll(topBar, grid);
scene = new Scene(root, WINDOW_WIDTH, WINDOW_HEIGHT);
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;
}
/**
* createGrid generates the button grid
* that represents the minefield of the
* given dimensions
* @param width of minefield grid
* @param height of minefield grid
* @return GridPane containing buttons of minefield
*/
private GridPane createGrid(final int width, final int height)
{
final GridPane grid;
grid = new GridPane();
grid.setPadding(new Insets(GAME_PADDING));
grid.setHgap(GAME_PADDING);
grid.setVgap(GAME_PADDING);
grid.setAlignment(Pos.CENTER);
for (int j = 0; j < height; j++)
{
for (int i = 0; i < width; i++)
{
final int index;
final Button button;
index = (j * width) + i;
button = new Button();
button.setFont(FONT);
button.setPrefSize(BUTTON_WIDTH, BUTTON_HEIGHT);
button.setStyle(BUTTON_THEMES.get(DEFAULT_BUTTON_KEY));
button.setOnMouseEntered(e -> button.setCursor(javafx.scene.Cursor.HAND));
button.setOnMouseExited(e -> button.setCursor(javafx.scene.Cursor.DEFAULT));
button.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY)
{
handleReveal(index);
}
else if (e.getButton() == MouseButton.SECONDARY)
{
handleFlag(index);
}
});
grid.add(button, i, j);
this.buttons.add(button);
}
}
return grid;
}
/**
* handleReveal reveals the given cell in the
* minefield ensures the timer is running if first
* reveal, call randomize on board if random mode
* enabled, and refresh all button UI elements.
* @param index to reveal
*/
private void handleReveal(final int index)
{
if (!timerRunning)
{
startTimer();
}
try
{
final boolean hitMine;
hitMine = this.game.reveal(index);
if (hitMine)
{
handleLoss();
return;
}
if (this.game.isRandomMode())
{
this.game.randomizeRemaining();
}
refreshAllButtons();
if (this.game.hasWon())
{
handleWin();
return;
}
}
catch (final InvalidMoveException e)
{
final Alert warn = new Alert(Alert.AlertType.WARNING);
warn.setHeaderText("Invalid move");
warn.setContentText(e.getMessage());
warn.showAndWait();
}
}
/**
* handleFlag updates the given cell to be
* flagged, questions, or back to no flag.
* @param index to flag
*/
private void handleFlag(final int index)
{
final int newState;
newState = this.game.toggleFlag(index);
if (newState == Mines.FLAG)
{
this.flagsPlaced++;
}
else if (newState == Mines.FLAG_QUESTION)
{
this.flagsPlaced--;
}
this.flagLabel.setText("Flags: " +
this.flagsPlaced +
" / " +
this.game.getTotalMines());
updateButtonDisplay(index);
}
/**
* updateButtonDisplay updates a given button
* by index depending on how the cell is configured.
* Flagged, questioned, or revealed.
* @param index of cell to update button
*/
private void updateButtonDisplay(final int index)
{
final Button button;
button = this.buttons.get(index);
if (this.game.isFlagged(index))
{
button.setText("F");
button.setStyle(BUTTON_THEMES.get(DEFAULT_BUTTON_KEY));
return;
}
if (this.game.isQuestionMarked(index))
{
button.setText("?");
button.setStyle(BUTTON_THEMES.get(DEFAULT_BUTTON_KEY));
return;
}
if (!this.game.isRevealed(index))
{
button.setText("");
button.setStyle(BUTTON_THEMES.get(DEFAULT_BUTTON_KEY));
return;
}
final int value;
value = this.game.getFieldValue(index);
if (value == MINE_KEY)
{
button.setText("*");
button.setStyle(BUTTON_THEMES.get(MINE_KEY));
}
else if (value == ZERO_KEY)
{
button.setText(" ");
button.setStyle(BUTTON_THEMES.get(ZERO_KEY));
}
else
{
button.setText(Integer.toString(value));
button.setStyle(BUTTON_THEMES.get(value));
}
}
/**
* refreshAllButtons iterates over all buttons
* and calls updateButtonDisplay for that button,
* as well as disables button if revealed, preventing
* further clicking on the button.
*/
private void refreshAllButtons()
{
for (int i = 0; i < this.buttons.size(); i++)
{
updateButtonDisplay(i);
if (this.game.isRevealed(i))
{
disableButton(this.buttons.get(i));
}
}
}
/**
* startTimer when initial cell is revealed to
* track the game time.
*/
private void startTimer()
{
final KeyFrame tick;
seconds = STARTING_SECONDS;
this.timerLabel.setText("Time: " + seconds);
tick = new KeyFrame(Duration.seconds(TIMER_TICK_SECONDS), e -> {
this.seconds++;
this.timerLabel.setText("Time: " + this.seconds + "s");
});
this.timer = new Timeline(tick);
this.timer.setCycleCount(Animation.INDEFINITE);
this.timer.play();
this.timerRunning = true;
}
/**
* stopTime once game is over, either lost or won.
*/
private void stopTimer()
{
if (this.timer != null)
{
this.timer.stop();
}
this.timerRunning = false;
}
/**
* handleWin displays a win message window, with the time
* taken to win the game, show all mines, and disable all
* buttons.
*/
private void handleWin()
{
final Alert winAlert;
final MinesScore score;
final String difficulty;
final int mines;
stopTimer();
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(winMessage.toString());
winAlert.showAndWait();
this.gameStage.close();
}
/**
* handleLoss displays a loss message, reveals all
* mines and stops timer. Forces user to restart game.
*/
private void handleLoss()
{
final Alert lossAlert;
stopTimer();
disableAllButtons();
showAllMines();
lossAlert = new Alert(Alert.AlertType.ERROR);
lossAlert.setHeaderText("You lost...");
lossAlert.setContentText("You dug up a mine and lost your legs.");
lossAlert.showAndWait();
this.gameStage.close();
}
/**
* showAllMines shows all mine locations for when
* a user wins or loses.
*/
private void showAllMines()
{
for (int i = 0; i < this.buttons.size(); i++)
{
if (this.game.isMine(i))
{
final Button button;
button = this.buttons.get(i);
button.setText("*");
button.setStyle(BUTTON_THEMES.get(MINE_KEY));
}
}
}
/**
* disableAllButtons iterates over all buttons
* and calls the disableButton method on it.
*/
private void disableAllButtons()
{
final Iterator<Button> iterator;
iterator = this.buttons.iterator();
while (iterator.hasNext())
{
final Button button;
button = iterator.next();
disableButton(button);
}
}
/**
* disableButton prevents user from
* interacting with it by clicking.
* @param button to disable
*/
private void disableButton(final Button button)
{
button.setMouseTransparent(true);
button.setFocusTraversable(false);
}
public Stage getGameStage()
{
return this.gameStage;
}
}