Compare commits

..

10 Commits

Author SHA1 Message Date
d681f20b90
Update JavaDoc 2024-12-16 03:15:16 +00:00
ef569e1bcb
Revert "Update JavaDoc"
This reverts commit 7708654bdb51864fdd7cada0c2d4223e441bc707.
2024-12-16 03:07:48 +00:00
74ba71d52c
Fix tempramental width management on non minesweeper stages 2024-12-16 03:00:41 +00:00
7708654bdb
Update JavaDoc 2024-07-29 17:05:28 +01:00
004fe776eb Merge branch 'master' of github.com:shrapnelnet/libremines 2024-07-28 23:12:06 +01:00
c2c5ac6e68
Fix numberOfUnrevealedTiles logic 2024-07-28 23:11:44 +01:00
0198ba6e56
Add JavaDocs 2024-07-28 23:11:22 +01:00
shr4pnel
f5ca09cc67
Update screenshot, better build info 2024-07-27 23:36:02 +01:00
1187bedc5b
Use LTS JavaFX 2024-07-24 21:00:08 +01:00
21fa946011
Add unimplemented, about pages 2024-07-24 20:59:50 +01:00
11 changed files with 478 additions and 39 deletions

View File

@ -2,35 +2,33 @@
"named this way because i didn't realise there was another libremines!" "named this way because i didn't realise there was another libremines!"
![Libremines screenshot](https://github.com/shrapnelnet/libremines/assets/133451255/f19e0006-a587-4f39-8626-67606db7cd58) ![Libremines screenshot](https://github.com/user-attachments/assets/e0a4e551-afdf-4fc5-91c5-7ff45d36cb64)
## Building ## Building
### Requirements: ### Requirements:
- Java 21 or newer (openjdk-21-jdk) - Java 21
- Maven 3.8.8 (note: some versions cause gluon to freak out) - Maven 3.8.8 or lower
- GraalVM 21 - GraalVM 21
### Instructions: ### Instructions:
Create the maven wrapper. gluonfx does not like versions of maven that are not 3.8.8: Create the maven wrapper at version 3.8.8. GluonFX does not work with newer versions as of 2024-07-27.
```shell ```shell
mvn wrapper:wrapper -Dmaven=3.8.8 mvn wrapper:wrapper -Dmaven=3.8.8
``` ```
This is helpful for version management, in case you have an incompatible version of maven installed. Install dependencies, build and run:
Install dependencies:
```shell ```shell
./mvnw install ./mvnw dependency:resolve gluonfx:build
```
Build and run:
```shell
GRAALVM_HOME=/path/to/graalvm ./mvnw gluonfx:build
``` ```
A native binary should be placed at `target/gluonfx/<your architecture>/libremines` A native binary should be placed at `target/gluonfx/<your architecture>/libremines`
Alternatively, to run using the JVM, use:
```shell
./mvnw javafx:run
```

14
pom.xml
View File

@ -6,31 +6,23 @@
<groupId>com.shr4pnel.minesweeper</groupId> <groupId>com.shr4pnel.minesweeper</groupId>
<artifactId>libremines</artifactId> <artifactId>libremines</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0</version>
<properties> <properties>
<maven.compiler.source>21</maven.compiler.source> <maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target> <maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
<profiles>
<profile>
<id>web</id>
<properties>
<gluonfx.target>web</gluonfx.target>
</properties>
</profile>
</profiles>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId> <artifactId>javafx-controls</artifactId>
<version>21.0.3</version> <version>21.0.4</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId> <artifactId>javafx-fxml</artifactId>
<version>21.0.3</version> <version>21.0.4</version>
</dependency> </dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -1,55 +1,167 @@
package com.shr4pnel.minesweeper; package com.shr4pnel.minesweeper;
import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.MenuItem;
import javafx.scene.control.RadioMenuItem; import javafx.scene.control.RadioMenuItem;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton; import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
/**
* The JavaFX controller for minesweeper.fxml.
* Handles all operations performed within the GUI.
* @author shrapnelnet admin@shr4pnel.com
*/
public class Controller { public class Controller {
/**
* The grid within the FXML holding all the tiles.
*/
@FXML @FXML
private GridPane grid; private GridPane grid;
/**
* Images within the FXML structure.
*/
@FXML @FXML
private ImageView smiley, time_1, time_2, time_3, bomb_2, bomb_3; private ImageView smiley, time_1, time_2, time_3, bomb_2, bomb_3;
/**
* Menu items
*/
@FXML @FXML
private RadioMenuItem color, marks; private RadioMenuItem color, marks;
/**
* The menu item "about".
*/
@FXML
private MenuItem about;
/**
* The underlying 2D array representing where bombs are on the grid.
*/
private Grid gridHandler; private Grid gridHandler;
/**
* A wrapper to make operations on the underlying grid easier.
*/
private GridWrapper wrapper; private GridWrapper wrapper;
/**
* Handles the grid during a game over state preventing further clicking of the grid.
*/
private boolean gameOver = false; private boolean gameOver = false;
/**
* A variable specifying if the game has been loaded for the first time.
*/
private boolean isFirstLoad = true; private boolean isFirstLoad = true;
/**
* The game timer.
*/
static Timer timer = new Timer(); static Timer timer = new Timer();
/**
* The time that has elapsed since the game started.
*/
private int time = 0; private int time = 0;
/**
* The time the game was started at.
*/
private long startTime; private long startTime;
/**
* The number of bombs on the grid.
*/
private int bombCount = 99; private int bombCount = 99;
/**
* A 2D array holding all tiles that have previously recursively been expanded.
*/
private boolean[][] expandedTiles; private boolean[][] expandedTiles;
/**
* A boolean that shows if the user has performed their first click on the GridPane.
*/
private boolean isFirstClick = true; private boolean isFirstClick = true;
/**
* Sets important variables on Main class initialization
*/
@FXML @FXML
private void initialize() { private void initialize() {
setNotYetImplemented(color); setNotYetImplemented(color);
setNotYetImplemented(marks); setNotYetImplemented(marks);
about.setOnAction(this::openAbout);
setupGrid(); setupGrid();
gridHandler = new Grid(); gridHandler = new Grid();
wrapper = gridHandler.grid; wrapper = gridHandler.grid;
expandedTiles = new boolean[30][16]; expandedTiles = new boolean[30][16];
} }
private void setNotYetImplemented(RadioMenuItem node) { /**
node.setOnAction((ActionEvent e) -> System.out.println("https://http.cat/images/501.jpg")); * Opens the about window when the radiomenuitem is clicked
*
* @param actionEvent The node that the window opens on, on click
*/
private void openAbout(ActionEvent actionEvent) {
try {
Parent root = FXMLLoader.load(getClass().getResource("about.fxml"));
Stage stage = new Stage();
stage.setTitle("About");
stage.setMinWidth(455);
stage.setMinHeight(275);
stage.setScene(new Scene(root));
stage.show();
} catch (IOException e) {
e.printStackTrace();
}
} }
/**
* Opens a window if an unimplemented menu item is clicked
*
* @param node The node that the window opens on, on click
*/
private void setNotYetImplemented(RadioMenuItem node) {
node.setOnAction(new EventHandler<>() {
@Override
public void handle(ActionEvent actionEvent) {
try {
Parent root = FXMLLoader.load(getClass().getResource("unimplemented.fxml"));
Stage stage = new Stage();
stage.setTitle("Unimplemented!");
stage.setMinHeight(420);
stage.setMinWidth(525);
stage.setScene(new Scene(root));
stage.show();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
/**
* Fills the GridPane with blank buttons
*/
private void setupGrid() { private void setupGrid() {
for (int column = 0; column < 30; ++column) { for (int column = 0; column < 30; ++column) {
for (int row = 0; row < 16; ++row) { for (int row = 0; row < 16; ++row) {
@ -59,9 +171,13 @@ public class Controller {
} }
} }
/**
* Used during initialization to fill the board
*
* @return A blank button
*/
private Button createBlankButton() { private Button createBlankButton() {
Image blank = Image blank = new Image(String.valueOf(getClass().getResource("img/blank.png")), 16, 16, true, false);
new Image(String.valueOf(getClass().getResource("img/blank.png")), 16, 16, true, true);
ImageView blankImage = new ImageView(blank); ImageView blankImage = new ImageView(blank);
Button blankButton = new Button(); Button blankButton = new Button();
blankButton.setGraphic(blankImage); blankButton.setGraphic(blankImage);
@ -72,18 +188,34 @@ public class Controller {
return blankButton; return blankButton;
} }
/**
* Changes smiley face to concerned look when the mouse is held
*
* @param mouseEvent Stub, required by EventHandler interface
*/
private void mouseHeld(MouseEvent mouseEvent) { private void mouseHeld(MouseEvent mouseEvent) {
if (gameOver) if (gameOver)
return; return;
setImage(smiley, "img/face_ooh.png"); setImage(smiley, "img/face_ooh.png");
} }
/**
* Changes smiley face back to smiling after mouse release
*
* @param mouseEvent Stub, required by EventHandler interface
*/
private void mouseReleased(MouseEvent mouseEvent) { private void mouseReleased(MouseEvent mouseEvent) {
if (gameOver) if (gameOver)
return; return;
setImage(smiley, "img/face_smile.png"); setImage(smiley, "img/face_smile.png");
} }
/**
* Handles the main GridPane being clicked
* Identifies the intention of the click (e.g. flag, open tile)
*
* @param e The event fired on click
*/
private void buttonClicked(MouseEvent e) { private void buttonClicked(MouseEvent e) {
if (gameOver) { if (gameOver) {
return; return;
@ -104,9 +236,33 @@ public class Controller {
if (buttonImage.getUrl().contains("flagged.png")) { if (buttonImage.getUrl().contains("flagged.png")) {
return; return;
} }
// if (buttonImage.getUrl().contains("num") && !buttonImage.getUrl().contains("num_0.png")) {
// chord(clicked, buttonImage.getUrl());
// return;
// }
handlePrimaryClick(clicked, column, row); handlePrimaryClick(clicked, column, row);
} }
// private void chord(Button clicked, String clickedURL) {
// int column = GridPane.getColumnIndex(clicked);
// int row = GridPane.getRowIndex(clicked);
// int urlLength = clickedURL.length();
// char expectedAdjacentChar = clickedURL.charAt(urlLength-5);
// int expectedAdjacent = Integer.parseInt(Character.toString(expectedAdjacentChar));
// int actualAdjacent = wrapper.atColumn(column).atRow(row).adjacentBombCount();
// if (expectedAdjacent != actualAdjacent) {
// return;
// }
// // chord logic
// }
/**
* If the first tile clicked was a bomb, move that bomb to the first available column on row 0 before opening it
*
* @param column The column of the clicked tile
* @param row The row of the clicked tile
* @return An array containing the column and row the bomb was moved to
*/
private int[] setBombIfFirstTileIsBomb(int column, int row) { private int[] setBombIfFirstTileIsBomb(int column, int row) {
for (int c = 0; c < 30; ++c) { for (int c = 0; c < 30; ++c) {
for (int r = 0; r < 16; ++r) { for (int r = 0; r < 16; ++r) {
@ -117,15 +273,27 @@ public class Controller {
} }
} }
} }
// this realistically should never happen... sorry!
return null; return null;
} }
/**
* Extracts the URL of a given button
*
* @param button A javafx.scene.control.Button object
* @return A string representation of the image URL
*/
private String getButtonURL(Button button) { private String getButtonURL(Button button) {
ImageView graphic = (ImageView) button.getGraphic(); ImageView graphic = (ImageView) button.getGraphic();
Image image = graphic.getImage(); Image image = graphic.getImage();
return image.getUrl(); return image.getUrl();
} }
/**
* Iterates through the grid to find all tiles that are either blank or flagged
*
* @return The number of tiles that were blank or flagged
*/
private int numberOfUnrevealedTiles() { private int numberOfUnrevealedTiles() {
int unrevealedTiles = 0; int unrevealedTiles = 0;
int column, row; int column, row;
@ -134,7 +302,7 @@ public class Controller {
Button current = (Button) getNodeByRowColumnIndex(row, column); Button current = (Button) getNodeByRowColumnIndex(row, column);
assert current != null; assert current != null;
String currentURL = getButtonURL(current); String currentURL = getButtonURL(current);
if (currentURL.contains("blank.png") || currentURL.contains("flag.png")) { if (currentURL.contains("blank.png") || currentURL.contains("flagged.png")) {
unrevealedTiles++; unrevealedTiles++;
} }
} }
@ -142,11 +310,24 @@ public class Controller {
return unrevealedTiles; return unrevealedTiles;
} }
/**
* Checks if the number of unrevealed tiles is equal to 99 (the win condition)
*
* @return A boolean representing if the game has been won
* @see #numberOfUnrevealedTiles()
*/
private boolean checkWinCondition() { private boolean checkWinCondition() {
int unrevealedTiles = numberOfUnrevealedTiles(); int unrevealedTiles = numberOfUnrevealedTiles();
return unrevealedTiles == 99; return unrevealedTiles == 99;
} }
/**
* Handles a left mouse click on an unopened tile
*
* @param clicked The button that was clicked
* @param column The column that it was clicked at
* @param row The row that it was clicked at
*/
private void handlePrimaryClick(Button clicked, int column, int row) { private void handlePrimaryClick(Button clicked, int column, int row) {
if (wrapper.atColumn(column).atRow(row).isBomb() && !isFirstClick) { if (wrapper.atColumn(column).atRow(row).isBomb() && !isFirstClick) {
gameOver(clicked); gameOver(clicked);
@ -174,6 +355,13 @@ public class Controller {
} }
} }
/**
* If a tile is surrounded by tiles with no adjacent bombs, open all the tiles recursively
*
* @param column The column that was clicked
* @param row The row that was clicked
* @see #expandTile(int, int)
*/
private void recursiveExpandTiles(int column, int row) { private void recursiveExpandTiles(int column, int row) {
if (column < 0 || column >= 30 || row < 0 || row >= 16 || if (column < 0 || column >= 30 || row < 0 || row >= 16 ||
expandedTiles[column][row] && !wrapper.atColumn(column).atRow(row).isBomb()) { expandedTiles[column][row] && !wrapper.atColumn(column).atRow(row).isBomb()) {
@ -192,6 +380,12 @@ public class Controller {
} }
} }
/**
* Opens a tile, and adds it to an array to prevent overflow
*
* @param column The column of the button to open
* @param row The row of the button to open
*/
private void expandTile(int column, int row) { private void expandTile(int column, int row) {
Node tile = getNodeByRowColumnIndex(row, column); Node tile = getNodeByRowColumnIndex(row, column);
if (tile != null) { if (tile != null) {
@ -207,6 +401,13 @@ public class Controller {
} }
} }
/**
* Iterates over the grid and returns the node when it's at the correct child
*
* @param row The desired row to find
* @param column The desired column to find
* @return The node at the specified position
*/
private Node getNodeByRowColumnIndex(int row, int column) { private Node getNodeByRowColumnIndex(int row, int column) {
for (Node node : grid.getChildren()) { for (Node node : grid.getChildren()) {
if (GridPane.getRowIndex(node) == row && GridPane.getColumnIndex(node) == column) { if (GridPane.getRowIndex(node) == row && GridPane.getColumnIndex(node) == column) {
@ -216,6 +417,9 @@ public class Controller {
return null; return null;
} }
/**
* Resets important state variables on restart
*/
@FXML @FXML
private void reinitialize() { private void reinitialize() {
gameOver = false; gameOver = false;
@ -229,6 +433,11 @@ public class Controller {
isFirstClick = true; isFirstClick = true;
} }
/**
* Resets the timer to 0
*
* @see #reinitialize()
*/
private void resetTimer() { private void resetTimer() {
URL zeroSecondURL = getClass().getResource("img/0_seconds.png"); URL zeroSecondURL = getClass().getResource("img/0_seconds.png");
time_1.setImage(new Image(String.valueOf(zeroSecondURL))); time_1.setImage(new Image(String.valueOf(zeroSecondURL)));
@ -239,6 +448,11 @@ public class Controller {
timer = new Timer(); timer = new Timer();
} }
// todo figure out why createBlankButton errors here
/**
* Refills the grid with new blank buttons
*/
private void resetGrid() { private void resetGrid() {
URL blank = getClass().getResource("img/blank.png"); URL blank = getClass().getResource("img/blank.png");
for (Node node : grid.getChildren()) { for (Node node : grid.getChildren()) {
@ -247,6 +461,11 @@ public class Controller {
} }
} }
/**
* When a bomb is clicked, change the smiley, set gameOver to true.
*
* @param tileClicked The node that was clicked, to set the correct image
*/
private void gameOver(Node tileClicked) { private void gameOver(Node tileClicked) {
gameOver = true; gameOver = true;
timer.cancel(); timer.cancel();
@ -255,6 +474,9 @@ public class Controller {
showAllBombs(GridPane.getColumnIndex(tileClicked), GridPane.getRowIndex(tileClicked)); showAllBombs(GridPane.getColumnIndex(tileClicked), GridPane.getRowIndex(tileClicked));
} }
/**
* Creates a win state, change the smiley.
*/
private void win() { private void win() {
gameOver = true; gameOver = true;
timer.cancel(); timer.cancel();
@ -262,6 +484,9 @@ public class Controller {
flagAllRemaining(); flagAllRemaining();
} }
/**
* After a win, set every bomb tile that is unflagged to flagged.
*/
private void flagAllRemaining() { private void flagAllRemaining() {
int column, row; int column, row;
for (column = 0; column < 30; ++column) { for (column = 0; column < 30; ++column) {
@ -277,12 +502,17 @@ public class Controller {
} }
} }
/**
* Create a flag on a tile, representing a tile that the user suspects has a bomb behind it.
* This prevents the tile being clicked on.
*
* @param tileClicked The tile that the user clicks.
*/
private void flag(Node tileClicked) { private void flag(Node tileClicked) {
Button tileAsButton = (Button) tileClicked; Button tileAsButton = (Button) tileClicked;
ImageView tileGraphic = (ImageView) tileAsButton.getGraphic(); ImageView tileGraphic = (ImageView) tileAsButton.getGraphic();
Image tileGraphicImage = tileGraphic.getImage(); Image tileGraphicImage = tileGraphic.getImage();
if (!tileGraphicImage.getUrl().contains("blank.png") && if (!tileGraphicImage.getUrl().contains("blank.png") && !tileGraphicImage.getUrl().contains("flagged.png")) {
!tileGraphicImage.getUrl().contains("flagged.png")) {
return; return;
} }
boolean flagged = tileGraphicImage.getUrl().contains("flagged.png"); boolean flagged = tileGraphicImage.getUrl().contains("flagged.png");
@ -299,17 +529,30 @@ public class Controller {
} }
/**
* On flag or reinitialization, update the bomb counter to represent the amount of currently flagged tiles
*/
private void updateBombCounter() { private void updateBombCounter() {
String bombCountString = String.format("%03d", bombCount); String bombCountString = String.format("%03d", bombCount);
setBombCounterImage(bomb_2, bombCountString.charAt(1)); setBombCounterImage(bomb_2, bombCountString.charAt(1));
setBombCounterImage(bomb_3, bombCountString.charAt(2)); setBombCounterImage(bomb_3, bombCountString.charAt(2));
} }
/**
* Used to update the digits of the bomb counter.
*
* @param imageView The ImageView representing the counter digit to be changed.
* @param digit The digit the counter should be changed to.
* @see #updateBombCounter()
*/
private void setBombCounterImage(ImageView imageView, char digit) { private void setBombCounterImage(ImageView imageView, char digit) {
URL imageURL = getClass().getResource("img/" + digit + "_seconds.png"); URL imageURL = getClass().getResource("img/" + digit + "_seconds.png");
imageView.setImage(new Image(String.valueOf(imageURL))); imageView.setImage(new Image(String.valueOf(imageURL)));
} }
/**
* Starts the timer when the user clicks or flags their first tile.
*/
private void scheduleTimer() { private void scheduleTimer() {
startTime = System.currentTimeMillis(); startTime = System.currentTimeMillis();
TimerTask task = new TimerTask() { TimerTask task = new TimerTask() {
@ -325,6 +568,11 @@ public class Controller {
timer.schedule(task, 0, 1000); timer.schedule(task, 0, 1000);
} }
/**
* Calculates the time since the game started, and passes it on.
*
* @see #setTimerImage(ImageView, char)
*/
private void updateTimer() { private void updateTimer() {
long elapsedTimeMillis = System.currentTimeMillis() - startTime; long elapsedTimeMillis = System.currentTimeMillis() - startTime;
time = (int) (elapsedTimeMillis / 1000); time = (int) (elapsedTimeMillis / 1000);
@ -334,11 +582,24 @@ public class Controller {
setTimerImage(time_3, timeString.charAt(2)); setTimerImage(time_3, timeString.charAt(2));
} }
/**
* Updates the timer digits individually based on updateTimer()
*
* @param imageView The ImageView to be updated
* @param digit The digit to update the ImageView with
* @see #updateTimer()
*/
private void setTimerImage(ImageView imageView, char digit) { private void setTimerImage(ImageView imageView, char digit) {
URL imageURL = getClass().getResource("img/" + digit + "_seconds.png"); URL imageURL = getClass().getResource("img/" + digit + "_seconds.png");
imageView.setImage(new Image(String.valueOf(imageURL))); imageView.setImage(new Image(String.valueOf(imageURL)));
} }
/**
* On game over, show every bomb that was left on the grid, as well as every incorrectly flagged bomb.
*
* @param clickedColumn The column that caused the game over.
* @param clickedRow The row that caused the game over.
*/
private void showAllBombs(int clickedColumn, int clickedRow) { private void showAllBombs(int clickedColumn, int clickedRow) {
for (Node node : grid.getChildren()) { for (Node node : grid.getChildren()) {
Button b = (Button) node; Button b = (Button) node;
@ -355,27 +616,53 @@ public class Controller {
} }
} }
/**
* Sets a button's graphic.
*
* @param button The button to change.
* @param imagePath A path to an image to change the graphic to.
*/
private void setImage(Button button, String imagePath) { private void setImage(Button button, String imagePath) {
URL imageURL = getClass().getResource(imagePath); URL imageURL = getClass().getResource(imagePath);
button.setGraphic(new ImageView(new Image(String.valueOf(imageURL), 16, 16, true, false))); button.setGraphic(new ImageView(new Image(String.valueOf(imageURL), 16, 16, true, false)));
} }
/**
* Replaces an ImageView's graphic with another.
*
* @param imageView The ImageView to change.
* @param imagePath A path to an image to change the graphic to.
*/
private void setImage(ImageView imageView, String imagePath) { private void setImage(ImageView imageView, String imagePath) {
URL imageURL = getClass().getResource(imagePath); URL imageURL = getClass().getResource(imagePath);
imageView.setImage(new Image(String.valueOf(imageURL))); imageView.setImage(new Image(String.valueOf(imageURL)));
} }
/**
* Replaces the smiley with a pressed down variant when it is clicked.
*/
@FXML @FXML
private void smileyPressed() { private void smileyPressed() {
setImage(smiley, "img/face_smile_pressed.png"); setImage(smiley, "img/face_smile_pressed.png");
} }
/**
* Releases the smiley from being pressed down, starts reinitialization.
*
* @see #reinitialize()
*/
@FXML @FXML
private void smileyReleased() { private void smileyReleased() {
setImage(smiley, "img/face_smile.png"); setImage(smiley, "img/face_smile.png");
reinitialize(); reinitialize();
} }
/**
* Sets the graphic of a tile to indicate how many bombs are adjacent to it.
*
* @param tileClicked The tile to update.
* @param adjacentBombs The number of bombs adjacent to the tile.
*/
private void setAdjacentCount(Node tileClicked, int adjacentBombs) { private void setAdjacentCount(Node tileClicked, int adjacentBombs) {
Button button = (Button) tileClicked; Button button = (Button) tileClicked;
setImage(button, "img/num_" + adjacentBombs + ".png"); setImage(button, "img/num_" + adjacentBombs + ".png");

View File

@ -1,18 +1,31 @@
package com.shr4pnel.minesweeper; package com.shr4pnel.minesweeper;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
/**
* Used to fill the GridWrapper with bombs. Planned to merge into GridWrapper.
* @since 1.0.0
*/
public class Grid { public class Grid {
/**
* An instance of GridWrapper used to check generation of bombs.
*/
final GridWrapper grid = new GridWrapper(); final GridWrapper grid = new GridWrapper();
/**
* Generates the bomb-grid structure on instantiation
*
* @see #generateBombs(int)
*/
public Grid() { public Grid() {
// todo fix beginner mode and intermediate!
// sorry :3
// 99 bombs in expert:
generateBombs(99); generateBombs(99);
} }
/**
* Generates a boolean 2D array by randomly selecting a column and row, and setting it to true to represent a bomb.
*
* @param bombMax The number of bombs to generate.
*/
private void generateBombs(int bombMax) { private void generateBombs(int bombMax) {
int i; int i;
boolean success; boolean success;

View File

@ -1,33 +1,76 @@
package com.shr4pnel.minesweeper; package com.shr4pnel.minesweeper;
/**
* Simplifies operations on the bomb array. Preventing direct access leads to cleaner code.
* @since 1.0.0
*/
public class GridWrapper { public class GridWrapper {
/**
* Number of columns.
*/
private static final int COLUMNS = 30; private static final int COLUMNS = 30;
/**
* Number of rows.
*/
private static final int ROWS = 16; private static final int ROWS = 16;
/**
* A low level 2D array representing the position of bombs.
*/
final boolean[][] grid = new boolean[COLUMNS][ROWS]; final boolean[][] grid = new boolean[COLUMNS][ROWS];
/**
* Points at a column in grid.
*/
private int currentColumn; private int currentColumn;
/**
* Points at a row in grid.
*/
private int currentRow; private int currentRow;
/**
* On instantiation, initialize currentColumn and currentRow to 0
*/
public GridWrapper() { public GridWrapper() {
this.currentColumn = 0; this.currentColumn = 0;
this.currentRow = 0; this.currentRow = 0;
} }
/**
* Sets currentColumn to a specified column.
*
* @param column The column specified.
* @return Current GridWrapper instance.
*/
public GridWrapper atColumn(int column) { public GridWrapper atColumn(int column) {
this.currentColumn = column; this.currentColumn = column;
return this; return this;
} }
/**
* Sets currentColumn to a specified column.
*
* @param row The row specified.
* @return Current GridWrapper instance.
*/
public GridWrapper atRow(int row) { public GridWrapper atRow(int row) {
this.currentRow = row; this.currentRow = row;
return this; return this;
} }
/**
* Sets a bomb in grid at the position specified by currentColumn and currentRow.
*/
public void setBomb() { public void setBomb() {
if (isValid(currentColumn, currentRow)) { if (isValid(currentColumn, currentRow)) {
grid[currentColumn][currentRow] = true; grid[currentColumn][currentRow] = true;
} }
} }
/**
* Switches a bomb from one position to another.
*
* @param destinationColumn The new column for the bomb.
* @param destinationRow The new row for the bomb.
*/
public void switchBomb(int destinationColumn, int destinationRow) { public void switchBomb(int destinationColumn, int destinationRow) {
if (isValid(currentColumn, currentRow) && isValid(destinationColumn, destinationRow)) { if (isValid(currentColumn, currentRow) && isValid(destinationColumn, destinationRow)) {
grid[destinationColumn][destinationRow] = true; grid[destinationColumn][destinationRow] = true;
@ -35,10 +78,20 @@ public class GridWrapper {
} }
} }
/**
* Checks if there is a bomb at the specified position.
*
* @return The value in grid specified by currentColumn and currentRow.
*/
public boolean isBomb() { public boolean isBomb() {
return isValid(currentColumn, currentRow) && grid[currentColumn][currentRow]; return isValid(currentColumn, currentRow) && grid[currentColumn][currentRow];
} }
/**
* Checks for bombs in every direction from the currentColumn and currentRow.
*
* @return The number of adjacent bombs.
*/
public int adjacentBombCount() { public int adjacentBombCount() {
int count = 0; int count = 0;
@ -69,15 +122,40 @@ public class GridWrapper {
return count; return count;
} }
// todo use switchBomb
/**
* Swaps one bomb with another.
*
* @param oldColumn The original column of the bomb.
* @param oldRow The original row of the bomb.
* @param newColumn The destination column of the bomb.
* @param newRow The destination row of the bomb.
*/
public void updateGrid(int oldColumn, int oldRow, int newColumn, int newRow) { public void updateGrid(int oldColumn, int oldRow, int newColumn, int newRow) {
grid[oldColumn][oldRow] = false; grid[oldColumn][oldRow] = false;
grid[newColumn][newRow] = true; grid[newColumn][newRow] = true;
} }
/**
* Checks if a bomb is at a specified position.
*
* @param column The column specified.
* @param row The row specified.
* @return A boolean AND representing a valid position and a bomb being present.
* @see #isValid(int, int)
*/
private boolean isBombAt(int column, int row) { private boolean isBombAt(int column, int row) {
return isValid(column, row) && grid[column][row]; return isValid(column, row) && grid[column][row];
} }
/**
* Checks if a given column and row is in bounds.
*
* @param column The column provided.
* @param row The row provided.
* @return A boolean representing the tile being in bounds.
*/
private boolean isValid(int column, int row) { private boolean isValid(int column, int row) {
return column >= 0 && column < COLUMNS && row >= 0 && row < ROWS; return column >= 0 && column < COLUMNS && row >= 0 && row < ROWS;
} }

View File

@ -1,6 +1,7 @@
package com.shr4pnel.minesweeper; package com.shr4pnel.minesweeper;
import java.io.IOException; import java.io.IOException;
import javafx.application.Application; import javafx.application.Application;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
@ -8,7 +9,17 @@ import javafx.scene.Scene;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.stage.Stage; import javafx.stage.Stage;
/**
* The application opening point, used to bootstrap JavaFX and open to the GUI.
* @since 1.0.0
*/
public class Main extends Application { public class Main extends Application {
/**
* JavaFX opening method. Creates the stage and bootstraps the application.
*
* @param stage The stage object passed in by JavaFX.
* @throws IOException If the FXML template or app icon are not found in resources.
*/
@Override @Override
public void start(Stage stage) throws IOException { public void start(Stage stage) throws IOException {
Image icon = new Image(String.valueOf(getClass().getResource("winmine.png"))); Image icon = new Image(String.valueOf(getClass().getResource("winmine.png")));
@ -21,10 +32,19 @@ public class Main extends Application {
stage.show(); stage.show();
} }
/**
* Application opening method. Calls the JavaFX start method.
*
* @param args Optional commandline parameters, unimplemented.
* @see #start(Stage)
*/
public static void main(String[] args) { public static void main(String[] args) {
launch(); launch();
} }
/**
* Cancels the timer on application stop.
*/
@Override @Override
@FXML @FXML
public void stop() { public void stop() {

View File

@ -1,3 +1,8 @@
/**
* The main module for the program. Required to launch to JavaFX as well as bundle into a native binary.
* @author shrapnelnet admin@shr4pnel.com
* @since 1.0.0
*/
module libremines { module libremines {
requires javafx.controls; requires javafx.controls;
requires javafx.fxml; requires javafx.fxml;

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.image.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<AnchorPane prefHeight="225.0" prefWidth="454.0" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1">
<children>
<ImageView fitHeight="150.0" fitWidth="200.0" layoutX="146.0" layoutY="31.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@winmine.png" />
</image>
</ImageView>
<Text layoutY="42.0" strokeType="OUTSIDE" strokeWidth="0.0" text="made with love - shrapnelnet" textAlignment="CENTER" wrappingWidth="454.0">
<font>
<Font name="Source Code Pro" size="13.0" />
</font>
</Text>
<Text layoutY="183.0" strokeType="OUTSIDE" strokeWidth="0.0" text="greeTz: b4rkod, ping, cockpit, vulon, i330 n' porcupine" textAlignment="CENTER" wrappingWidth="454.0">
<font>
<Font name="Source Code Pro" size="13.0" />
</font>
</Text>
<Text layoutY="200.0" strokeType="OUTSIDE" strokeWidth="0.0" text="next time make your own!" textAlignment="CENTER" wrappingWidth="454.0">
<font>
<Font name="Source Code Pro" size="13.0" />
</font>
</Text>
</children>
</AnchorPane>

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -8,7 +8,7 @@
<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="350.0" prefWidth="500.0" style="-fx-border-color: black; -fx-border-width: 1px;" stylesheets="@fix-glow.css" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.shr4pnel.minesweeper.Controller"> <VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="350.0" prefWidth="500.0" style="-fx-border-color: black; -fx-border-width: 1px;" stylesheets="@fix-glow.css" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.shr4pnel.minesweeper.Controller">
<children> <children>
<MenuBar style="-fx-background-color: white;"> <MenuBar prefWidth="498.0" style="-fx-background-color: white;">
<menus> <menus>
<Menu mnemonicParsing="false" text="Game"> <Menu mnemonicParsing="false" text="Game">
<items> <items>
@ -23,7 +23,7 @@
<ToggleGroup fx:id="difficulty" /> <ToggleGroup fx:id="difficulty" />
</toggleGroup> </toggleGroup>
</RadioMenuItem> </RadioMenuItem>
<RadioMenuItem mnemonicParsing="false" text="Internmediate" toggleGroup="$difficulty" /> <RadioMenuItem mnemonicParsing="false" text="Intermediate" toggleGroup="$difficulty" />
<RadioMenuItem mnemonicParsing="false" selected="true" text="Expert" toggleGroup="$difficulty" /> <RadioMenuItem mnemonicParsing="false" selected="true" text="Expert" toggleGroup="$difficulty" />
<SeparatorMenuItem mnemonicParsing="false" /> <SeparatorMenuItem mnemonicParsing="false" />
<RadioMenuItem fx:id="marks" mnemonicParsing="false" selected="true" text="Marks (?)" /> <RadioMenuItem fx:id="marks" mnemonicParsing="false" selected="true" text="Marks (?)" />
@ -36,7 +36,7 @@
</Menu> </Menu>
<Menu mnemonicParsing="false" text="Help"> <Menu mnemonicParsing="false" text="Help">
<items> <items>
<MenuItem mnemonicParsing="false" text="About Minesweeper" /> <MenuItem fx:id="about" mnemonicParsing="false" text="About Minesweeper" />
</items> </items>
</Menu> </Menu>
</menus> </menus>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.image.*?>
<?import javafx.scene.layout.*?>
<Pane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="420.0" prefWidth="525.0" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1">
<children>
<ImageView fitHeight="420.0" fitWidth="531.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@img/204.jpg" />
</image>
</ImageView>
</children>
</Pane>