diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..3fe6c62
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,14 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.{java,xml,yml,yaml,md}]
+indent_style = space
+indent_size = 4
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..d9d9d49
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,12 @@
+version: 2
+updates:
+ - package-ecosystem: maven
+ directory: /
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 10
+
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..0a9ec9a
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,29 @@
+name: CI
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK 21
+ uses: actions/setup-java@v5
+ with:
+ java-version: "21"
+ distribution: temurin
+ cache: maven
+
+ - name: Format check (Spotless)
+ run: mvn -B spotless:check
+
+ - name: Compile
+ run: mvn -B -DskipTests compile
+
+ - name: Test
+ run: mvn -B test
diff --git a/.gitignore b/.gitignore
index d7cb1ac..f9c9918 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,20 @@
-# Ignore the .idea folder
+# IDE
.idea/
+*.iml
+.vscode/
+*.swp
+*~
-# Ignore the out folder
+# Build
+target/
out/
+build/
+bin/
+obj/
-# IML files
-*.iml
+# OS
+.DS_Store
+Thumbs.db
-# yml files
-*.yml
+# Logs
+*.log
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f4f7fae
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Joel Shearon
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index fdccf04..3a0f2e1 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,73 @@
# randobotjava
-A (now less) buggy JavaFx robot game I made to play with JavaFx concepts
-
-Options include:
-Setting the grid size for the game
-Autoplay for when you are lazy
-Manual play either with direction keys or have the game decide a move for you
-Ability to exit or restart the game
-
-
-# Future features
-* Making it an actual game where the treasure is hidden and you have to find it like a pirate or whatever (Robot Pirates are
-a thing, right? Right?)
-* Gonna make this bot real smart so it can find the treasure quicker and in a future release it will probably steal your
-girl
-* A scoreboard for when you want to feel proud about things that don't matter (I know I do since I am making this project and
-feeling good about it, right? Raight? Guise?)
-* Online multiplayer so you can race your friends at pressing buttons quickly so its basically Rocket League at that point. Lit.
+
+[](https://github.com/hippi345/randobotjava/actions/workflows/ci.yml)
+[](LICENSE)
+
+A JavaFX desktop game where a robot hunts for treasure on a grid. You can play manually, step move-by-move, or let autoplay run until the bot finds the loot.
+
+## Features
+
+- Configurable grid size (1–15, default 5)
+- Manual play: direction buttons or “Next move” (bot chooses intelligently)
+- Autoplay until the treasure is found
+- Reset and return to the main menu
+
+## Requirements
+
+- **JDK 21** (LTS)
+- **Maven 3.9+** (for builds)
+- A display for the JavaFX UI (desktop environment)
+
+No API keys, environment variables, or network access are required.
+
+## Setup
+
+```bash
+git clone https://github.com/hippi345/randobotjava.git
+cd randobotjava
+mvn -B package
+```
+
+## Run
+
+```bash
+mvn javafx:run
+```
+
+Or after packaging:
+
+```bash
+mvn -B package
+java --module-path "$HOME/.m2/repository/org/openjfx/javafx-controls/21.0.6/javafx-controls-21.0.6.jar:$(dirname $(find ~/.m2/repository/org/openjfx -name 'javafx-graphics*.jar' | head -1))" --add-modules javafx.controls,javafx.graphics -cp target/randobotjava-1.0.0-SNAPSHOT.jar sample.Main
+```
+
+Using the Maven JavaFX plugin (`mvn javafx:run`) is the simplest option.
+
+## Tests and linting
+
+```bash
+# Unit tests (offline, no credentials)
+mvn test
+
+# Google Java Format via Spotless
+mvn spotless:check
+mvn spotless:apply # auto-format
+```
+
+## Project structure
+
+```
+├── pom.xml # Maven build (Java 21, JavaFX, JUnit 5)
+├── src/main/java/sample/
+│ ├── Main.java # JavaFX application entry
+│ ├── Game.java # Game rules and turns
+│ ├── View.java # UI and grid rendering
+│ ├── Constants.java # Grid defaults
+│ ├── interfaces/ # Game and point contracts
+│ └── models/ # Bot, treasure, grid types
+└── src/test/java/ # JUnit 5 tests
+```
+
+## License
+
+MIT License — see [LICENSE](LICENSE) (Copyright © 2026 Joel Shearon).
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..f631cf8
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,15 @@
+# Security Policy
+
+## Supported versions
+
+Security fixes are applied on the `master` branch. Use the latest release from that branch.
+
+## Reporting a vulnerability
+
+If you discover a security issue, please open a [private security advisory](https://github.com/hippi345/randobotjava/security/advisories/new) on GitHub or contact the repository owner. Do not open a public issue for sensitive reports.
+
+We will acknowledge reports as quickly as possible and work on a fix or mitigation.
+
+## Secrets and credentials
+
+This project is a local desktop game and does not require API keys or cloud credentials. Do not commit secrets to the repository.
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..5978887
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,73 @@
+
+
+ 4.0.0
+
+ com.hippi345
+ randobotjava
+ 1.0.0-SNAPSHOT
+ randobotjava
+ JavaFX treasure-hunt robot game
+ https://github.com/hippi345/randobotjava
+
+
+ UTF-8
+ 21
+ 21.0.6
+ 5.11.4
+
+
+
+
+ org.openjfx
+ javafx-controls
+ ${javafx.version}
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.2
+
+
+ org.openjfx
+ javafx-maven-plugin
+ 0.0.8
+
+ sample.Main
+
+
+
+ com.diffplug.spotless
+ spotless-maven-plugin
+ 2.43.0
+
+
+
+ 1.23.0
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/META-INF/MANIFEST.MF b/src/META-INF/MANIFEST.MF
deleted file mode 100644
index 1f6226f..0000000
--- a/src/META-INF/MANIFEST.MF
+++ /dev/null
@@ -1,3 +0,0 @@
-Manifest-Version: 1.0
-Main-Class: sample.Main
-
diff --git a/src/main/java/sample/Constants.java b/src/main/java/sample/Constants.java
new file mode 100644
index 0000000..72d69d5
--- /dev/null
+++ b/src/main/java/sample/Constants.java
@@ -0,0 +1,9 @@
+package sample;
+
+final class Constants {
+ static final int DEFAULT_GRID_SIZE = 5;
+ static final int MAX_GRID_SIZE = 15;
+ static final String LIGHT_BLUE = "-fx-background-color:#00BFFF; -fx-opacity:1;";
+
+ private Constants() {}
+}
diff --git a/src/main/java/sample/Game.java b/src/main/java/sample/Game.java
new file mode 100644
index 0000000..4ec8636
--- /dev/null
+++ b/src/main/java/sample/Game.java
@@ -0,0 +1,97 @@
+package sample;
+
+import sample.interfaces.IGame;
+import sample.interfaces.IMoveablePoint;
+import sample.interfaces.IPoint;
+import sample.models.Bot;
+import sample.models.GameStatusEnum;
+import sample.models.MoveEnum;
+import sample.models.Point;
+import sample.models.Treasure;
+
+class Game implements IGame {
+ private int turnCount = 0;
+ private GameStatusEnum status;
+ private final int gridSize;
+ private final GameBoardListener boardListener;
+ IMoveablePoint bot;
+ Treasure treasure;
+
+ Game(int gridSize, GameBoardListener boardListener) {
+ this.gridSize = gridSize;
+ this.boardListener = boardListener;
+ status = GameStatusEnum.NotStarted;
+ this.bot = new Bot(gridSize);
+ this.treasure = new Treasure();
+ initializePositions();
+ }
+
+ @Override
+ public void makeMove() {
+ makeMove(this.bot.determineMovement());
+ }
+
+ @Override
+ public void makeMove(MoveEnum move) {
+ executeTurn(move);
+ }
+
+ @Override
+ public GameStatusEnum getStatus() {
+ return status;
+ }
+
+ @Override
+ public IPoint getBot() {
+ return bot;
+ }
+
+ @Override
+ public IPoint getTreasure() {
+ return treasure;
+ }
+
+ int getGridSize() {
+ return gridSize;
+ }
+
+ void initializePositions() {
+ this.treasure.randomizeLocation(gridSize);
+ this.bot.randomizeLocation(gridSize);
+ while (this.bot.equals(this.treasure)) {
+ this.bot.randomizeLocation(gridSize);
+ }
+ boardListener.onPositionsChanged(null, this.bot, this.treasure);
+ }
+
+ private void executeTurn(MoveEnum botMovementDirection) {
+ if (status == GameStatusEnum.Complete) {
+ return;
+ } else if (status == GameStatusEnum.NotStarted) {
+ status = GameStatusEnum.InProgress;
+ }
+
+ ++turnCount;
+ System.out.println("Current turn: " + turnCount);
+
+ IPoint previousBotPoint = new Point(this.bot);
+ this.bot.move(botMovementDirection);
+
+ determineCurrentStatus();
+ boardListener.onPositionsChanged(previousBotPoint, this.bot, this.treasure);
+ if (status == GameStatusEnum.Complete) {
+ System.out.println("You found the treasure!");
+ boardListener.onGameComplete();
+ }
+ }
+
+ private boolean treasureIsFound() {
+ return this.bot.equals(this.treasure);
+ }
+
+ private void determineCurrentStatus() {
+ if (treasureIsFound()) {
+ status = GameStatusEnum.Complete;
+ }
+ }
+}
diff --git a/src/main/java/sample/GameBoardListener.java b/src/main/java/sample/GameBoardListener.java
new file mode 100644
index 0000000..efca632
--- /dev/null
+++ b/src/main/java/sample/GameBoardListener.java
@@ -0,0 +1,11 @@
+package sample;
+
+import sample.interfaces.IPoint;
+
+/** Notifies the UI when bot or treasure positions change. */
+interface GameBoardListener {
+ void onPositionsChanged(
+ IPoint previousBotPosition, IPoint currentBotPosition, IPoint treasurePosition);
+
+ default void onGameComplete() {}
+}
diff --git a/src/main/java/sample/Main.java b/src/main/java/sample/Main.java
new file mode 100644
index 0000000..1311879
--- /dev/null
+++ b/src/main/java/sample/Main.java
@@ -0,0 +1,70 @@
+package sample;
+
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+import sample.interfaces.IGame;
+import sample.models.GameStatusEnum;
+
+public class Main extends Application {
+ private static IGame game;
+ static int gridSizeForGame;
+ static View gameView;
+
+ public static Stage startGui = new Stage();
+
+ public static void main(String[] args) {
+ launch(args);
+ }
+
+ @Override
+ public void start(Stage primaryStage) {
+ View startView = new View();
+ startView.startScreen(startGui, (Game) game);
+
+ startGui.setTitle("Treasure Hunt");
+ startGui.setScene(new Scene(startView.gridPane, 400, 200));
+ startGui.show();
+ }
+
+ static void setupView(View gameView) {
+ gameView.setupButtons(
+ (o) -> game.makeMove(),
+ (o) -> prepareGame(),
+ (o) -> runAutoPlay(),
+ (o) -> backToStartup());
+
+ gameView.setupDirectionButtons(game, gameView);
+ }
+
+ private static void runAutoPlay() {
+ Thread autoPlayThread =
+ new Thread(
+ () -> {
+ while (game.getStatus() != GameStatusEnum.Complete) {
+ Platform.runLater(() -> game.makeMove());
+ try {
+ Thread.sleep(50);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ }
+ },
+ "autoplay");
+ autoPlayThread.setDaemon(true);
+ autoPlayThread.start();
+ }
+
+ static void prepareGame() {
+ game = new Game(gridSizeForGame, gameView);
+ }
+
+ public static void backToStartup() {
+ View.mainGame.close();
+ View startView = new View();
+ startView.startScreen(startGui, (Game) game);
+ startGui.show();
+ }
+}
diff --git a/src/main/java/sample/View.java b/src/main/java/sample/View.java
new file mode 100644
index 0000000..14be696
--- /dev/null
+++ b/src/main/java/sample/View.java
@@ -0,0 +1,254 @@
+package sample;
+
+import java.util.Hashtable;
+import java.util.function.Consumer;
+import javafx.application.Platform;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.GridPane;
+import javafx.scene.text.Text;
+import javafx.stage.Stage;
+import sample.interfaces.IGame;
+import sample.interfaces.IPoint;
+import sample.models.MoveEnum;
+import sample.models.Point;
+
+class View implements GameBoardListener {
+ static Stage mainGame;
+ GridPane gridPane;
+ int gridSize = Constants.DEFAULT_GRID_SIZE;
+ Hashtable pointNodeMap = new Hashtable<>();
+
+ View() {
+ mainGame = new Stage();
+ this.gridPane = new GridPane();
+ }
+
+ private void setGridSize(int gridSize) {
+ this.gridSize = gridSize;
+ }
+
+ void setupTheGridPane() {
+ this.gridPane.setHgap(8);
+ this.gridPane.setVgap(8);
+ setupGui();
+ setupMatrixMap();
+ }
+
+ private void setupGui() {
+ for (int i = 0; i < this.gridSize; i++) {
+ for (int j = 0; j < this.gridSize; j++) {
+ this.gridPane.add(new Text(""), i, j);
+ }
+ }
+ this.gridPane.setStyle(Constants.LIGHT_BLUE);
+ }
+
+ void startScreen(Stage startGui, Game game) {
+ this.gridPane.setHgap(8);
+ this.gridPane.setVgap(8);
+ this.gridPane.setStyle(Constants.LIGHT_BLUE);
+
+ Text handle = new Text("Welcome to the Random Bot Game!");
+ Text argInquire = new Text("What is the Grid size? (size <= 15)");
+ Button submitStart = new Button("Go!");
+ CheckBox defaultBehaviors = new CheckBox("Defaults for the application");
+ TextField gridSizeStart = new TextField();
+
+ gridSizeStart
+ .textProperty()
+ .addListener(
+ new ChangeListener() {
+ @Override
+ public void changed(
+ ObservableValue extends String> observable,
+ String oldValue,
+ String newValue) {
+ if (!newValue.matches("\\d*")) {
+ gridSizeStart.setText(newValue.replaceAll("[^\\d]", ""));
+ }
+ }
+ });
+
+ submitStart.setOnAction(
+ actionEvent -> prepTheGame(defaultBehaviors, gridSizeStart, startGui, game));
+ defaultBehaviors
+ .selectedProperty()
+ .addListener((observable, oldValue, newValue) -> gridSizeStart.setText(""));
+
+ this.gridPane.add(handle, 0, 0);
+ this.gridPane.add(argInquire, 0, 1);
+ this.gridPane.add(gridSizeStart, 0, 2);
+ this.gridPane.add(defaultBehaviors, 1, 2);
+ this.gridPane.add(submitStart, 0, 3);
+ }
+
+ private void prepTheGame(CheckBox defaults, TextField textInput, Stage startGui, Game game) {
+ int resolvedGridSize;
+ if (defaults.isSelected() || textInput.getText().isBlank()) {
+ textInput.clear();
+ resolvedGridSize = Constants.DEFAULT_GRID_SIZE;
+ } else {
+ resolvedGridSize = Integer.parseInt(textInput.getText());
+ if (resolvedGridSize < 1) {
+ resolvedGridSize = Constants.DEFAULT_GRID_SIZE;
+ } else if (resolvedGridSize > Constants.MAX_GRID_SIZE) {
+ resolvedGridSize = Constants.MAX_GRID_SIZE;
+ alertMsgOnMax();
+ }
+ }
+ startGui.close();
+ Main.gridSizeForGame = resolvedGridSize;
+ startGameGui(resolvedGridSize, game);
+ }
+
+ private void alertMsgOnMax() {
+ Stage alert = new Stage();
+ Text message =
+ new Text(
+ "Applying the max size (15) as the actual grid size since input exceeded it.");
+ Button ok = new Button("Ok");
+ ok.setOnAction(actionEvent -> alert.close());
+ GridPane msgGridPane = new GridPane();
+ msgGridPane.add(message, 0, 0);
+ msgGridPane.add(ok, 0, 1);
+ alert.setScene(new Scene(msgGridPane, 400, 75));
+ alert.show();
+ alert.setAlwaysOnTop(true);
+ }
+
+ private void startGameGui(int parseInt, Game game) {
+ View gameView = new View();
+ gameView.setGridSize(parseInt);
+ Main.gameView = gameView;
+
+ gameView.setupTheGridPane();
+
+ Main.prepareGame();
+ Main.setupView(gameView);
+
+ double size = (13.0 * Math.pow(parseInt, 2)) + 50;
+ mainGame.setTitle("Treasure Hunt");
+ mainGame.setScene(new Scene(gameView.gridPane, size, size));
+ mainGame.show();
+ }
+
+ private void setupMatrixMap() {
+ for (Node node : gridPane.getChildren()) {
+ int currentColumnIndex = GridPane.getColumnIndex(node);
+ int currentRowIndex = GridPane.getRowIndex(node);
+
+ if (node instanceof Text) {
+ ((Text) node).setText(currentColumnIndex + " " + currentRowIndex + " empty");
+ pointNodeMap.put(new Point(currentColumnIndex, currentRowIndex), node);
+ }
+ }
+ }
+
+ @Override
+ public void onGameComplete() {
+ setupEndGameGui();
+ }
+
+ @Override
+ public void onPositionsChanged(
+ IPoint previousBotPosition, IPoint currentBotPosition, IPoint treasurePosition) {
+ if (previousBotPosition != null) {
+ Node previousNode = pointNodeMap.get(previousBotPosition);
+ if (previousNode instanceof Text) {
+ ((Text) previousNode)
+ .setText(
+ previousBotPosition.getX()
+ + " "
+ + previousBotPosition.getY()
+ + " empty");
+ }
+ }
+
+ Node currentBotNode = pointNodeMap.get(currentBotPosition);
+ if (currentBotNode instanceof Text) {
+ ((Text) currentBotNode)
+ .setText(currentBotPosition.getX() + " " + currentBotPosition.getY() + " bot");
+ }
+
+ Node treasureNode = pointNodeMap.get(treasurePosition);
+ if (treasureNode != currentBotNode && treasureNode instanceof Text) {
+ ((Text) treasureNode)
+ .setText(treasurePosition.getX() + " " + treasurePosition.getY() + " treasure");
+ }
+ }
+
+ void setupButtons(
+ Consumer