impliment something from each lesson

This commit is contained in:
SowinskiBraeden committed 2025-11-27 11:56:48 -08:00
1 parent d5692ab1e9
commit 6c2255ea1c
7 files changed
+903 -433

No files matched your search

View File
Whitespace-only changes.
@@ -0,0 +1,25 @@
package ca.bcit.comp2522.project;
public abstract class GameBoard
{
protected int width;
protected int height;
protected GameBoard(final int width, final int height)
{
this.width = width;
this.height = height;
}
public final int getWidth()
{
return this.width;
}
public final int getHeight()
{
return this.height;
}
public abstract void reset();
}
@@ -0,0 +1,9 @@
package ca.bcit.comp2522.project;
public class InvalidMoveException extends Exception
{
public InvalidMoveException(final String message)
{
super(message);
}
}
+312 -433
View File
@@ -1,45 +1,17 @@
package ca.bcit.comp2522.project;
import javafx.animation.Timeline;
import javafx.application.Application;
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.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;
import java.util.HashMap;
import java.util.Map;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Random;
import java.util.Scanner;
import java.util.function.Consumer;
public class Mines extends Application
public class Mines extends GameBoard
{
private static final int FONT_SIZE = 18;
private static final Font FONT = Font.font("Arial", FontWeight.BOLD, FONT_SIZE);
private static final int WINDOW_WIDTH = 1920;
private static final int WINDOW_HEIGHT = 1080;
private static final int BUTTON_WIDTH = 60;
private static final int BUTTON_HEIGHT = 60;
private static final int MENU_BUTTON_WIDTH = 180;
private static final int EASY_WIDTH = 8;
private static final int EASY_HEIGHT = 8;
private static final int HARD_WIDTH = 36;
private static final int HARD_HEIGHT = 16;
private static final int PADDING = 20;
private static final int GAME_PADDING = 5;
private static final int VERTICAL_MARGIN = 15;
private static final int EASY_MINES = 10;
private static final int HARD_MINES = 99;
private static final int MINE = -1;
private static final int NO_MINE = 0;
@@ -49,109 +21,131 @@ public class Mines extends Application
private static final int FIRST_ROW = 0;
private static final int FIRST_COL = 0;
private static final int NO_FLAG = 0;
private static final int FLAG = 1;
private static final int FLAG_QUESTION = 2;
public static final int NO_FLAG = 0;
public static final int FLAG = 1;
public static final int FLAG_QUESTION = 2;
private static final int DEFAULT_BUTTON = -2;
private static final Map<Integer, String> BUTTON_THEMES = new HashMap<>();
private static final int INITIAL_BEST_SCORE = Integer.MAX_VALUE;
static {
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 static final String SCORE_FILE_NAME = "scores.txt";
private static final Random RANDOM_GENERATOR;
static
{
RANDOM_GENERATOR = new Random();
}
private final int totalMines;
private boolean randomMode;
private int[] field;
private boolean[] revealed;
private int[] flagged;
private Button[] buttons;
private int width;
private int height;
private int flags;
private Label flagLabel;
private Label timerLabel;
private int totalMines;
private int bestScoreSeconds;
private int seconds;
private Timeline timer;
private boolean timerRunning = false;
private boolean randomMode = false;
private void startTimer() {
seconds = 0;
timerLabel.setText("Time: 0");
timer = new javafx.animation.Timeline(
new javafx.animation.KeyFrame(javafx.util.Duration.seconds(1), e -> {
seconds++;
timerLabel.setText("Time: " + seconds);
})
);
timer.setCycleCount(javafx.animation.Animation.INDEFINITE);
timer.play();
timerRunning = true;
public Mines(
final int width,
final int height,
final int mines,
final boolean randomMode
) {
super(width, height);
this.totalMines = mines;
this.randomMode = randomMode;
this.bestScoreSeconds = INITIAL_BEST_SCORE;
reset();
loadBestScore();
}
private void stopTimer() {
if (timer != null) {
timer.stop();
}
timerRunning = false;
public void startNewGame()
{
reset();
}
public void startNewGame(final boolean randomMode)
{
this.randomMode = randomMode;
reset();
}
public void setRandomMode(final boolean randomMode)
{
this.randomMode = randomMode;
}
public boolean isRandomMode()
{
return this.randomMode;
}
@Override
public void start(final Stage stage)
public void reset()
{
final Label title;
final Button smallBtn;
final Button largeBtn;
final int totalCells;
title = new Label("Select Game Size");
title.setStyle("-fx-font-size: 20px; -fx-font-weight: bold;");
totalCells = this.width * this.height;
smallBtn = new Button("8 × 8 (Beginner)");
largeBtn = new Button("36 × 16 (Expert)");
this.field = new int[totalCells];
this.revealed = new boolean[totalCells];
this.flagged = new int[totalCells];
smallBtn.setPrefWidth(MENU_BUTTON_WIDTH);
largeBtn.setPrefWidth(MENU_BUTTON_WIDTH);
smallBtn.setOnAction(e -> startGame(EASY_WIDTH, EASY_HEIGHT, EASY_MINES));
largeBtn.setOnAction(e -> startGame(HARD_WIDTH, HARD_HEIGHT, HARD_MINES));
generateField();
}
final VBox root;
final Scene scene;
private void generateField()
{
int placedMines;
final int totalCells;
root = new VBox(VERTICAL_MARGIN);
scene = new Scene(root, WINDOW_WIDTH, WINDOW_HEIGHT);
placedMines = NO_MINE;
totalCells = this.width * this.height;
root.setPadding(new Insets(PADDING));
root.setAlignment(Pos.CENTER);
root.getChildren().addAll(title, smallBtn, largeBtn);
while (placedMines < totalMines)
{
final int index;
stage.setTitle("Random Mines - A Minesweeper Game");
stage.setResizable(false);
stage.setScene(scene);
stage.show();
index = RANDOM_GENERATOR.nextInt(totalCells);
if (this.field[index] != MINE)
{
this.field[index] = MINE;
placedMines++;
}
}
for (int i = 0; i < this.field.length; i++)
{
if (this.field[i] == MINE)
{
continue;
}
final int[] count;
count = new int[] { NO_MINE };
forEachNeighbor(i, neighborIndex -> {
if (this.field[neighborIndex] == MINE)
{
count[SELF_OFFSET]++;
}
});
this.field[i] = count[SELF_OFFSET];
}
}
private void forEachNeighbor(
final int index,
final int index,
final Consumer<Integer> action
) {
final int row;
final int col;
row = index / width;
col = index % width;
row = index / this.width;
col = index % this.width;
for (int dr = MIN_OFFSET; dr <= MAX_OFFSET; dr++)
{
@@ -168,70 +162,34 @@ public class Mines extends Application
nr = row + dr;
nc = col + dc;
if (nr < FIRST_ROW || nr >= height ||
nc < FIRST_COL || nc >= width)
if (nr < FIRST_ROW || nr >= this.height ||
nc < FIRST_COL || nc >= this.width)
{
continue;
}
final int neighborIndex;
neighborIndex = nr * width + nc;
neighborIndex = nr * this.width + nc;
action.accept(neighborIndex);
}
}
}
private void generateField(final int mines)
{
this.revealed = new boolean[width * height];
this.field = new int[width * height];
this.flagged = new int[width * height];
final Random rand;
int placed;
rand = new Random();
placed = NO_MINE;
while (placed < mines)
{
final int index;
index = rand.nextInt(width * height);
if (this.field[index] != MINE)
{
this.field[index] = MINE;
placed++;
}
}
for (int i = 0; i < this.field.length; i++)
{
if (this.field[i] == MINE)
{
continue;
}
final int[] count = { NO_MINE };
forEachNeighbor(i, neighborIndex -> {
if (this.field[neighborIndex] == MINE)
{
count[SELF_OFFSET]++;
}
});
this.field[i] = count[SELF_OFFSET];
}
}
private void popFieldVoid(final int index)
{
forEachNeighbor(index, neighborIndex -> {
if (!this.revealed[neighborIndex])
{
reveal(neighborIndex);
try
{
reveal(neighborIndex);
}
catch (final InvalidMoveException e)
{
// revealing neighbors should never be invalid
}
if (this.field[neighborIndex] == NO_MINE)
{
@@ -241,284 +199,14 @@ public class Mines extends Application
});
}
private void startGame(
final int width,
final int height,
final int mines
) {
this.flags = NO_FLAG;
this.buttons = new Button[width * height];
this.width = width;
this.height = height;
this.totalMines = mines;
generateField(mines);
final Stage gameStage;
final VBox box;
final VBox topBar;
final GridPane grid;
final Label modeLabel;
final Button toggleModeBtn;
gameStage = new Stage();
box = new VBox();
grid = createGrid(width, height);
modeLabel = new Label("Random Mode: OFF");
toggleModeBtn = new Button("Toggle Random Mode");
this.flagLabel = new Label("Flags: 0 / " + totalMines);
this.timerLabel = new Label("Time: 0");
this.flagLabel.setFont(FONT);
this.timerLabel.setFont(FONT);
toggleModeBtn.setOnAction(e -> {
randomMode = !randomMode;
modeLabel.setText("Random Mode: " + (randomMode ? "ON" : "OFF"));
});
topBar = new VBox(5, flagLabel, timerLabel);
topBar.setAlignment(Pos.CENTER);
box.getChildren().add(0, topBar);
box.getChildren().add(grid);
box.setAlignment(Pos.CENTER);
grid.setAlignment(Pos.CENTER);
final Scene scene;
scene = new Scene(box, WINDOW_WIDTH, WINDOW_HEIGHT);
root.getChildren().addAll(title, smallBtn, largeBtn, modeLabel, toggleModeBtn);
gameStage.setScene(scene);
gameStage.setTitle("Random Mines " + width + "x" + height);
gameStage.show();
startTimer();
}
private void flag(final int index)
public boolean reveal(final int index)
throws InvalidMoveException
{
this.flagged[index] = (this.flagged[index] + FLAG) % (FLAG_QUESTION + FLAG);
final String buttonText;
buttonText = this.flagged[index] == FLAG ? "F" :
this.flagged[index] == FLAG_QUESTION ? "?" : "";
this.buttons[index].setText(buttonText);
this.flags = this.flagged[index] == FLAG ?
this.flags + FLAG :
this.flagged[index] == FLAG_QUESTION ?
this.flags - FLAG :
this.flags;
this.flagLabel.setText("Flags: " + flags + " / " + totalMines);
}
private void checkWin()
{
int numRevealed;
numRevealed = NO_MINE;
for (int i = 0; i < this.field.length; i++)
if (this.flagged[index] == FLAG)
{
if (this.revealed[i])
{
numRevealed++;
}
else if (this.field[i] != MINE)
{
// if a non revealed cell is not a mine, we know its not a win
return;
}
else
{
// do nothing
continue;
}
throw new InvalidMoveException("Cannot reveal a flagged cell.");
}
if (numRevealed == (this.field.length - totalMines))
{
// WIN CONDITION
stopTimer();
final Alert win;
win = new Alert(Alert.AlertType.INFORMATION);
win.setHeaderText("You Win!");
win.setContentText("You successfully cleared all safe squares!");
win.showAndWait();
((Stage) buttons[0].getScene().getWindow()).close();
}
}
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);
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
final Button button;
final int index;
index = (j * width) + i;
button = new Button();
button.setFont(FONT);
button.setPrefSize(BUTTON_WIDTH, BUTTON_HEIGHT);
button.setStyle(BUTTON_THEMES.get(DEFAULT_BUTTON));
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)
{
final boolean lossed;
lossed = reveal(index);
if (lossed)
{
final Alert loss;
loss = new Alert(Alert.AlertType.ERROR);
loss.setHeaderText("You lost...");
loss.setContentText("Unfortunately, you dug up a mine and lost your legs. I hear there is a deal on wheelchairs though.");
loss.showAndWait();
final Stage gameStage;
gameStage = (Stage) buttons[index].getScene().getWindow();
gameStage.close();
return;
}
checkWin();
randomizeRemaining();
}
else if (e.getButton() == MouseButton.SECONDARY)
{
flag(index);
}
else
{
// do nothing
return;
}
});
grid.add(button, i, j);
this.buttons[index] = button;
}
}
return grid;
}
private void randomizeRemaining()
{
int regenMine;
regenMine = NO_MINE;
for (int i = 0; i < this.field.length; i++)
{
// Ignore correctly flagged mines
if (this.field[i] == MINE && this.flagged[i] == FLAG)
{
continue;
}
if (this.field[i] == MINE)
{
regenMine++;
}
this.field[i] = NO_MINE;
}
final Random rand;
int placed;
rand = new Random();
placed = NO_MINE;
while (placed < regenMine)
{
final int index;
index = rand.nextInt(width * height);
if (this.field[index] == NO_MINE && !this.revealed[index])
{
this.field[index] = MINE;
placed++;
}
}
for (int i = 0; i < this.field.length; i++)
{
if (this.field[i] == MINE)
{
continue;
}
final int[] count = { NO_MINE };
forEachNeighbor(i, neighborIndex -> {
if (this.field[neighborIndex] == MINE)
{
count[SELF_OFFSET]++;
}
});
this.field[i] = count[SELF_OFFSET];
}
for (int i = 0; i < this.field.length; i++)
{
if (this.revealed[i])
{
this.buttons[i].setText(this.field[i] == NO_MINE ? "" : "" + this.field[i]);
this.buttons[i].setStyle(BUTTON_THEMES.get(this.field[i]));
}
if (this.revealed[i] && this.field[i] == NO_MINE)
{
popFieldVoid(i);
}
}
}
private boolean reveal(final int index)
{
if (this.revealed[index])
{
return false;
}
final String buttonText;
final Button button;
button = this.buttons[index];
buttonText = "" + (this.field[index] == MINE ? "*" :
this.field[index] == NO_MINE ? " " :
this.field[index]);
button.setText(buttonText);
button.setStyle(BUTTON_THEMES.get(this.field[index]));
button.setMouseTransparent(true);
button.setFocusTraversable(false);
this.revealed[index] = true;
if (this.field[index] == NO_MINE)
@@ -529,8 +217,199 @@ public class Mines extends Application
return this.field[index] == MINE;
}
public static void main(final String[] args)
public int toggleFlag(final int index)
{
launch(args);
final int nextState;
nextState = (this.flagged[index] + FLAG) % (FLAG_QUESTION + FLAG);
this.flagged[index] = nextState;
return nextState;
}
public int getFieldValue(final int index)
{
return this.field[index];
}
public boolean isRevealed(final int index)
{
return this.revealed[index];
}
public boolean isMine(final int index)
{
return this.field[index] == MINE;
}
public boolean isFlagged(final int index)
{
return this.flagged[index] == FLAG;
}
public boolean isQuestionMarked(final int index)
{
return this.flagged[index] == FLAG_QUESTION;
}
public int getTotalMines()
{
return this.totalMines;
}
public boolean hasWon()
{
int revealedCount;
revealedCount = NO_MINE;
for (int i = 0; i < this.field.length; i++)
{
if (this.revealed[i])
{
revealedCount++;
}
else if (this.field[i] != MINE)
{
return false;
}
}
return revealedCount == (this.field.length - this.totalMines);
}
public void randomizeRemaining()
{
if (!this.randomMode)
{
return;
}
int minesToReplant;
minesToReplant = NO_MINE;
for (int i = 0; i < this.field.length; i++)
{
if (this.field[i] == MINE && this.flagged[i] == FLAG)
{
continue;
}
if (this.field[i] == MINE)
{
minesToReplant++;
}
this.field[i] = NO_MINE;
}
while (minesToReplant > NO_MINE)
{
final int index;
index = RANDOM_GENERATOR.nextInt(this.width * this.height);
if (this.field[index] == NO_MINE && !this.revealed[index])
{
this.field[index] = MINE;
minesToReplant--;
}
}
for (int i = 0; i < this.field.length; i++)
{
if (this.field[i] == MINE)
{
continue;
}
final int[] count;
count = new int[] { NO_MINE };
forEachNeighbor(i, neighborIndex -> {
if (this.field[neighborIndex] == MINE)
{
count[SELF_OFFSET]++;
}
});
this.field[i] = count[SELF_OFFSET];
}
for (int i = 0; i < this.field.length; i++)
{
if (this.revealed[i] && this.field[i] == NO_MINE)
{
popFieldVoid(i);
}
}
}
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;
}
}
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;
}
public int getBestScoreSeconds()
{
return this.bestScoreSeconds;
}
}
@@ -0,0 +1,471 @@
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.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
public class MinesUI
{
private static final int FONT_SIZE = 18;
private static final Font FONT = Font.font("Arial", FontWeight.BOLD, FONT_SIZE);
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 HARD_WIDTH = 36;
private static final int HARD_HEIGHT = 16;
private static final int HARD_MINES = 99;
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 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 Label flagLabel;
private Label timerLabel;
private Label bestLabel;
private boolean randomMode;
private int flagsPlaced;
private int seconds;
private Timeline timer;
private boolean timerRunning;
public MinesUI()
{
this.buttons = new ArrayList<>();
this.randomMode = false;
this.timerRunning = false;
}
public void showMainMenu(final Stage primaryStage)
{
final Label titleLabel;
final Button easyButton;
final Button hardButton;
final Label modeLabel;
final Button toggleModeButton;
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)");
hardButton = new Button("36 x 16 (Expert)");
easyButton.setPrefWidth(MENU_BUTTON_WIDTH);
easyButton.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");
toggleModeButton.setOnAction(e -> {
this.randomMode = !this.randomMode;
modeLabel.setText("Random Mode: " + (this.randomMode ? "ON" : "OFF"));
});
easyButton.setOnAction(e -> startGame(EASY_WIDTH, EASY_HEIGHT, EASY_MINES, primaryStage));
hardButton.setOnAction(e -> startGame(HARD_WIDTH, HARD_HEIGHT, HARD_MINES, primaryStage));
root = new VBox(VERTICAL_MARGIN);
root.setPadding(new Insets(PADDING));
root.setAlignment(Pos.CENTER);
root.getChildren().addAll(titleLabel, easyButton, hardButton, modeLabel, toggleModeButton);
scene = new Scene(root, WINDOW_WIDTH, WINDOW_HEIGHT);
primaryStage.setTitle(TITLE_TEXT);
primaryStage.setResizable(false);
primaryStage.setScene(scene);
primaryStage.show();
}
private void startGame(
final int width,
final int height,
final int mines,
final Stage ownerStage
) {
final Stage gameStage;
final VBox root;
final VBox topBar;
final GridPane grid;
final Scene scene;
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 int bestScore;
bestScore = this.game.getBestScoreSeconds();
if (bestScore == Integer.MAX_VALUE)
{
this.bestLabel = new Label("Best: -");
}
else
{
this.bestLabel = new Label("Best: " + bestScore + "s");
}
this.flagLabel.setFont(FONT);
this.timerLabel.setFont(FONT);
this.bestLabel.setFont(FONT);
topBar = new VBox(
TOPBAR_SPACING,
this.flagLabel,
this.timerLabel,
this.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);
gameStage = new Stage();
gameStage.initOwner(ownerStage);
gameStage.setTitle("Random Mines " + width + "x" + height);
gameStage.setScene(scene);
gameStage.show();
}
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;
}
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();
}
}
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);
}
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));
}
}
private void refreshAllButtons()
{
for (int i = 0; i < this.buttons.size(); i++)
{
updateButtonDisplay(i);
if (this.game.isRevealed(i))
{
disableButton(this.buttons.get(i));
}
}
}
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;
}
private void stopTimer()
{
if (this.timer != null)
{
this.timer.stop();
}
this.timerRunning = false;
}
private void handleWin()
{
final Alert winAlert;
stopTimer();
this.game.saveScore(this.seconds);
this.bestLabel.setText("Best: " + this.game.getBestScoreSeconds() + "s");
disableAllButtons();
winAlert = new Alert(Alert.AlertType.INFORMATION);
winAlert.setHeaderText("You Win!");
winAlert.setContentText("You successfully cleared all safe squares!");
winAlert.showAndWait();
}
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. Better luck next time.");
lossAlert.showAndWait();
}
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));
}
}
}
private void disableAllButtons()
{
final Iterator<Button> iterator;
iterator = this.buttons.iterator();
while (iterator.hasNext())
{
final Button button;
button = iterator.next();
disableButton(button);
}
}
private void disableButton(final Button button)
{
button.setMouseTransparent(true);
button.setFocusTraversable(false);
}
}
@@ -0,0 +1,25 @@
package ca.bcit.comp2522.project;
import javafx.application.Application;
import javafx.stage.Stage;
/**
* Main entry point for the Minesweeper game.
* Lesson 9: JavaFX GUI entry class.
*/
public class MyGame extends Application
{
@Override
public void start(final Stage primaryStage)
{
final MinesUI ui;
ui = new MinesUI();
ui.showMainMenu(primaryStage);
}
public static void main(final String[] args)
{
launch(args);
}
}
@@ -0,0 +1,61 @@
package ca.bcit.comp2522.project.tests;
import ca.bcit.comp2522.project.Mines;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Lesson 10: unit tests for MinesGame.
*/
public class MinesTest
{
private static final int TEST_WIDTH = 5;
private static final int TEST_HEIGHT = 5;
private static final int TEST_MINES = 3;
@Test
public void newGameHasCorrectMineCount()
{
final Mines game;
int mineCount;
game = new Mines(TEST_WIDTH, TEST_HEIGHT, TEST_MINES, false);
mineCount = 0;
for (int i = 0; i < TEST_WIDTH * TEST_HEIGHT; i++)
{
if (game.isMine(i))
{
mineCount++;
}
}
assertEquals(TEST_MINES, mineCount);
}
@Test
public void revealNonMineDoesNotLose()
throws Exception
{
final Mines game;
int safeIndex;
game = new Mines(TEST_WIDTH, TEST_HEIGHT, TEST_MINES, false);
safeIndex = -1;
for (int i = 0; i < TEST_WIDTH * TEST_HEIGHT; i++)
{
if (!game.isMine(i))
{
safeIndex = i;
break;
}
}
assertNotEquals(-1, safeIndex);
assertFalse(game.reveal(safeIndex));
assertTrue(game.isRevealed(safeIndex));
}
}