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

537 lines
16 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.util.Random;
import java.util.function.Consumer;
public class Mines extends Application
{
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;
private static final int MIN_OFFSET = -1;
private static final int MAX_OFFSET = 1;
private static final int SELF_OFFSET = 0;
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;
private static final int DEFAULT_BUTTON = -2;
private static final Map<Integer, String> BUTTON_THEMES = new HashMap<>();
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 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 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;
}
private void stopTimer() {
if (timer != null) {
timer.stop();
}
timerRunning = false;
}
@Override
public void start(final Stage stage)
{
final Label title;
final Button smallBtn;
final Button largeBtn;
title = new Label("Select Game Size");
title.setStyle("-fx-font-size: 20px; -fx-font-weight: bold;");
smallBtn = new Button("8 × 8 (Beginner)");
largeBtn = new Button("36 × 16 (Expert)");
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));
final VBox root;
final Scene scene;
root = new VBox(VERTICAL_MARGIN);
scene = new Scene(root, WINDOW_WIDTH, WINDOW_HEIGHT);
root.setPadding(new Insets(PADDING));
root.setAlignment(Pos.CENTER);
root.getChildren().addAll(title, smallBtn, largeBtn);
stage.setTitle("Random Mines - A Minesweeper Game");
stage.setResizable(false);
stage.setScene(scene);
stage.show();
}
private void forEachNeighbor(
final int index,
final Consumer<Integer> action
) {
final int row;
final int col;
row = index / width;
col = index % width;
for (int dr = MIN_OFFSET; dr <= MAX_OFFSET; dr++)
{
for (int dc = MIN_OFFSET; dc <= MAX_OFFSET; dc++)
{
if (dr == SELF_OFFSET && dc == SELF_OFFSET)
{
continue;
}
final int nr;
final int nc;
nr = row + dr;
nc = col + dc;
if (nr < FIRST_ROW || nr >= height ||
nc < FIRST_COL || nc >= width)
{
continue;
}
final int neighborIndex;
neighborIndex = nr * 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);
if (this.field[neighborIndex] == NO_MINE)
{
popFieldVoid(neighborIndex);
}
}
});
}
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)
{
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.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;
}
}
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)
{
popFieldVoid(index);
}
return this.field[index] == MINE;
}
public static void main(final String[] args)
{
launch(args);
}
}