From 70ca3b7e8c0e4a0560d022e78f170ef5d41e2e43 Mon Sep 17 00:00:00 2001 From: nyanyan <46193012+zentox@users.noreply.github.com> Date: Mon, 19 Jun 2023 18:12:03 +0200 Subject: [PATCH] Feature/solution (#1) * Fix checkstyle * Add dependency to fopbot * Custom world implementation * Interface for creating problem solver * Simple maze solver algorithm * Visualizer for maze * Add default constructors * Json Junit Test Support * Snapshot dependency * Improve code namings * numberOfSteps should also contain the direction as input * Add color to start and end point * Change x to dx * Remove obsolete comments * Remove obsolete comments * Remove obsolete code * Remove obsolete code * Remove helper function since they have the same signature as the main function * Fix no color end point * Change naming * CI --- .github/workflows/gradle-check.yml | 43 +++++ build.gradle.kts | 3 +- gradle/libs.versions.toml | 13 +- settings.gradle.kts | 9 + src/main/java/h06/problems/MazeSolver.java | 9 + .../h06/problems/MazeSolverIterative.java | 64 +++++++ .../h06/problems/MazeSolverRecursive.java | 63 +++++++ src/main/java/h06/problems/ProblemSolver.java | 46 +++++ src/main/java/h06/ui/MazeVisualizer.java | 86 +++++++++ src/main/java/h06/ui/ProblemVisualizer.java | 51 ++++++ src/main/java/h06/world/DirectionVector.java | 108 +++++++++++ src/main/java/h06/world/Field.java | 70 ++++++++ src/main/java/h06/world/Wall.java | 47 +++++ src/main/java/h06/world/World.java | 169 ++++++++++++++++++ src/test/java/h06/ExampleJUnitTest.java | 2 +- 15 files changed, 777 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/gradle-check.yml create mode 100644 src/main/java/h06/problems/MazeSolver.java create mode 100644 src/main/java/h06/problems/MazeSolverIterative.java create mode 100644 src/main/java/h06/problems/MazeSolverRecursive.java create mode 100644 src/main/java/h06/problems/ProblemSolver.java create mode 100644 src/main/java/h06/ui/MazeVisualizer.java create mode 100644 src/main/java/h06/ui/ProblemVisualizer.java create mode 100644 src/main/java/h06/world/DirectionVector.java create mode 100644 src/main/java/h06/world/Field.java create mode 100644 src/main/java/h06/world/Wall.java create mode 100644 src/main/java/h06/world/World.java diff --git a/.github/workflows/gradle-check.yml b/.github/workflows/gradle-check.yml new file mode 100644 index 0000000..fad293c --- /dev/null +++ b/.github/workflows/gradle-check.yml @@ -0,0 +1,43 @@ +name: Gradle Check +on: + workflow_dispatch: + pull_request: + push: + branches: + - master + - main +jobs: + gradle_check: + name: "Check on ${{ matrix.os }}" + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + - name: Restore LFS Cache + uses: actions/cache@v2 + id: lfs-cache + with: + path: .git/lfs + key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }} + - name: Git LFS Pull + run: | + git lfs pull + git add . + git reset --hard + - name: Set up JDK + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: '17' + - name: Validate Gradle Wrapper + uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Gradle check + run: ./gradlew clean check --continue diff --git a/build.gradle.kts b/build.gradle.kts index 4152966..29b1d54 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,8 @@ jagr { dependencies { implementation(libs.annotations) implementation(libs.algoutils.student) - testImplementation(libs.junit.core) + testImplementation(libs.bundles.junit) + implementation(libs.fopbot) } application { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 93ece1c..b02b636 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,12 +1,17 @@ [versions] -algoutils = "0.5.0" +algoutils = "0.6.3" + +[plugins] +style = { id = "org.sourcegrade.style", version = "2.1.0" } +jagr-gradle = { id = "org.sourcegrade.jagr-gradle", version = "0.7.0" } [libraries] annotations = "org.jetbrains:annotations:23.0.0" algoutils-student = { module = "org.tudalgo:algoutils-student", version.ref = "algoutils" } algoutils-tutor = { module = "org.tudalgo:algoutils-tutor", version.ref = "algoutils" } junit-core = { module = "org.junit.jupiter:junit-jupiter", version = "5.9.1" } +junit-pioneer = { module = "org.junit-pioneer:junit-pioneer", version = "1.7.1" } +fopbot = { module = "org.tudalgo:fopbot", version = "0.5.0-SNAPSHOT" } -[plugins] -style = { id = "org.sourcegrade.style", version = "2.1.0" } -jagr-gradle = { id = "org.sourcegrade.jagr-gradle", version = "0.7.0" } +[bundles] +junit = ["junit-core", "junit-pioneer"] diff --git a/settings.gradle.kts b/settings.gradle.kts index 3e39466..8ff6b5a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,8 +1,17 @@ dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { + maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") mavenCentral() } } +pluginManagement { + repositories { + maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") + mavenCentral() + gradlePluginPortal() + } +} + rootProject.name = "H06-Root" diff --git a/src/main/java/h06/problems/MazeSolver.java b/src/main/java/h06/problems/MazeSolver.java new file mode 100644 index 0000000..a809de8 --- /dev/null +++ b/src/main/java/h06/problems/MazeSolver.java @@ -0,0 +1,9 @@ +package h06.problems; + +/** + * A solver for a maze. + * + * @author Nhan Huynh + */ +public interface MazeSolver extends ProblemSolver { +} diff --git a/src/main/java/h06/problems/MazeSolverIterative.java b/src/main/java/h06/problems/MazeSolverIterative.java new file mode 100644 index 0000000..ee7de31 --- /dev/null +++ b/src/main/java/h06/problems/MazeSolverIterative.java @@ -0,0 +1,64 @@ +package h06.problems; + +import h06.world.DirectionVector; +import h06.world.World; + +import java.awt.Point; + +/** + * An iterative implementation of a maze solver. The solver uses the left-hand rule to find a path from the start to + * the end of the maze. + * + * @author Nhan Huynh + */ +public class MazeSolverIterative implements MazeSolver { + + /** + * Constructs an iterative maze solver. + */ + public MazeSolverIterative() { + } + + @Override + public DirectionVector nextStep(World world, Point p, DirectionVector d) { + DirectionVector next = d.rotate270(); + for (int i = 0; i < DirectionVector.values().length; i++) { + if (!world.isBlocked(p, next)) { + return next; + } + + next = next.rotate90(); + } + return d; + } + + @Override + public int numberOfSteps(World world, Point s, Point e, DirectionVector d) { + int steps = 0; + Point next = s; + DirectionVector nextDir = d; + while (!next.equals(e)) { + nextDir = nextStep(world, next, nextDir); + next = nextDir.getMovement(next); + steps++; + } + steps++; + return steps; + } + + @Override + public Point[] solve(World world, Point s, Point e, DirectionVector d) { + int size = numberOfSteps(world, s, e, d); + Point[] path = new Point[size]; + int index = 0; + Point next = s; + DirectionVector dir = d; + while (!next.equals(e)) { + path[index++] = next; + dir = nextStep(world, next, dir); + next = dir.getMovement(next); + } + path[index] = next; + return path; + } +} diff --git a/src/main/java/h06/problems/MazeSolverRecursive.java b/src/main/java/h06/problems/MazeSolverRecursive.java new file mode 100644 index 0000000..5342108 --- /dev/null +++ b/src/main/java/h06/problems/MazeSolverRecursive.java @@ -0,0 +1,63 @@ +package h06.problems; + +import h06.world.DirectionVector; +import h06.world.World; + +import java.awt.Point; + +/** + * A recursive implementation of a maze solver. The solver uses the left-hand rule to find a path from the start to + * the end of the maze. + * + * @author Nhan Huynh + */ +public class MazeSolverRecursive implements MazeSolver { + + /** + * Constructs a recursive maze solver. + */ + public MazeSolverRecursive() { + } + + @Override + public DirectionVector nextStep(World world, Point p, DirectionVector d) { + return !world.isBlocked(p, d) ? d : nextStep(world, p, d.rotate90()); + } + + @Override + public int numberOfSteps(World world, Point s, Point e, DirectionVector d) { + if (s.equals(e)) { + return 1; + } + DirectionVector next = nextStep(world, s, d.rotate270()); + return 1 + numberOfSteps(world, next.getMovement(s), e, next); + } + + @Override + public Point[] solve(World world, Point s, Point e, DirectionVector d) { + int size = numberOfSteps(world, s, e, d); + Point[] path = new Point[size]; + solveHelper(world, s, e, d, path, 0); + return path; + } + + /** + * Helper method for solve. Returns the path from p to end. + * + * @param world the world to solve the maze in + * @param p the current point + * @param e the end point + * @param d the current direction + * @param path the path calculated so far from s to p + * @param index the index of the next free spot in path + */ + private void solveHelper(World world, Point p, Point e, DirectionVector d, Point[] path, int index) { + if (p.equals(e)) { + path[index] = p; + return; + } + path[index++] = p; + DirectionVector next = nextStep(world, p, d.rotate270()); + solveHelper(world, next.getMovement(p), e, next, path, index); + } +} diff --git a/src/main/java/h06/problems/ProblemSolver.java b/src/main/java/h06/problems/ProblemSolver.java new file mode 100644 index 0000000..4d6b3ca --- /dev/null +++ b/src/main/java/h06/problems/ProblemSolver.java @@ -0,0 +1,46 @@ +package h06.problems; + +import h06.world.DirectionVector; +import h06.world.World; + +import java.awt.Point; + +/** + * A solver for a problem. + * + * @author Nhan Huynh + */ +public interface ProblemSolver { + + /** + * Computes the next step of solving the problem. + * + * @param world the world to solve the problem in + * @param p the current position + * @param d the current direction + * @return the next step of solving the problem + */ + DirectionVector nextStep(World world, Point p, DirectionVector d); + + /** + * Computes the number of steps needed to solve the problem. + * + * @param world the world to solve the problem in + * @param s the start position + * @param e the end position + * @param d the starting direction + * @return the number of steps needed to solve the problem + */ + int numberOfSteps(World world, Point s, Point e, DirectionVector d); + + /** + * Computes the path to solve the problem. + * + * @param world the world to solve the problem in + * @param s the start position + * @param e the end position + * @param d the starting direction + * @return the path to solve the problem + */ + Point[] solve(World world, Point s, Point e, DirectionVector d); +} diff --git a/src/main/java/h06/ui/MazeVisualizer.java b/src/main/java/h06/ui/MazeVisualizer.java new file mode 100644 index 0000000..f54ae47 --- /dev/null +++ b/src/main/java/h06/ui/MazeVisualizer.java @@ -0,0 +1,86 @@ +package h06.ui; + +import fopbot.Direction; +import fopbot.Robot; +import h06.problems.ProblemSolver; +import h06.world.DirectionVector; +import h06.world.World; + +import java.awt.Color; +import java.awt.Point; + +/** + * Visualizes a maze problem. + * + * @author Nhan Huynh + */ +public class MazeVisualizer implements ProblemVisualizer { + + /** + * The world to visualize. + */ + private World world; + + @Override + public void init(World world) { + this.world = world; + int width = world.getWidth(); + int height = world.getHeight(); + fopbot.World.setSize(width, height); + + // Place walls + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + if (world.isBlocked(x, y, true)) { + fopbot.World.placeHorizontalWall(x, y); + } + if (world.isBlocked(x, y, false)) { + fopbot.World.placeVerticalWall(x, y); + } + } + } + } + + @Override + public void show(boolean visible) { + fopbot.World.setVisible(visible); + } + + @Override + public void run(ProblemSolver solver, Point s, Point e) { + Point[] path = solver.solve(world, s, e, DirectionVector.UP); + fopbot.World.getGlobalWorld().setFieldColor(s.x, s.y, Color.BLUE); + fopbot.World.getGlobalWorld().setFieldColor(e.x, e.y, Color.YELLOW); + Robot mazeRunner = new Robot(s.x, s.y); + + for (int i = 1; i < path.length; i++) { + Point p = path[i]; + int x = mazeRunner.getX(); + int y = mazeRunner.getY(); + + // Compute movement direction + Direction direction; + if (p.x > x && p.y == y) { + direction = Direction.RIGHT; + } else if (p.x < x && p.y == y) { + direction = Direction.LEFT; + } else if (p.x == x && p.y > y) { + direction = Direction.UP; + } else { + direction = Direction.DOWN; + } + + // Turn robot to the correct direction and then move + while (mazeRunner.getDirection() != direction) { + mazeRunner.turnLeft(); + } + mazeRunner.move(); + } + mazeRunner.turnOff(); + } + + @Override + public void setDelay(int delay) { + fopbot.World.setDelay(delay); + } +} diff --git a/src/main/java/h06/ui/ProblemVisualizer.java b/src/main/java/h06/ui/ProblemVisualizer.java new file mode 100644 index 0000000..f1e5dae --- /dev/null +++ b/src/main/java/h06/ui/ProblemVisualizer.java @@ -0,0 +1,51 @@ +package h06.ui; + +import h06.problems.ProblemSolver; +import h06.world.World; + +import java.awt.Point; + +/** + * Visualizes a problem. + * + * @author Nhan Huynh + */ +public interface ProblemVisualizer { + + /** + * Initializes the visualizer with the given world to visualize. + * + * @param world the world to visualize + */ + void init(World world); + + /** + * Shows or hides the visualizer. + * + * @param visible whether the visualizer should be visible + */ + void show(boolean visible); + + /** + * Shows the visualizer. + */ + default void show() { + show(true); + } + + /** + * Runs the visualizer with the given solver and start and end points to solve the problem. + * + * @param solver the solver to use + * @param s the start point + * @param e the end point + */ + void run(ProblemSolver solver, Point s, Point e); + + /** + * Sets the delay between each step in milliseconds. + * + * @param delay the delay between each step in milliseconds + */ + void setDelay(int delay); +} diff --git a/src/main/java/h06/world/DirectionVector.java b/src/main/java/h06/world/DirectionVector.java new file mode 100644 index 0000000..689427c --- /dev/null +++ b/src/main/java/h06/world/DirectionVector.java @@ -0,0 +1,108 @@ +package h06.world; + +import java.awt.Point; + +/** + * The DirectionVector enum represents vectors in four directions: UP, RIGHT, DOWN, and LEFT. + * + *

Each DirectionVector has an x and y component that can be used for movement or other purposes. + * + * @author Nhan Huynh + */ +public enum DirectionVector { + + /** + * The direction vector pointing up. + */ + UP(0, 1), + + /** + * The direction vector pointing right. + */ + RIGHT(1, 0), + + /** + * The direction vector pointing down. + */ + DOWN(0, -1), + + /** + * The direction vector pointing left. + */ + LEFT(-1, 0); + + /** + * The x component of the direction vector. + */ + private final int dx; + + /** + * The y component of the direction vector. + */ + private final int dy; + + /** + * Constructs a new direction vector with the given x and y components. + * + * @param dx the x component of the direction vector + * @param dy the y component of the direction vector + */ + DirectionVector(int dx, int dy) { + this.dx = dx; + this.dy = dy; + } + + /** + * Returns the direction vector counterclockwise to this direction vector (90 degrees to the left). + * + * @return the direction vector counterclockwise to this direction vector + */ + public DirectionVector rotate270() { + return this == UP ? LEFT : this == LEFT ? DOWN : this == DOWN ? RIGHT : UP; + } + + /** + * Returns the direction vector clockwise to this direction vector (90 degrees to the right). + * + * @return the direction vector clockwise to this direction vector + */ + public DirectionVector rotate90() { + if (this == UP) { + return RIGHT; + } else if (this == RIGHT) { + return DOWN; + } else if (this == DOWN) { + return LEFT; + } else { + return UP; + } + } + + /** + * Returns the x component of the direction vector. + * + * @return the x component of the direction vector + */ + public int getDx() { + return dx; + } + + /** + * Returns the y component of the direction vector. + * + * @return the y component of the direction vector + */ + public int getDy() { + return dy; + } + + /** + * Returns a point that is the result of adding this direction vector to the given point. + * + * @param p The Point to add to. + * @return a point that is the result of adding this direction vector to the given point + */ + public Point getMovement(Point p) { + return new Point(p.x + dx, p.y + dy); + } +} diff --git a/src/main/java/h06/world/Field.java b/src/main/java/h06/world/Field.java new file mode 100644 index 0000000..e41c3cc --- /dev/null +++ b/src/main/java/h06/world/Field.java @@ -0,0 +1,70 @@ +package h06.world; + +import fopbot.FieldEntity; + +/** + * A field in a world which can contains entities. + * + * @author Nhan Huynh + */ +public class Field { + + /** + * The initial capacity of the array of entities. + */ + private static final int INITIAL_CAPACITY = 10; + + /** + * The entities on the field. + */ + private FieldEntity[] entities; + + /** + * The index of the next free slot in the array of entities. + */ + private int nextFreeIndex; + + /** + * Creates a new field with the initial capacity. + */ + public Field() { + this(INITIAL_CAPACITY); + } + + /** + * Creates a new field with the given capacity. + * + * @param capacity the initial capacity of the field + */ + public Field(int capacity) { + this.entities = new FieldEntity[capacity]; + this.nextFreeIndex = 0; + } + + /** + * Adds the given entity to the field. + * + * @param entity the entity to add + */ + public void addEntity(FieldEntity entity) { + // If array is full, increase its size + if (nextFreeIndex == entities.length) { + FieldEntity[] newEntities = new FieldEntity[entities.length * 2]; + for (int i = 0; i < entities.length; i++) { + newEntities[i] = entities[i]; + } + entities = newEntities; + } + entities[nextFreeIndex] = entity; + nextFreeIndex++; + } + + /** + * Returns the entities on the field. + * + * @return the entities on the field + */ + public FieldEntity[] getEntities() { + return entities; + } +} diff --git a/src/main/java/h06/world/Wall.java b/src/main/java/h06/world/Wall.java new file mode 100644 index 0000000..9b538db --- /dev/null +++ b/src/main/java/h06/world/Wall.java @@ -0,0 +1,47 @@ +package h06.world; + +import fopbot.FieldEntity; + +/** + * A wall with orientation. + * + * @author Nhan Huynh + */ +public class Wall extends FieldEntity { + + /** + * Whether the wall is horizontal or vertical. + */ + private final boolean horizontal; + + /** + * Creates a vertical wall at the given position. + * + * @param x the x coordinate of the wall + * @param y the y coordinate of the wall + */ + public Wall(int x, int y) { + this(x, y, false); + } + + /** + * Creates a wall at the given position. + * + * @param x the x coordinate of the wall + * @param y the y coordinate of the wall + * @param horizontal whether the wall is horizontal or vertical + */ + public Wall(int x, int y, boolean horizontal) { + super(x, y); + this.horizontal = horizontal; + } + + /** + * Returns whether the wall is horizontal or vertical. + * + * @return whether the wall is horizontal or vertical + */ + public boolean isHorizontal() { + return horizontal; + } +} diff --git a/src/main/java/h06/world/World.java b/src/main/java/h06/world/World.java new file mode 100644 index 0000000..ae8f537 --- /dev/null +++ b/src/main/java/h06/world/World.java @@ -0,0 +1,169 @@ +package h06.world; + +import fopbot.Field; +import fopbot.FieldEntity; + +import java.awt.Point; + +/** + * Represents a 2D world. + * + * @author Nhan Huynh + */ +public class World { + + /** + * The width of the world. + */ + private final int width; + + /** + * The height of the world. + */ + private final int height; + + /** + * The fields of the world. + */ + private final Field[][] fields; + + /** + * Creates a new world with the given width and height. + * + * @param width the width of the world + * @param height the height of the world + */ + public World(int width, int height) { + this.width = width; + this.height = height; + this.fields = new Field[width][height]; + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + fields[x][y] = new Field(); + } + } + } + + /** + * Returns the width of the world. + * + * @return the width of the world + */ + public int getWidth() { + return width; + } + + /** + * Returns the height of the world. + * + * @return the height of the world + */ + public int getHeight() { + return height; + } + + /** + * Places a wall at the given position. The wall will separate the field from its neighbor on the right or below. + * + * @param x the x coordinate of the wall + * @param y the y coordinate of the wall + * @param horizontal the orientation of the wall + */ + public void placeWall(int x, int y, boolean horizontal) { + fields[x][y].getEntities().add(new Wall(x, y, horizontal)); + } + + /** + * Checks if the given position is outside the world. + * + * @param x the x coordinate to check + * @param y the y coordinate to check + * @return {@code true} if the position is outside the world, {@code false} otherwise + */ + public boolean isOutside(int x, int y) { + return x < 0 || x >= width || y < 0 || y >= height; + } + + /** + * Check if the given point plus the direction vector is outside the world. + * + * @param p the point to check + * @param d the direction vector to add to the point + * @return {@code true} if the position is outside the world, {@code false} otherwise + */ + public boolean isOutside(Point p, DirectionVector d) { + Point pos = d.getMovement(p); + return isOutside(pos.x, pos.y); + } + + /** + * Checks if the given position is blocked by any boundaries. + * + * @param x the x coordinate to check + * @param y the y coordinate to check + * @param horizontal the orientation of the wall + * @return {@code true} if the position is blocked, {@code false} otherwise + */ + public boolean isBlocked(int x, int y, boolean horizontal) { + if (isOutside(x, y)) { + return true; + } + + for (FieldEntity entity : fields[x][y].getEntities()) { + if (entity instanceof Wall wall) { + if (wall.isHorizontal() == horizontal) { + return true; + } + } + } + return false; + } + + /** + * Checks if the given position plus direction vector is blocked by any boundaries. + * + * @param p the point to check + * @param d the direction vector to add to the point + * @return {@code true} if the position is blocked, {@code false} otherwise + */ + public boolean isBlocked(Point p, DirectionVector d) { + // Outside + if (isOutside(p, d)) { + return true; + } + Point r = d.getMovement(p); + int x; + int y; + boolean horizontal; + if (p.x > r.x) { + // Right, vertical check + x = r.x; + y = r.y; + horizontal = false; + } else if (p.x < r.x) { + // Left vertical check, the wall is on the right of the field, that's why we need to check the left of + // the field + x = r.x - 1; + y = r.y; + if (x < 0) { + return false; + } + horizontal = false; + } else if (p.y > r.y) { + // Down horizontal check + x = r.x; + y = r.y; + horizontal = true; + } else { + // Up horizontal check, the wall is on the top of the field, that's why we need to check the bottom of + // the field + x = r.x; + y = r.y - 1; + horizontal = true; + if (y < 0) { + return false; + } + } + return isBlocked(x, y, horizontal); + } +} diff --git a/src/test/java/h06/ExampleJUnitTest.java b/src/test/java/h06/ExampleJUnitTest.java index 70753f2..cba2577 100644 --- a/src/test/java/h06/ExampleJUnitTest.java +++ b/src/test/java/h06/ExampleJUnitTest.java @@ -2,7 +2,7 @@ import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * An example JUnit test class.