From e6d10e6dc2be5ae1f9ab9a351f2cbd8bf1b2821c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 26 Sep 2026 16:25:45 +0000 Subject: [PATCH 1/2] Repo health: Maven, Java 21, tests, CI, and docs - Add Maven build with JavaFX 21 and JUnit 5 - Restructure to src/main/java and src/test/java - Decouple game logic from UI; fix main(), autoplay thread, empty grid input - Add Spotless formatting, GitHub Actions CI, Dependabot - Add MIT LICENSE, SECURITY.md, README, and .editorconfig Co-authored-by: Joel Town Road --- .editorconfig | 14 + .github/dependabot.yml | 12 + .github/workflows/ci.yml | 29 ++ .gitignore | 21 +- LICENSE | 21 ++ README.md | 89 ++++-- SECURITY.md | 15 + pom.xml | 73 +++++ src/META-INF/MANIFEST.MF | 3 - src/main/java/sample/Constants.java | 9 + src/main/java/sample/Game.java | 97 +++++++ src/main/java/sample/GameBoardListener.java | 11 + src/main/java/sample/Main.java | 70 +++++ src/main/java/sample/View.java | 254 ++++++++++++++++ src/main/java/sample/interfaces/IGame.java | 16 + .../sample/interfaces/IMoveablePoint.java | 8 +- .../java}/sample/interfaces/IPoint.java | 3 +- src/main/java/sample/models/Bot.java | 114 ++++++++ .../java}/sample/models/GameStatusEnum.java | 0 .../java}/sample/models/MoveEnum.java | 0 src/main/java/sample/models/Point.java | 88 ++++++ .../java}/sample/models/Treasure.java | 3 +- src/sample/Constants.java | 10 - src/sample/Game.java | 109 ------- src/sample/Main.java | 65 ----- src/sample/View.java | 274 ------------------ src/sample/interfaces/IGame.java | 16 - src/sample/models/Bot.java | 136 --------- src/sample/models/Point.java | 96 ------ src/test/java/sample/GameTest.java | 55 ++++ src/test/java/sample/models/PointTest.java | 49 ++++ 31 files changed, 1021 insertions(+), 739 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 pom.xml delete mode 100644 src/META-INF/MANIFEST.MF create mode 100644 src/main/java/sample/Constants.java create mode 100644 src/main/java/sample/Game.java create mode 100644 src/main/java/sample/GameBoardListener.java create mode 100644 src/main/java/sample/Main.java create mode 100644 src/main/java/sample/View.java create mode 100644 src/main/java/sample/interfaces/IGame.java rename src/{ => main/java}/sample/interfaces/IMoveablePoint.java (50%) rename src/{ => main/java}/sample/interfaces/IPoint.java (76%) create mode 100644 src/main/java/sample/models/Bot.java rename src/{ => main/java}/sample/models/GameStatusEnum.java (100%) rename src/{ => main/java}/sample/models/MoveEnum.java (100%) create mode 100644 src/main/java/sample/models/Point.java rename src/{ => main/java}/sample/models/Treasure.java (82%) delete mode 100644 src/sample/Constants.java delete mode 100644 src/sample/Game.java delete mode 100644 src/sample/Main.java delete mode 100644 src/sample/View.java delete mode 100644 src/sample/interfaces/IGame.java delete mode 100644 src/sample/models/Bot.java delete mode 100644 src/sample/models/Point.java create mode 100644 src/test/java/sample/GameTest.java create mode 100644 src/test/java/sample/models/PointTest.java 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..d2f4fd5 --- /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@v4 + 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. + +[![CI](https://github.com/hippi345/randobotjava/actions/workflows/ci.yml/badge.svg)](https://github.com/hippi345/randobotjava/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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 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 nextFunction, + Consumer resetFunction, + Consumer autoPlayFunction, + Consumer leaveGame) { + Button nextPlay = new Button("Next move"); + Button reset = new Button("Reset"); + Button autoPlay = new Button("AutoPlay"); + Button exit = new Button("Leave game"); + + nextPlay.setOnAction(actionEvent -> nextFunction.accept(null)); + reset.setOnAction(actionEvent -> resetFunction.accept(null)); + autoPlay.setOnAction(actionEvent -> autoPlayFunction.accept(null)); + exit.setOnAction(actionEvent -> leaveGame.accept(null)); + + gridPane.add(nextPlay, 1, gridSize + 1); + gridPane.add(reset, 1, gridSize + 2); + gridPane.add(autoPlay, 1, gridSize + 3); + gridPane.add(exit, 1, gridSize + 4); + } + + void setupDirectionButtons(IGame game, View gameView) { + Button upMovement = new Button("Move Up"); + Button downMovement = new Button("Move Down"); + Button leftMovement = new Button("Move Left"); + Button rightMovement = new Button("Move Right"); + upMovement.setOnAction(event -> game.makeMove(MoveEnum.Up)); + downMovement.setOnAction(event -> game.makeMove(MoveEnum.Down)); + leftMovement.setOnAction(event -> game.makeMove(MoveEnum.Left)); + rightMovement.setOnAction(event -> game.makeMove(MoveEnum.Right)); + + gameView.gridPane.add(upMovement, 1, gridSize + 5); + gameView.gridPane.add(downMovement, 1, gridSize + 7); + gameView.gridPane.add(leftMovement, 0, gridSize + 6); + gameView.gridPane.add(rightMovement, 2, gridSize + 6); + } + + static void setupEndGameGui() { + Stage endGameGui = new Stage(); + GridPane endPane = new GridPane(); + endPane.setHgap(5); + endPane.setVgap(5); + Text endMsg = new Text("Congrats on finding the treasure! Give it another go or quit?"); + Button continueButton = new Button("New Game"); + Button exitButton = new Button("Exit"); + + continueButton.setOnAction( + event -> { + Main.startGui.close(); + endGameGui.close(); + Main.backToStartup(); + }); + + exitButton.setOnAction( + event -> { + Main.startGui.close(); + endGameGui.close(); + System.out.println("Game closed"); + Platform.exit(); + }); + + endPane.add(endMsg, 0, 0); + endPane.add(continueButton, 0, 2); + endPane.add(exitButton, 0, 3); + + endGameGui.setScene(new Scene(endPane, 300, 300)); + endGameGui.show(); + } +} diff --git a/src/main/java/sample/interfaces/IGame.java b/src/main/java/sample/interfaces/IGame.java new file mode 100644 index 0000000..4d43cb8 --- /dev/null +++ b/src/main/java/sample/interfaces/IGame.java @@ -0,0 +1,16 @@ +package sample.interfaces; + +import sample.models.GameStatusEnum; +import sample.models.MoveEnum; + +public interface IGame { + void makeMove(); + + void makeMove(MoveEnum move); + + GameStatusEnum getStatus(); + + IPoint getBot(); + + IPoint getTreasure(); +} diff --git a/src/sample/interfaces/IMoveablePoint.java b/src/main/java/sample/interfaces/IMoveablePoint.java similarity index 50% rename from src/sample/interfaces/IMoveablePoint.java rename to src/main/java/sample/interfaces/IMoveablePoint.java index d5d7b37..0e10fc5 100644 --- a/src/sample/interfaces/IMoveablePoint.java +++ b/src/main/java/sample/interfaces/IMoveablePoint.java @@ -3,7 +3,9 @@ import sample.models.MoveEnum; public interface IMoveablePoint extends IPoint { - void RandomizeLocation(int bound); - void Move(MoveEnum direction); - MoveEnum DetermineMovement(); + void randomizeLocation(int bound); + + void move(MoveEnum direction); + + MoveEnum determineMovement(); } diff --git a/src/sample/interfaces/IPoint.java b/src/main/java/sample/interfaces/IPoint.java similarity index 76% rename from src/sample/interfaces/IPoint.java rename to src/main/java/sample/interfaces/IPoint.java index c0cdef8..d69d47c 100644 --- a/src/sample/interfaces/IPoint.java +++ b/src/main/java/sample/interfaces/IPoint.java @@ -1,8 +1,7 @@ package sample.interfaces; -import sample.models.Point; - public interface IPoint { int getX(); + int getY(); } diff --git a/src/main/java/sample/models/Bot.java b/src/main/java/sample/models/Bot.java new file mode 100644 index 0000000..1bbf8c7 --- /dev/null +++ b/src/main/java/sample/models/Bot.java @@ -0,0 +1,114 @@ +package sample.models; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Random; +import sample.interfaces.IMoveablePoint; + +public class Bot extends Point implements IMoveablePoint { + private static final boolean MOVE_INTELLIGENTLY = true; + private final HashSet visitedPoints = new HashSet<>(); + private final int movementBoundary; + private final Random randomNumberGenerator = new Random(); + + public Bot(int movementBoundary) { + super(); + this.movementBoundary = movementBoundary; + } + + @Override + public void move(MoveEnum direction) { + ArrayList possibleMoves = getPossibleMoves(); + if (possibleMoves.contains(direction)) { + super.moveInDirection(direction); + this.visitedPoints.add(new Point(this.x, this.y)); + } + } + + @Override + public MoveEnum determineMovement() { + if (MOVE_INTELLIGENTLY) { + return determineIntelligentMove(); + } + return determineRandomMove(); + } + + @Override + public void randomizeLocation(int bound) { + super.randomizeLocation(bound); + visitedPoints.clear(); + visitedPoints.add(new Point(this.x, this.y)); + } + + private MoveEnum determineRandomMove() { + ArrayList currentPossibleMoves = getPossibleMoves(); + int randomMovementNumber = randomNumberGenerator.nextInt(currentPossibleMoves.size()); + return currentPossibleMoves.get(randomMovementNumber); + } + + private MoveEnum determineIntelligentMove() { + ArrayList possibleMoves = getPossibleMoves(); + if (possibleMoves.isEmpty()) { + return MoveEnum.Stay; + } + ArrayList preferredMoves = getPreferredMoves(possibleMoves); + ArrayList pool = preferredMoves.isEmpty() ? possibleMoves : preferredMoves; + return pool.get(randomNumberGenerator.nextInt(pool.size())); + } + + private ArrayList getPreferredMoves(Iterable possibleMoves) { + ArrayList preferredMoves = new ArrayList<>(); + + for (MoveEnum move : possibleMoves) { + switch (move) { + case Up: + if (!hasVisitedPoint(this.x, this.y - 1)) { + preferredMoves.add(move); + } + break; + case Right: + if (!hasVisitedPoint(this.x + 1, this.y)) { + preferredMoves.add(move); + } + break; + case Down: + if (!hasVisitedPoint(this.x, this.y + 1)) { + preferredMoves.add(move); + } + break; + case Left: + if (!hasVisitedPoint(this.x - 1, this.y)) { + preferredMoves.add(move); + } + break; + default: + break; + } + } + + return preferredMoves; + } + + private ArrayList getPossibleMoves() { + ArrayList possibleMoves = new ArrayList<>(); + + if (this.y != 0) { + possibleMoves.add(MoveEnum.Up); + } + if (this.x != this.movementBoundary - 1) { + possibleMoves.add(MoveEnum.Right); + } + if (this.y != this.movementBoundary - 1) { + possibleMoves.add(MoveEnum.Down); + } + if (this.x != 0) { + possibleMoves.add(MoveEnum.Left); + } + + return possibleMoves; + } + + private boolean hasVisitedPoint(int x, int y) { + return visitedPoints.contains(new Point(x, y)); + } +} diff --git a/src/sample/models/GameStatusEnum.java b/src/main/java/sample/models/GameStatusEnum.java similarity index 100% rename from src/sample/models/GameStatusEnum.java rename to src/main/java/sample/models/GameStatusEnum.java diff --git a/src/sample/models/MoveEnum.java b/src/main/java/sample/models/MoveEnum.java similarity index 100% rename from src/sample/models/MoveEnum.java rename to src/main/java/sample/models/MoveEnum.java diff --git a/src/main/java/sample/models/Point.java b/src/main/java/sample/models/Point.java new file mode 100644 index 0000000..870b84d --- /dev/null +++ b/src/main/java/sample/models/Point.java @@ -0,0 +1,88 @@ +package sample.models; + +import java.util.Random; +import sample.interfaces.IPoint; + +public class Point implements IPoint { + int x; + int y; + + public Point() { + x = y = 0; + } + + public Point(int x, int y) { + this.x = x; + this.y = y; + } + + public Point(IPoint p) { + this.x = p.getX(); + this.y = p.getY(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || !(o instanceof Point)) { + return false; + } + + Point p = (Point) o; + + return p.x == this.x && p.y == this.y; + } + + @Override + public int hashCode() { + return 31 * x + y; + } + + public void randomizeLocation(int bound) { + Random random = new Random(); + x = random.nextInt(bound); + y = random.nextInt(bound); + } + + @Override + public int getX() { + return this.x; + } + + @Override + public int getY() { + return this.y; + } + + public void relocate(int x, int y) { + this.x = x; + this.y = y; + } + + protected void moveTo(int x, int y) { + this.x = x; + this.y = y; + } + + protected void moveInDirection(MoveEnum direction) { + switch (direction) { + case Up: + --this.y; + break; + case Right: + ++this.x; + break; + case Down: + ++this.y; + break; + case Left: + --this.x; + break; + default: + break; + } + } +} diff --git a/src/sample/models/Treasure.java b/src/main/java/sample/models/Treasure.java similarity index 82% rename from src/sample/models/Treasure.java rename to src/main/java/sample/models/Treasure.java index 5abc8ec..e6f701c 100644 --- a/src/sample/models/Treasure.java +++ b/src/main/java/sample/models/Treasure.java @@ -2,8 +2,7 @@ // treasure is just a point. makes sense. public class Treasure extends Point { - public Treasure() - { + public Treasure() { super(); } } diff --git a/src/sample/Constants.java b/src/sample/Constants.java deleted file mode 100644 index 8ad6e5f..0000000 --- a/src/sample/Constants.java +++ /dev/null @@ -1,10 +0,0 @@ -package sample; - -class Constants -{ - // Grid sizes - static int DEFAULT_GRIDSIZE = 5; - static int MAX_GRIDSIZE = 15; - static String LIGHT_BLUE = "-fx-background-color:#00BFFF; -fx-opacity:1;"; - static boolean GAME_COMPLETE = false; -} diff --git a/src/sample/Game.java b/src/sample/Game.java deleted file mode 100644 index d864c47..0000000 --- a/src/sample/Game.java +++ /dev/null @@ -1,109 +0,0 @@ -package sample; - -import javafx.stage.Stage; -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 for the game components -class Game implements IGame -{ - private int turnCount = 0; - private GameStatusEnum _status; - IMoveablePoint bot; - Treasure treasure; - - // Game constructor - Game(int gridSizePassed, View gameView) - { - _status = GameStatusEnum.NotStarted; - this.bot = new Bot(gridSizePassed); - this.treasure = new Treasure(); - InitializeGame(gameView); - } - - @Override - public void MakeMove() - { - MoveEnum move = this.bot.DetermineMovement(); - MakeMove(move); - } - - @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; - } - - // Private methods - // game element initialization - private void InitializeGame(View gameView) - { - this.treasure.RandomizeLocation(gameView.gridSize); - this.bot.RandomizeLocation(gameView.gridSize); - while(this.bot.equals(this.treasure)) - { - this.bot.RandomizeLocation(gameView.gridSize); - } - gameView.adjustBotAndTreasureLocations(null, this.bot, this.treasure); - } - - // auto move execution - private void executeTurn(MoveEnum botMovementDirection) - { - // Set status - 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(); - Main.gameView.adjustBotAndTreasureLocations(previousBotPoint, this.bot, this.treasure); - if (_status == GameStatusEnum.Complete) - { - System.out.println("You found the treasure!"); - View.setupEndGameGUI(); - } - } - - // condition checking for whether the bot is on the treasure location - private boolean treasureIsFound() - { - return this.bot.equals(this.treasure); - } - - private void DetermineCurrentStatus() - { - if (treasureIsFound()) - { - _status = GameStatusEnum.Complete; - } - } -} diff --git a/src/sample/Main.java b/src/sample/Main.java deleted file mode 100644 index f0cb314..0000000 --- a/src/sample/Main.java +++ /dev/null @@ -1,65 +0,0 @@ -package sample; - -import javafx.application.Application; -import javafx.scene.Scene; -import javafx.stage.Stage; -import sample.interfaces.IGame; -import sample.models.GameStatusEnum; - -// needs an extensive review before moving forward -public class Main extends Application -{ - private static IGame game; - static int gridSizeForGame; - static View gameView; - - public static Stage startGUI = new Stage(); - - @Override - public void start(Stage primaryStage) - { - View startView = new View(); - startView.startScreen(startGUI, (Game) game); - - // set the stage and start the show - startGUI.setTitle("Treasure Hunt"); - startGUI.setScene(new Scene(startView.gridPane, 400, 200)); - startGUI.show(); - } - - static void setupView(View gameView) - { - // setting up the buttons which go into the UI - gameView.setupButtons( - (o) -> game.MakeMove(), - (o) -> prepareGame(), - (o) -> RunAutoPlay(), - (o) -> backToStartup()); - - gameView.setupDirectionButtons(game, gameView); - } - - // run moveBot continually with the warning on infinite loops suppressed - @SuppressWarnings("InfiniteLoopStatement") - private static void RunAutoPlay() - { - while(game.GetStatus() != GameStatusEnum.Complete) - { - game.MakeMove(); - } - } - - // sets up the Game object with bot and treasure objects in place with coordinates on the grid - static void prepareGame() { - game = new Game(gridSizeForGame, gameView); - } - - // method to return the GUI to the main menu - public static void backToStartup() - { - View.mainGame.close(); - View startView = new View(); - startView.startScreen(startGUI, (Game) game); - startGUI.show(); - } -} \ No newline at end of file diff --git a/src/sample/View.java b/src/sample/View.java deleted file mode 100644 index 4d5da71..0000000 --- a/src/sample/View.java +++ /dev/null @@ -1,274 +0,0 @@ -package sample; - -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; -import java.util.Dictionary; -import java.util.Hashtable; -import java.util.function.Consumer; - -class View -{ - static Stage mainGame; - private static View instance = null; - GridPane gridPane; - int gridSize = Constants.DEFAULT_GRIDSIZE; - Hashtable _pointNodeMap = new Hashtable<>(); - - // Constructor must be private for the Singleton pattern. - View() - { - mainGame = new Stage(); - this.gridPane = new GridPane(); - } - - // setter for grid size - private void setGridSize(int gridSize) - { - this.gridSize = gridSize; - } - - // setting up gaps specific to every grid pane - void setupTheGridPane() { - this.gridPane.setHgap(8); - this.gridPane.setVgap(8); - setupGUI(); - SetupMatrixMap(); - } - - // sets up the labels from the grid size passed into the command line - 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); - } - - // setting up the start screen - void startScreen(Stage startGUI, Game game) - { - // gap between elements in the start screen - this.gridPane.setHgap(8); - this.gridPane.setVgap(8); - this.gridPane.setStyle(Constants.LIGHT_BLUE); - - // elements for the start screen gui stage - 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(); - - // force the field to be numeric only - gridSizeStart.textProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue observable, String oldValue, - String newValue) { - if (!newValue.matches("\\d*")) { - gridSizeStart.setText(newValue.replaceAll("[^\\d]", "")); - } - } - }); - - // button action to start the actual game - submitStart.setOnAction(actionEvent -> prepTheGame(defaultBehaviors, gridSizeStart, startGUI, game)); - // listener on the checkbox such that the defaults are applied, the text is cleared - // the checkbox is also checked on the game running for whether to apply defaults or not - defaultBehaviors.selectedProperty().addListener((observable, oldValue, - newValue) -> gridSizeStart.setText("")); - // add the elements to the grid pane - 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 gridSize; - // if defaults are checked then apply the default 5x5 size - if (defaults.isSelected()) - { - textInput.clear(); - gridSize = Constants.DEFAULT_GRIDSIZE; - } - else { - gridSize = Integer.parseInt(textInput.getText()); - if (gridSize > Constants.MAX_GRIDSIZE) - { - gridSize = Constants.MAX_GRIDSIZE; - alertMsgOnMax(); - } - } - startGUI.close(); - // apply the computed grid size to the grid pane used in main which - // is the central grid pane resource for setting up the game gui - Main.gridSizeForGame = gridSize; - // start the actual game gui - startGameGUI(gridSize, game); - } - - // creates a pop up GUI stage alerting the user that the number provided exceeds the max so the max was used - 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); - } - - // startup of the game gui stage - private void startGameGUI(int parseInt, Game game) { - // creating the new game stage gui and view - View gameView = new View(); - // setting the view grid size - gameView.setGridSize(parseInt); - // setting the main game view to a fresh game view - Main.gameView = gameView; - // establishes the actual View for the game which is managed in the View class - - // creation of the grid pane - gameView.setupTheGridPane(); - - // preparation of the game components for movement and treasure hunting - Main.prepareGame(); - Main.setupView(gameView); - - // size based on arg algorithm - double size = (13.0 * Math.pow(parseInt,2)) + 50; - // set the stage and start the show - 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); - } - } - } - - // sets the text on the nodes to reflect the move just made - public void adjustBotAndTreasureLocations(IPoint previousBotPosition, IPoint currentBotPosition, IPoint treasurePosition) { - if(previousBotPosition != null) - { - Node previousNode = _pointNodeMap.get(previousBotPosition); - ((Text)previousNode).setText(previousBotPosition.getX() + " " + previousBotPosition.getY() + " empty"); - } - - Node currentBotNode = _pointNodeMap.get(currentBotPosition); - ((Text)currentBotNode).setText(currentBotPosition.getX() + " " + currentBotPosition.getY() + " bot"); - - Node treasureNode = _pointNodeMap.get(treasurePosition); - if(treasureNode != currentBotNode) - { - ((Text)treasureNode).setText(treasurePosition.getX() + " " + treasurePosition.getY() + " treasure"); - } - } - - // setting up the button element components of the game gui - void setupButtons(Consumer nextFunction, Consumer resetFunction, Consumer autoPlayFunction, - Consumer leaveGame) - { - // four main buttons - Button nextPlay = new Button("Next move"); - Button reset = new Button("Reset"); - Button autoPlay = new Button("AutoPlay"); - Button exit = new Button("Leave game"); - - // so we have a button for each cardinal movement and then those would go to - // functions in the Game class *moving over to there* - nextPlay.setOnAction(actionEvent -> nextFunction.accept(null)); - reset.setOnAction(actionEvent -> resetFunction.accept(null)); - autoPlay.setOnAction(actionEvent -> autoPlayFunction.accept(null)); - exit.setOnAction(actionEvent -> leaveGame.accept(null)); - - // adding the buttons to the grid pane - gridPane.add(nextPlay,1, gridSize + 1); - gridPane.add(reset,1, gridSize + 2); - gridPane.add(autoPlay,1,gridSize + 3); - gridPane.add(exit, 1, gridSize + 4); - - } - void setupDirectionButtons(IGame game, View gameView) - { - // cardinal buttons - Button upMovement = new Button("Move Up"); - Button downMovement = new Button("Move Down"); - Button leftMovement = new Button("Move Left"); - Button rightMovement = new Button("Move Right"); - upMovement.setOnAction(event -> game.MakeMove(MoveEnum.Up)); - downMovement.setOnAction(event -> game.MakeMove(MoveEnum.Down)); - leftMovement.setOnAction(event -> game.MakeMove(MoveEnum.Left)); - rightMovement.setOnAction(event -> game.MakeMove(MoveEnum.Right)); - - // buttons to the grid pane - gameView.gridPane.add(upMovement, 1, gridSize + 5); - gameView.gridPane.add(downMovement, 1, gridSize + 7); - gameView.gridPane.add(leftMovement, 0, gridSize + 6); - gameView.gridPane.add(rightMovement, 2, gridSize + 6); - } - - public static void setupEndGameGUI() - { - Stage endGameGUI = new Stage(); - GridPane endPane = new GridPane(); - endPane.setHgap(5); - endPane.setVgap(5); - Text endMsg = new Text("Congrats on finding the treasure! Give it another go or quit?"); - Button continueButton = new Button("New Game"); - Button exitButton = new Button("Exit"); - - continueButton.setOnAction(event -> - { - Main.startGUI.close(); - endGameGUI.close(); - Main.backToStartup(); - }); - - exitButton.setOnAction(event -> { - Main.startGUI.close(); - endGameGUI.close(); - System.out.println("Game closed"); - System.exit(69); - }); - - endPane.add(endMsg, 0,0); - endPane.add(continueButton, 0, 2); - endPane.add(exitButton, 0, 3); - - endGameGUI.setScene(new Scene(endPane, 300, 300)); - endGameGUI.show(); - } -} diff --git a/src/sample/interfaces/IGame.java b/src/sample/interfaces/IGame.java deleted file mode 100644 index ad91702..0000000 --- a/src/sample/interfaces/IGame.java +++ /dev/null @@ -1,16 +0,0 @@ -package sample.interfaces; - -import sample.models.Bot; -import sample.models.GameStatusEnum; -import sample.models.MoveEnum; -import sample.models.Treasure; - -public interface IGame { - void MakeMove(); - void MakeMove(MoveEnum move); - GameStatusEnum GetStatus(); - - // TODO: Remote these explicit get calls when we move to a "paint all objects" design. - IPoint getBot(); - IPoint getTreasure(); -} diff --git a/src/sample/models/Bot.java b/src/sample/models/Bot.java deleted file mode 100644 index 3a83eb5..0000000 --- a/src/sample/models/Bot.java +++ /dev/null @@ -1,136 +0,0 @@ -package sample.models; - -import sample.interfaces.IMoveablePoint; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Random; - -public class Bot extends Point implements IMoveablePoint -{ - private final boolean MOVE_INTELLIGENTLY = true; - private HashSet visitedPoints = new HashSet<>(); - private int movementBoundary; - private Random randomNumberGenerator = new Random(System.currentTimeMillis()); - - public Bot(int movementBoundary) - { - super(); - this.movementBoundary = movementBoundary; - } - - // Implementation from IMoveablePoint - // Self-checks that the move is possible. - @Override - public void Move(MoveEnum direction) - { - ArrayList possibleMoves = getPossibleMoves(); - if (possibleMoves.contains(direction)) - { - super.MoveInDirection(direction); - this.visitedPoints.add(new Point(this.x, this.y)); - } - } - - // Implementation from IMoveablePoint - // Determines move based on config values. - @Override - public MoveEnum DetermineMovement() - { - MoveEnum move; - - if(MOVE_INTELLIGENTLY) - { - move = this.DetermineIntelligentMove(); - } - else - { - move = this.DetermineRandomMove(); - } - - return move; - } - - // Internal method to determine the next random move. - private MoveEnum DetermineRandomMove() - { - ArrayList currentPossibleMoves = getPossibleMoves(); - int randomMovementNumber = randomNumberGenerator.nextInt(currentPossibleMoves.size()); - MoveEnum chosenMove = currentPossibleMoves.get(randomMovementNumber); - - return chosenMove; - } - - // Internal method to determine the next move intelligently. - private MoveEnum DetermineIntelligentMove() - { - // Determine which moves are possible, then add them to the list - ArrayList possibleMoves = getPossibleMoves(); - ArrayList preferredMoves = getPreferredMoves(possibleMoves); - MoveEnum move = MoveEnum.Stay; - if (preferredMoves.size() != 0) - { - int randomMovementNumber = this.randomNumberGenerator.nextInt(preferredMoves.size()); - MoveEnum chosenMove = preferredMoves.get(randomMovementNumber); - if (possibleMoves.contains(chosenMove)) { - move = chosenMove; - } - } - else - { - int randomMovementNumber = randomNumberGenerator.nextInt(possibleMoves.size()); - MoveEnum chosenMove = possibleMoves.get(randomMovementNumber); - move = chosenMove; - } - - return move; - } - - private ArrayList getPreferredMoves(Iterable possibleMoves) - { - ArrayList preferredMoves = new ArrayList<>(); - - for (MoveEnum move : possibleMoves) - { - switch (move) - { - case Up: - if(!hasVisitedPoint(this.x, this.y - 1)) - preferredMoves.add(move); - break; - case Right: - if(!hasVisitedPoint(this.x + 1, this.y)) - preferredMoves.add(move); - break; - case Down: - if(!hasVisitedPoint(this.x, this.y + 1)) - preferredMoves.add(move); - break; - case Left: - if(!hasVisitedPoint(this.x - 1, this.y)) - preferredMoves.add(move); - break; - } - } - - return preferredMoves; - } - - // Internal method to determine which moves are possible. - private ArrayList getPossibleMoves() - { - ArrayList possibleMoves = new ArrayList(); - - if(this.y != 0) possibleMoves.add(MoveEnum.Up); - if(this.x != this.movementBoundary - 1) possibleMoves.add(MoveEnum.Right); - if(this.y != this.movementBoundary - 1) possibleMoves.add(MoveEnum.Down); - if(this.x != 0) possibleMoves.add(MoveEnum.Left); - - return possibleMoves; - } - - private boolean hasVisitedPoint(int x, int y) - { - return visitedPoints.contains(new Point(x, y)); - } -} diff --git a/src/sample/models/Point.java b/src/sample/models/Point.java deleted file mode 100644 index b3c3818..0000000 --- a/src/sample/models/Point.java +++ /dev/null @@ -1,96 +0,0 @@ -package sample.models; - -import sample.interfaces.IMoveablePoint; -import sample.interfaces.IPoint; - -import java.util.Random; - -public class Point implements IPoint -{ - int x; - int y; - - Point() - { - x = y = 0; - } - - public Point(int x, int y) - { - this.x = x; - this.y = y; - } - - public Point(IPoint p) - { - this.x = p.getX(); - this.y = p.getY(); - } - - @Override - public boolean equals(Object o) - { - if (this == o) - { - return true; - } - - if(o == null || !(o instanceof Point)) - { - return false; - } - - Point p = (Point) o; - - return p.x == this.x && p.y == this.y; - } - - @Override - public int hashCode() - { - String hashString = this.x + "," + this.y; - return hashString.hashCode(); - } - - public void RandomizeLocation(int bound) - { - Random random = new Random(System.currentTimeMillis()); - x = random.nextInt(bound); - y = random.nextInt(bound); - } - - public int getX() - { - return this.x; - } - - public int getY() - { - return this.y; - } - - protected void MoveTo(int x, int y) - { - this.x = x; - this.y = y; - } - - protected void MoveInDirection(MoveEnum direction) - { - switch (direction) - { - case Up: - --this.y; - break; - case Right: - ++this.x; - break; - case Down: - ++this.y; - break; - case Left: - --this.x; - break; - } - } -} diff --git a/src/test/java/sample/GameTest.java b/src/test/java/sample/GameTest.java new file mode 100644 index 0000000..b97fe54 --- /dev/null +++ b/src/test/java/sample/GameTest.java @@ -0,0 +1,55 @@ +package sample; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import sample.models.Bot; +import sample.models.GameStatusEnum; +import sample.models.MoveEnum; +import sample.models.Treasure; + +class GameTest { + + @Test + void firstMoveSetsStatusToInProgress() { + RecordingListener listener = new RecordingListener(); + Game game = new Game(5, listener); + Bot bot = (Bot) game.getBot(); + bot.relocate(2, 2); + assertEquals(GameStatusEnum.NotStarted, game.getStatus()); + game.makeMove(MoveEnum.Up); + assertEquals(GameStatusEnum.InProgress, game.getStatus()); + } + + @Test + void movingOntoTreasureCompletesGame() { + RecordingListener listener = new RecordingListener(); + Game game = new Game(5, listener); + Bot bot = (Bot) game.getBot(); + Treasure treasure = (Treasure) game.getTreasure(); + bot.relocate(0, 0); + treasure.relocate(1, 0); + listener.reset(); + + game.makeMove(MoveEnum.Right); + + assertEquals(GameStatusEnum.Complete, game.getStatus()); + assertEquals(1, listener.updateCount); + } + + private static final class RecordingListener implements GameBoardListener { + int updateCount; + + @Override + public void onPositionsChanged( + sample.interfaces.IPoint previousBotPosition, + sample.interfaces.IPoint currentBotPosition, + sample.interfaces.IPoint treasurePosition) { + updateCount++; + } + + void reset() { + updateCount = 0; + } + } +} diff --git a/src/test/java/sample/models/PointTest.java b/src/test/java/sample/models/PointTest.java new file mode 100644 index 0000000..e3aa967 --- /dev/null +++ b/src/test/java/sample/models/PointTest.java @@ -0,0 +1,49 @@ +package sample.models; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import org.junit.jupiter.api.Test; + +class PointTest { + + @Test + void equalsAndHashCodeUseCoordinates() { + Point a = new Point(2, 3); + Point b = new Point(2, 3); + Point c = new Point(2, 4); + + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + assertNotEquals(a, c); + } + + @Test + void moveInDirectionUpdatesCoordinates() { + Bot bot = new Bot(5); + bot.relocate(2, 2); + bot.move(MoveEnum.Up); + assertEquals(2, bot.getX()); + assertEquals(1, bot.getY()); + + bot.move(MoveEnum.Right); + assertEquals(3, bot.getX()); + assertEquals(1, bot.getY()); + } + + @Test + void botRespectsGridBoundaries() { + Bot bot = new Bot(3); + bot.relocate(0, 0); + bot.move(MoveEnum.Left); + assertEquals(0, bot.getX()); + bot.move(MoveEnum.Up); + assertEquals(0, bot.getY()); + + bot.relocate(2, 2); + bot.move(MoveEnum.Right); + assertEquals(2, bot.getX()); + bot.move(MoveEnum.Down); + assertEquals(2, bot.getY()); + } +} From f68ee7174d60c66fe6db08682cd81a819af4f926 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 26 Sep 2026 16:26:46 +0000 Subject: [PATCH 2/2] CI: use actions/setup-java@v5 Co-authored-by: Joel Town Road --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2f4fd5..0a9ec9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: "21" distribution: temurin