diff --git a/CONTRACT.md b/CONTRACT.md index 2f4b5d9..4656be2 100644 --- a/CONTRACT.md +++ b/CONTRACT.md @@ -8,79 +8,83 @@ There are two layers, and keeping them separate is the whole point: - **The wire format** — a raw `int[][]` bitmask — is how a maze crosses from the site (which generates mazes in JavaScript) into Java. It is an implementation - detail. Only the library's *implementer* (Owen) touches it. -- **The student-facing API** — `Maze`, `Cell`, `Robot`, `MazeSolver` — is what - new students write code against. It is bitmask-free by design. + detail; students never see it. +- **The student-facing API** — `Maze`, `Cell`, `Robot`, `Direction`, and the + concrete `GridMaze` — is what students write code against. It is bitmask-free + by design. ## Wire format (implementer-facing) A maze arrives as `int[][] maze`, indexed `maze[row][col]`. Every row has the same length. Each cell's value is a bitmask of which sides are **open** (carved -through, i.e. passable), using the same convention as the `maze-generator` npm -package the lesson site uses to build mazes: - -| Direction | Bit | Value | -|-----------|------|-------| -| NORTH | 0001 | 1 | -| SOUTH | 0010 | 2 | -| EAST | 0100 | 4 | -| WEST | 1000 | 8 | - -A cell open to the north and east only is `NORTH \| EAST == 5`. A cell open on -all four sides is `15`. These constants are exposed as -`com.frc2713.mazesolver.Direction.{NORTH,SOUTH,EAST,WEST}`. - -The maze is assumed internally consistent: if `maze[r][c]` has `EAST` set, then -`maze[r][c + 1]` has `WEST` set, and so on. Implementations need not validate -this beyond basic shape checks. +through, i.e. passable), using the same bit values as the `maze-generator` npm +package the lesson site uses to build mazes. The bits are named the way they +read on screen (`UP`/`DOWN`/`LEFT`/`RIGHT`), which correspond to the generator's +compass directions: + +| Direction | Compass | Bit | Value | +|-----------|---------|------|-------| +| UP | north | 0001 | 1 | +| DOWN | south | 0010 | 2 | +| RIGHT | east | 0100 | 4 | +| LEFT | west | 1000 | 8 | + +A cell open upward and to the right only is `UP.bit() \| RIGHT.bit() == 5`. A +cell open on all four sides is `15`. `com.frc2713.mazesolver.Direction` is an +`enum { UP, DOWN, LEFT, RIGHT }`; each constant's `bit()` returns its value +above. + +The maze is assumed internally consistent: if `maze[r][c]` has `RIGHT` set, +then `maze[r][c + 1]` has `LEFT` set, and so on. Implementations need not +validate this beyond basic shape checks. Start is always the top-left cell (`[0][0]`); the goal is always the -bottom-right cell (`[rows - 1][cols - 1]`). Screen-relative directions map to -the bitmask as: **up** = NORTH, **down** = SOUTH, **right** = EAST, -**left** = WEST. +bottom-right cell (`[rows - 1][cols - 1]`). ## Student-facing API (what the site depends on) -These interfaces are shipped by the library. New students never see the bitmask; -they use these types. See the Javadoc in each interface for details. +These types are shipped by the library. Students never see the bitmask; they use +these. See the Javadoc in each type for details. ```java package com.frc2713.mazesolver; +public enum Direction { // UP=1, DOWN=2, RIGHT=4, LEFT=8 + UP, DOWN, LEFT, RIGHT; + int bit(); // its bit in a cell's open-sides mask + Direction opposite(); // turn around + Direction clockwise(); // turn right (UP→RIGHT→DOWN→LEFT) + Direction counterClockwise(); // turn left +} + public interface Cell { // one square; plain yes/no queries int row(); int col(); - boolean wallUp(); boolean wallDown(); - boolean wallLeft(); boolean wallRight(); + boolean wall(Direction dir); // is that side a wall? boolean isStart(); boolean isGoal(); } public interface Robot { // walks the maze one cell at a time + boolean readWallSensor(Direction dir); // true if that side is a wall + void drive(Direction dir); // move one cell; no-op into a wall + Direction facing(); // the way it last drove (starts RIGHT) + boolean atGoal(); int row(); int col(); Cell cell(); - boolean canMoveUp(); boolean canMoveDown(); - boolean canMoveLeft(); boolean canMoveRight(); - void moveUp(); void moveDown(); - void moveLeft(); void moveRight(); // throw IllegalStateException if blocked - boolean atGoal(); - int[][] trail(); // {row,col} pairs, start..current + int[][] trail(); // {row,col} pairs, start..current } public interface Maze { // the maze, hiding the int[][] int rows(); int cols(); Cell cellAt(int row, int col); - Robot robot(); // fresh robot at the start cell + Robot robot(); // fresh robot at the start cell boolean isGoalCell(int row, int col); } - -public interface MazeSolver { // the ALGORITHM — a student writes this - void solve(Robot robot); // move the robot until robot.atGoal() -} ``` ### Entry point the site relies on -The library **must** provide a concrete `Maze` the site can build directly from -the wire format: +The library ships the concrete `Maze` the site builds directly from the wire +format: ```java public final class GridMaze implements Maze { @@ -89,15 +93,28 @@ public final class GridMaze implements Maze { ``` The site constructs one per lesson snippet (`new GridMaze(grid)`), hands -`maze.robot()` to the student's `MazeSolver`, then reads `robot.trail()` to draw -the path. `GridMaze` (and the `Cell`/`Robot` it returns) is the implementer's -job; everything else in the student-facing API is an interface the student's -solver is written against. +`maze.robot()` to the student's algorithm, then reads `robot.trail()` to draw +the path. A `drive` into a wall is a no-op (no move, no trail entry, `facing()` +unchanged) — the site harness and lessons rely on that. `GridMaze` and the +`Cell`/`Robot` it returns are shipped by the library, not the site. + +### The library's own solver + +Separately from the interactive API, the library ships a batch solver: + +```java +public interface MazeSolver { int[][] solve(int[][] maze); } +``` + +`com.frc2713.mazesolver.DefaultMazeSolver` implements it as a breadth-first +search, returning the shortest path from start to goal as `{row, col}` pairs +(starting `{0, 0}`), or `new int[0][]` if none exists. It throws +`IllegalArgumentException` for a null, empty, or non-rectangular maze. ## Changing this contract If either side needs the shape of these types, the bitmask values, the screen-relative direction mapping, or the start/goal convention to change, update this file first and treat it as the single source of truth — the site's -`maze-generator` usage and this library's `Direction` constants both need to -move together. +`maze-generator` usage and this library's `Direction` enum both need to move +together. diff --git a/README.md b/README.md index cd67c40..98cd5e2 100644 --- a/README.md +++ b/README.md @@ -40,22 +40,23 @@ CI (`.github/workflows/build.yml`) additionally verifies, on every push: ## Project layout -The student-facing API is a set of interfaces; the concrete implementations -(the parts that read the bitmask) are the implementer's job. +The student-facing API is a set of interfaces plus the concrete `GridMaze` that +reads the bitmask so students never have to. ``` src/main/java/com/frc2713/mazesolver/ - Cell.java interface — one square, plain wallUp()/wallRight() queries - Robot.java interface — walks the maze; canMoveRight()/moveRight()/atGoal() - Maze.java interface — the maze, hands out Cells and a Robot - MazeSolver.java interface — the ALGORITHM a student writes: solve(Robot) - Direction.java N/S/E/W bitmask constants (implementer-facing only) -src/test/java/... unit tests - - -- to implement (see CONTRACT.md) -- - GridMaze + the Cell/Robot it returns the concrete side, reading int[][] + Direction.java enum — UP/DOWN/LEFT/RIGHT, with bit()/opposite()/turns + Cell.java interface — one square; wall(Direction) queries + Robot.java interface — walks the maze; readWallSensor/drive/facing + Maze.java interface — the maze, hands out Cells and a Robot + GridMaze.java concrete Maze built from an int[][] (the site's entry point) + GridRobot/GridCell.java the Robot/Cell a GridMaze returns + MazeSolver.java interface — a batch solver: int[][] solve(int[][]) + DefaultMazeSolver.java shipped MazeSolver (breadth-first, shortest path) +src/test/java/... unit tests ``` -New students write a `MazeSolver` against `Robot`/`Cell` and never touch a -bitmask. See [`CONTRACT.md`](CONTRACT.md) for the full API contract shared with -the lesson site, including the `GridMaze` entry point the site depends on. +Students drive a `Robot` (from `new GridMaze(grid).robot()`) with +`readWallSensor(Direction)` / `drive(Direction)` and never touch a bitmask. See +[`CONTRACT.md`](CONTRACT.md) for the full API contract shared with the lesson +site. diff --git a/src/main/java/com/frc2713/mazesolver/Cell.java b/src/main/java/com/frc2713/mazesolver/Cell.java index 7ab3f4f..2a8aadb 100644 --- a/src/main/java/com/frc2713/mazesolver/Cell.java +++ b/src/main/java/com/frc2713/mazesolver/Cell.java @@ -1,41 +1,24 @@ package com.frc2713.mazesolver; /** - * One square of the maze. - * - *
This is the beginner-facing view of a cell: instead of asking "what is the - * bitmask of this cell?", you ask plain yes/no questions like "is there a wall - * to my right?". Implementations (see the library README) translate the - * underlying N/S/E/W bitmask into these methods so the students using this - * library never touch a bitmask. - * - *
Directions are screen-relative, the way the maze is drawn: up is
- * toward row 0, down is toward the last row, left is toward
- * column 0, right is toward the last column.
+ * One square of a {@link Maze}. A cell knows where it sits and which of its four
+ * sides are walls — the same questions a robot standing on it can ask, without
+ * any bitmask arithmetic.
*/
public interface Cell {
- /** The row (0-based) this cell sits in; row 0 is the top. */
+ /** This cell's row (0 is the top row). */
int row();
- /** The column (0-based) this cell sits in; column 0 is the left edge. */
+ /** This cell's column (0 is the left column). */
int col();
- /** {@code true} if a wall blocks movement off the top of this cell. */
- boolean wallUp();
+ /** Is the given side of this cell a wall (i.e. blocked, not carved open)? */
+ boolean wall(Direction dir);
- /** {@code true} if a wall blocks movement off the bottom of this cell. */
- boolean wallDown();
-
- /** {@code true} if a wall blocks movement off the left of this cell. */
- boolean wallLeft();
-
- /** {@code true} if a wall blocks movement off the right of this cell. */
- boolean wallRight();
-
- /** {@code true} if this is the maze's start cell (top-left). */
+ /** Is this the start cell (top-left)? */
boolean isStart();
- /** {@code true} if this is the maze's goal cell (bottom-right). */
+ /** Is this the goal cell (bottom-right)? */
boolean isGoal();
}
diff --git a/src/main/java/com/frc2713/mazesolver/DefaultMazeSolver.java b/src/main/java/com/frc2713/mazesolver/DefaultMazeSolver.java
new file mode 100644
index 0000000..8f4f967
--- /dev/null
+++ b/src/main/java/com/frc2713/mazesolver/DefaultMazeSolver.java
@@ -0,0 +1,102 @@
+package com.frc2713.mazesolver;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * The library's {@link MazeSolver} implementation. Finds the shortest path from
+ * the top-left start to the bottom-right goal with a breadth-first search over
+ * the cells reachable through carved (open) sides.
+ */
+public final class DefaultMazeSolver implements MazeSolver {
+
+ @Override
+ public int[][] solve(int[][] maze) {
+ if (maze == null || maze.length == 0 || maze[0] == null || maze[0].length == 0) {
+ throw new IllegalArgumentException("maze must be a non-empty rectangular grid");
+ }
+ int rows = maze.length;
+ int cols = maze[0].length;
+ for (int[] row : maze) {
+ if (row == null || row.length != cols) {
+ throw new IllegalArgumentException("maze must be rectangular: every row the same length");
+ }
+ }
+
+ int goalR = rows - 1;
+ int goalC = cols - 1;
+
+ // parent[r][c] encodes the cell we arrived from as r*cols + c; -1 means
+ // unvisited, -2 marks the start (which has no parent).
+ int[][] parent = new int[rows][cols];
+ for (int[] row : parent) {
+ Arrays.fill(row, -1);
+ }
+
+ ArrayDeque Each direction carries the single bit it occupies in a cell's N/S/E/W
+ * bitmask (the maze wire format shared with the {@code maze-generator} npm
+ * package): {@code UP == 1}, {@code DOWN == 2}, {@code RIGHT == 4},
+ * {@code LEFT == 8}. On screen those are north/south/east/west respectively — a
+ * cell open to the north and east only is {@code UP.bit() | RIGHT.bit() == 5}.
+ * See {@link MazeSolver} for the maze format and {@code CONTRACT.md}.
*/
-public final class Direction {
+public enum Direction {
- public static final int NORTH = 1;
- public static final int SOUTH = 2;
- public static final int EAST = 4;
- public static final int WEST = 8;
+ UP(1),
+ DOWN(2),
+ RIGHT(4),
+ LEFT(8);
- private Direction() {
+ private final int bit;
+
+ Direction(int bit) {
+ this.bit = bit;
+ }
+
+ /**
+ * This direction's bit in a cell's open-sides bitmask
+ * ({@code UP=1, DOWN=2, RIGHT=4, LEFT=8}).
+ */
+ public int bit() {
+ return bit;
+ }
+
+ /** The opposite heading — where you'd point after turning all the way around. */
+ public Direction opposite() {
+ switch (this) {
+ case UP:
+ return DOWN;
+ case DOWN:
+ return UP;
+ case LEFT:
+ return RIGHT;
+ default:
+ return LEFT;
+ }
+ }
+
+ /** The heading 90° clockwise from this one (UP→RIGHT→DOWN→LEFT→UP). */
+ public Direction clockwise() {
+ switch (this) {
+ case UP:
+ return RIGHT;
+ case RIGHT:
+ return DOWN;
+ case DOWN:
+ return LEFT;
+ default:
+ return UP;
+ }
+ }
+
+ /** The heading 90° counter-clockwise from this one (UP→LEFT→DOWN→RIGHT→UP). */
+ public Direction counterClockwise() {
+ switch (this) {
+ case UP:
+ return LEFT;
+ case LEFT:
+ return DOWN;
+ case DOWN:
+ return RIGHT;
+ default:
+ return UP;
+ }
}
}
diff --git a/src/main/java/com/frc2713/mazesolver/GridCell.java b/src/main/java/com/frc2713/mazesolver/GridCell.java
new file mode 100644
index 0000000..f51a51d
--- /dev/null
+++ b/src/main/java/com/frc2713/mazesolver/GridCell.java
@@ -0,0 +1,45 @@
+package com.frc2713.mazesolver;
+
+/** One square of a {@link GridMaze}. Wall flags come from the cell's bitmask. */
+final class GridCell implements Cell {
+
+ private final GridMaze maze;
+ private final int row;
+ private final int col;
+
+ GridCell(GridMaze maze, int row, int col) {
+ this.maze = maze;
+ this.row = row;
+ this.col = col;
+ }
+
+ private int mask() {
+ return maze.mask(row, col);
+ }
+
+ @Override
+ public int row() {
+ return row;
+ }
+
+ @Override
+ public int col() {
+ return col;
+ }
+
+ @Override
+ public boolean wall(Direction dir) {
+ // A set bit means that side is OPEN, so a wall is the bit being clear.
+ return (mask() & dir.bit()) == 0;
+ }
+
+ @Override
+ public boolean isStart() {
+ return row == 0 && col == 0;
+ }
+
+ @Override
+ public boolean isGoal() {
+ return maze.isGoalCell(row, col);
+ }
+}
diff --git a/src/main/java/com/frc2713/mazesolver/GridMaze.java b/src/main/java/com/frc2713/mazesolver/GridMaze.java
new file mode 100644
index 0000000..6d54621
--- /dev/null
+++ b/src/main/java/com/frc2713/mazesolver/GridMaze.java
@@ -0,0 +1,58 @@
+package com.frc2713.mazesolver;
+
+/**
+ * A concrete {@link Maze} backed by an N/S/E/W bitmask grid (a set bit means
+ * that side of the cell is OPEN). Start is the top-left cell {@code (0, 0)};
+ * goal is the bottom-right cell {@code (rows - 1, cols - 1)} — the convention
+ * the whole maze module uses.
+ *
+ * This is the entry point lesson snippets use: {@code new GridMaze(grid)}
+ * turns a raw {@code int[][]} into a maze you can hand a {@link Robot}.
+ */
+public class GridMaze implements Maze {
+
+ private final int[][] grid;
+ private final int rows;
+ private final int cols;
+ private final int goalRow;
+ private final int goalCol;
+ private final GridRobot bot;
+
+ public GridMaze(int[][] grid) {
+ this.grid = grid;
+ this.rows = grid.length;
+ this.cols = grid.length == 0 ? 0 : grid[0].length;
+ this.goalRow = rows - 1;
+ this.goalCol = cols - 1;
+ this.bot = new GridRobot(this);
+ }
+
+ int mask(int row, int col) {
+ return grid[row][col];
+ }
+
+ @Override
+ public int rows() {
+ return rows;
+ }
+
+ @Override
+ public int cols() {
+ return cols;
+ }
+
+ @Override
+ public Cell cellAt(int row, int col) {
+ return new GridCell(this, row, col);
+ }
+
+ @Override
+ public boolean isGoalCell(int row, int col) {
+ return row == goalRow && col == goalCol;
+ }
+
+ @Override
+ public Robot robot() {
+ return bot;
+ }
+}
diff --git a/src/main/java/com/frc2713/mazesolver/GridRobot.java b/src/main/java/com/frc2713/mazesolver/GridRobot.java
new file mode 100644
index 0000000..2d9b121
--- /dev/null
+++ b/src/main/java/com/frc2713/mazesolver/GridRobot.java
@@ -0,0 +1,90 @@
+package com.frc2713.mazesolver;
+
+import java.util.ArrayList;
+
+/**
+ * A {@link Robot} that walks a {@link GridMaze}. It records a trail: every cell
+ * it has stood on, start first. A drive blocked by a wall does nothing and adds
+ * no trail entry, so an algorithm that drives into a wall leaves a trail that
+ * simply stays put there.
+ *
+ * The robot remembers one fact about itself — {@link #facing()} — and updates
+ * it every time it actually moves, because a robot that just drove up is now
+ * facing up. It starts facing {@link Direction#RIGHT}.
+ */
+final class GridRobot implements Robot {
+
+ private final GridMaze maze;
+ private int row = 0;
+ private int col = 0;
+ private Direction facing = Direction.RIGHT;
+ private final ArrayList A {@code Maze} is built from the raw {@code int[][]} bitmask grid the
- * lesson site produces (see {@code CONTRACT.md}), but it hides that entirely:
- * once you have a {@code Maze} you work with {@link Cell}s and a {@link Robot},
- * never bitmasks.
- *
- * The library must provide a concrete implementation constructable from an
- * {@code int[][]} grid so the site can build one — see {@code CONTRACT.md} for
- * the agreed entry point.
+ * A rectangular grid maze. Build one from an N/S/E/W bitmask grid with
+ * {@link GridMaze}. The start is always the top-left cell {@code (0, 0)} and the
+ * goal the bottom-right cell {@code (rows - 1, cols - 1)}.
*/
public interface Maze {
- /** Number of rows in the maze. */
+ /** Number of rows. */
int rows();
- /** Number of columns in the maze. */
+ /** Number of columns. */
int cols();
- /**
- * The cell at the given position.
- *
- * @throws IndexOutOfBoundsException if the position is off the grid
- */
+ /** The cell at the given row and column. */
Cell cellAt(int row, int col);
- /** A fresh robot standing on the start cell (top-left), ready to solve. */
- Robot robot();
-
- /** {@code true} if the given position is the goal cell (bottom-right). */
+ /** Is the given row/column the goal cell (bottom-right)? */
boolean isGoalCell(int row, int col);
+
+ /** A fresh {@link Robot} standing on the start cell of this maze. */
+ Robot robot();
}
diff --git a/src/main/java/com/frc2713/mazesolver/MazeSolver.java b/src/main/java/com/frc2713/mazesolver/MazeSolver.java
index ce5a737..e270b7c 100644
--- a/src/main/java/com/frc2713/mazesolver/MazeSolver.java
+++ b/src/main/java/com/frc2713/mazesolver/MazeSolver.java
@@ -1,35 +1,45 @@
package com.frc2713.mazesolver;
/**
- * A maze-solving algorithm — this is what a student writes.
+ * Solves a maze expressed as a grid of cells, where each cell is an
+ * N/S/E/W bitmask marking which sides are open (carved) rather than walled.
*
- * The library hands you a {@link Robot} sitting at the start of a maze; your
- * job is to move it until it reaches the goal. You decide how — that
- * decision is the algorithm. A simple "wall follower" might look like:
+ * Bitmask convention (matches the {@code maze-generator} npm package used
+ * to author lesson content, so a maze can round-trip between the site and
+ * this library unchanged). The bits are named the way they read on screen —
+ * see {@link Direction}:
+ * The path the robot took is available afterwards via {@link Robot#trail()}.
+ * Implementations must be pure: no networking, no threads, no filesystem,
+ * no AWT/Swing. This library runs inside a browser JVM (CheerpJ, OpenJDK 8),
+ * where none of those are available.
*/
public interface MazeSolver {
/**
- * Drive the robot from the start cell to the goal cell.
+ * Finds a path through the maze.
*
- * @param robot a robot positioned at the maze's start; move it with
- * {@link Robot#moveUp()} and friends until
- * {@link Robot#atGoal()} is {@code true}
+ * @param maze a rectangular grid, indexed {@code maze[row][col]}, where
+ * each value is an N/S/E/W bitmask (see {@link Direction})
+ * describing which sides of that cell are open. The maze is
+ * assumed to be a valid grid: every row the same length, and
+ * every "open" side agreed upon by both adjacent cells (e.g.
+ * if {@code maze[r][c]} has {@code RIGHT} set, then
+ * {@code maze[r][c + 1]} has {@code LEFT} set).
+ * @return the path from the top-left cell ({@code [0][0]}) to the
+ * bottom-right cell ({@code [rows - 1][cols - 1]}), as an ordered
+ * list of {@code [row, col]} coordinate pairs starting with
+ * {@code {0, 0}} and ending with the destination cell. Returns
+ * an empty array ({@code new int[0][]}) if no path exists.
+ * @throws IllegalArgumentException if {@code maze} is null, empty, or
+ * not a valid rectangular grid
*/
- void solve(Robot robot);
+ int[][] solve(int[][] maze);
}
diff --git a/src/main/java/com/frc2713/mazesolver/Robot.java b/src/main/java/com/frc2713/mazesolver/Robot.java
index a532597..bc32f5e 100644
--- a/src/main/java/com/frc2713/mazesolver/Robot.java
+++ b/src/main/java/com/frc2713/mazesolver/Robot.java
@@ -1,69 +1,55 @@
package com.frc2713.mazesolver;
/**
- * A robot that walks through the maze, one cell at a time.
+ * A robot that walks a {@link Maze} one cell at a time. It starts on the maze's
+ * start cell and knows only what's around it — never a bird's-eye view of the
+ * whole maze.
*
- * This is the main tool a student uses to write a solving algorithm. The
- * robot always sits in exactly one cell (its position). You ask whether it
- * can move a direction, and if so you tell it to move — for example:
- *
- * Directions are screen-relative (see {@link Cell}). A robot never needs to
- * know about bitmasks — it only knows where it is and which ways it can go.
+ * The robot has two abilities, mirroring a real one: it can sense a
+ * wall on any side ({@link #readWallSensor}) and it can drive one cell
+ * in a direction ({@link #drive}). It also remembers one fact about itself —
+ * which way it last drove, its {@link #facing()} — because a robot that just
+ * moved up is now facing up.
*/
public interface Robot {
- /** The robot's current row (0-based); row 0 is the top. */
- int row();
-
- /** The robot's current column (0-based); column 0 is the left edge. */
- int col();
-
- /** The cell the robot is currently standing on. */
- Cell cell();
-
- /** {@code true} if there is no wall above and a cell to move into. */
- boolean canMoveUp();
-
- /** {@code true} if there is no wall below and a cell to move into. */
- boolean canMoveDown();
-
- /** {@code true} if there is no wall to the left and a cell to move into. */
- boolean canMoveLeft();
+ /**
+ * Reads the wall sensor on one side of the robot's current cell.
+ *
+ * @return {@code true} if that side is a wall (the robot cannot drive that
+ * way), {@code false} if it is open
+ */
+ boolean readWallSensor(Direction dir);
- /** {@code true} if there is no wall to the right and a cell to move into. */
- boolean canMoveRight();
+ /**
+ * Drives one cell in the given direction. If that side is a wall the robot
+ * stays put and nothing happens; otherwise it moves and its
+ * {@link #facing()} becomes {@code dir}.
+ */
+ void drive(Direction dir);
/**
- * Step one cell up. Implementations should throw
- * {@link IllegalStateException} if {@link #canMoveUp()} is {@code false},
- * so a student learns to check before moving.
+ * The direction the robot last drove — the one fact it remembers about
+ * itself. Before it has moved, this is its initial heading.
*/
- void moveUp();
+ Direction facing();
- /** Step one cell down. See {@link #moveUp()} for the blocked-move rule. */
- void moveDown();
+ /** Is the robot standing on the goal cell? */
+ boolean atGoal();
- /** Step one cell left. See {@link #moveUp()} for the blocked-move rule. */
- void moveLeft();
+ /** The robot's current row (0 is the top row). */
+ int row();
- /** Step one cell right. See {@link #moveUp()} for the blocked-move rule. */
- void moveRight();
+ /** The robot's current column (0 is the left column). */
+ int col();
- /** {@code true} once the robot reaches the goal cell (bottom-right). */
- boolean atGoal();
+ /** The {@link Cell} the robot is currently standing on. */
+ Cell cell();
/**
- * Every cell the robot has stood on, in order, as {@code {row, col}} pairs —
- * starting with the start cell and ending with its current cell. Handy for
- * drawing the path the algorithm took.
+ * The trail of every cell the robot has stood on, in order, as
+ * {@code [row, col]} pairs starting with the start cell. A drive blocked by
+ * a wall adds no entry.
*/
int[][] trail();
}
diff --git a/src/test/java/com/frc2713/mazesolver/DirectionTest.java b/src/test/java/com/frc2713/mazesolver/DirectionTest.java
index c6af0fa..895a259 100644
--- a/src/test/java/com/frc2713/mazesolver/DirectionTest.java
+++ b/src/test/java/com/frc2713/mazesolver/DirectionTest.java
@@ -8,15 +8,36 @@ class DirectionTest {
@Test
void bitmaskValuesMatchMazeGeneratorConvention() {
- assertEquals(1, Direction.NORTH);
- assertEquals(2, Direction.SOUTH);
- assertEquals(4, Direction.EAST);
- assertEquals(8, Direction.WEST);
+ assertEquals(1, Direction.UP.bit());
+ assertEquals(2, Direction.DOWN.bit());
+ assertEquals(4, Direction.RIGHT.bit());
+ assertEquals(8, Direction.LEFT.bit());
}
@Test
void directionsAreDistinctBits() {
- int all = Direction.NORTH | Direction.SOUTH | Direction.EAST | Direction.WEST;
+ int all = Direction.UP.bit() | Direction.DOWN.bit()
+ | Direction.RIGHT.bit() | Direction.LEFT.bit();
assertEquals(15, all);
}
+
+ @Test
+ void oppositeFlipsTheHeading() {
+ assertEquals(Direction.DOWN, Direction.UP.opposite());
+ assertEquals(Direction.UP, Direction.DOWN.opposite());
+ assertEquals(Direction.RIGHT, Direction.LEFT.opposite());
+ assertEquals(Direction.LEFT, Direction.RIGHT.opposite());
+ }
+
+ @Test
+ void turnsRotateThroughAllFour() {
+ assertEquals(Direction.RIGHT, Direction.UP.clockwise());
+ assertEquals(Direction.DOWN, Direction.RIGHT.clockwise());
+ assertEquals(Direction.LEFT, Direction.DOWN.clockwise());
+ assertEquals(Direction.UP, Direction.LEFT.clockwise());
+
+ for (Direction d : Direction.values()) {
+ assertEquals(d, d.clockwise().counterClockwise());
+ }
+ }
}
diff --git a/src/test/java/com/frc2713/mazesolver/GridMazeTest.java b/src/test/java/com/frc2713/mazesolver/GridMazeTest.java
new file mode 100644
index 0000000..8f3c065
--- /dev/null
+++ b/src/test/java/com/frc2713/mazesolver/GridMazeTest.java
@@ -0,0 +1,105 @@
+package com.frc2713.mazesolver;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class GridMazeTest {
+
+ // A 3x3 maze with every interior wall carved open (fully connected). Bits:
+ // UP=1, DOWN=2, RIGHT=4, LEFT=8. Each cell is open toward every neighbor it
+ // has, so adjacent cells always agree.
+ private static int[][] openGrid() {
+ return new int[][] {
+ { 6, 14, 10 },
+ { 7, 15, 11 },
+ { 5, 13, 9 },
+ };
+ }
+
+ @Test
+ void robotSensesWallsAndOpenings() {
+ Robot robot = new GridMaze(openGrid()).robot();
+ // Top-left corner: walls up and left, openings down and right.
+ assertTrue(robot.readWallSensor(Direction.UP));
+ assertTrue(robot.readWallSensor(Direction.LEFT));
+ assertFalse(robot.readWallSensor(Direction.DOWN));
+ assertFalse(robot.readWallSensor(Direction.RIGHT));
+ }
+
+ @Test
+ void driveMovesAndUpdatesFacing() {
+ Robot robot = new GridMaze(openGrid()).robot();
+ assertEquals(Direction.RIGHT, robot.facing()); // initial heading
+
+ robot.drive(Direction.DOWN);
+ assertEquals(1, robot.row());
+ assertEquals(0, robot.col());
+ assertEquals(Direction.DOWN, robot.facing());
+
+ robot.drive(Direction.RIGHT);
+ assertEquals(1, robot.row());
+ assertEquals(1, robot.col());
+ assertEquals(Direction.RIGHT, robot.facing());
+ }
+
+ @Test
+ void driveIntoWallDoesNothing() {
+ Robot robot = new GridMaze(openGrid()).robot();
+ robot.drive(Direction.UP); // walled — should be a no-op
+ assertEquals(0, robot.row());
+ assertEquals(0, robot.col());
+ assertEquals(1, robot.trail().length); // no new trail entry
+ assertEquals(Direction.RIGHT, robot.facing()); // heading unchanged
+ }
+
+ @Test
+ void robotReachesGoalAndRecordsTrail() {
+ Robot robot = new GridMaze(openGrid()).robot();
+ robot.drive(Direction.DOWN);
+ robot.drive(Direction.DOWN);
+ robot.drive(Direction.RIGHT);
+ robot.drive(Direction.RIGHT);
+ assertTrue(robot.atGoal());
+ assertEquals(5, robot.trail().length);
+ assertArrayEquals(new int[] { 0, 0 }, robot.trail()[0]);
+ assertArrayEquals(new int[] { 2, 2 }, robot.trail()[4]);
+ }
+
+ @Test
+ void solverFindsShortestPath() {
+ int[][] path = new DefaultMazeSolver().solve(openGrid());
+ assertArrayEquals(new int[] { 0, 0 }, path[0]);
+ assertArrayEquals(new int[] { 2, 2 }, path[path.length - 1]);
+ assertEquals(5, path.length); // 4 steps is the shortest across a 3x3
+ // Every consecutive pair is an adjacent, carved move.
+ int[][] grid = openGrid();
+ for (int i = 1; i < path.length; i++) {
+ int dr = path[i][0] - path[i - 1][0];
+ int dc = path[i][1] - path[i - 1][1];
+ assertEquals(1, Math.abs(dr) + Math.abs(dc), "steps must be to an adjacent cell");
+ }
+ }
+
+ @Test
+ void solverReturnsEmptyWhenGoalUnreachable() {
+ // Start cell sealed off (no open sides); goal can't be reached.
+ int[][] sealed = {
+ { 0, 8 },
+ { 0, 1 },
+ };
+ assertEquals(0, new DefaultMazeSolver().solve(sealed).length);
+ }
+
+ @Test
+ void solverRejectsMalformedMazes() {
+ assertThrows(IllegalArgumentException.class, () -> new DefaultMazeSolver().solve(null));
+ assertThrows(IllegalArgumentException.class, () -> new DefaultMazeSolver().solve(new int[0][]));
+ assertThrows(IllegalArgumentException.class,
+ () -> new DefaultMazeSolver().solve(new int[][] { { 1, 2 }, { 3 } }));
+ }
+}
+ * UP = 1 (0b0001) (north)
+ * DOWN = 2 (0b0010) (south)
+ * RIGHT = 4 (0b0100) (east)
+ * LEFT = 8 (0b1000) (west)
+ *
+ * A cell's value is the bitwise OR of every open side, e.g. a cell open
+ * upward and to the right only is {@code UP.bit() | RIGHT.bit() == 5}.
*
- * {@code
- * public class MySolver implements MazeSolver {
- * public void solve(Robot robot) {
- * while (!robot.atGoal()) {
- * if (robot.canMoveRight()) robot.moveRight();
- * else if (robot.canMoveDown()) robot.moveDown();
- * else if (robot.canMoveLeft()) robot.moveLeft();
- * else robot.moveUp();
- * }
- * }
- * }
- * }
- *
- * {@code
- * while (!robot.atGoal()) {
- * if (robot.canMoveRight()) robot.moveRight();
- * else if (robot.canMoveDown()) robot.moveDown();
- * else robot.moveUp();
- * }
- * }
- *
- *