diff --git a/CONTRACT.md b/CONTRACT.md index 09fe681..2f4b5d9 100644 --- a/CONTRACT.md +++ b/CONTRACT.md @@ -4,12 +4,20 @@ This is the contract shared between this library and the `software_training` lesson site — both sides need to agree on it, since lesson snippets and this library speak the same maze format. -## Maze representation +There are two layers, and keeping them separate is the whole point: -A maze is `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 already uses to build mazes: +- **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. + +## 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 | |-----------|------|-------| @@ -18,38 +26,78 @@ npm package the lesson site already uses to build mazes: | 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 +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. `solve` is not required to -validate this beyond basic shape checks. +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. Start is always the top-left cell (`[0][0]`); the goal is always the -bottom-right cell (`[rows - 1][cols - 1]`). +bottom-right cell (`[rows - 1][cols - 1]`). Screen-relative directions map to +the bitmask as: **up** = NORTH, **down** = SOUTH, **right** = EAST, +**left** = WEST. -## Public API +## 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. ```java package com.frc2713.mazesolver; -public interface MazeSolver { - int[][] solve(int[][] maze); +public interface Cell { // one square; plain yes/no queries + int row(); int col(); + boolean wallUp(); boolean wallDown(); + boolean wallLeft(); boolean wallRight(); + boolean isStart(); boolean isGoal(); +} + +public interface Robot { // walks the maze one cell at a time + 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 +} + +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 + 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() } ``` -`solve` returns the path from start to goal as an ordered array of -`{row, col}` pairs, starting with `{0, 0}` and ending at the goal cell, or -`new int[0][]` if no path exists. It throws `IllegalArgumentException` for a -null, empty, or non-rectangular `maze`. +### Entry point the site relies on + +The library **must** provide a concrete `Maze` the site can build directly from +the wire format: + +```java +public final class GridMaze implements Maze { + public GridMaze(int[][] maze) { ... } // maze = the bitmask grid above +} +``` -`com.frc2713.mazesolver.DefaultMazeSolver` is the shipped implementation. +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. ## Changing this contract -If either side (this library or the lesson site) needs the shape of `solve`, -the bitmask values, or the start/goal convention to change, update this file -first and treat it as the single source of truth — the site's +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. diff --git a/README.md b/README.md index 4e3b212..cd67c40 100644 --- a/README.md +++ b/README.md @@ -40,13 +40,22 @@ 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. + ``` src/main/java/com/frc2713/mazesolver/ - MazeSolver.java the public API (interface) - Direction.java N/S/E/W bitmask constants - DefaultMazeSolver.java implementation — solving logic goes here -src/test/java/... unit tests + 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[][] ``` -See [`CONTRACT.md`](CONTRACT.md) for the API contract shared with the lesson -site. +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. diff --git a/src/main/java/com/frc2713/mazesolver/Cell.java b/src/main/java/com/frc2713/mazesolver/Cell.java new file mode 100644 index 0000000..7ab3f4f --- /dev/null +++ b/src/main/java/com/frc2713/mazesolver/Cell.java @@ -0,0 +1,41 @@ +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. + */ +public interface Cell { + + /** The row (0-based) this cell sits in; row 0 is the top. */ + int row(); + + /** The column (0-based) this cell sits in; column 0 is the left edge. */ + int col(); + + /** {@code true} if a wall blocks movement off the top of this cell. */ + boolean wallUp(); + + /** {@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). */ + boolean isStart(); + + /** {@code true} if this is the maze's 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 deleted file mode 100644 index d562b83..0000000 --- a/src/main/java/com/frc2713/mazesolver/DefaultMazeSolver.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.frc2713.mazesolver; - -/** - * The library's {@link MazeSolver} implementation. Solving logic goes here. - */ -public final class DefaultMazeSolver implements MazeSolver { - - @Override - public int[][] solve(int[][] maze) { - throw new UnsupportedOperationException("not yet implemented"); - } -} diff --git a/src/main/java/com/frc2713/mazesolver/Maze.java b/src/main/java/com/frc2713/mazesolver/Maze.java new file mode 100644 index 0000000..13c10bd --- /dev/null +++ b/src/main/java/com/frc2713/mazesolver/Maze.java @@ -0,0 +1,35 @@ +package com.frc2713.mazesolver; + +/** + * A maze the student can explore. + * + *
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. + */ +public interface Maze { + + /** Number of rows in the maze. */ + int rows(); + + /** Number of columns in the maze. */ + int cols(); + + /** + * The cell at the given position. + * + * @throws IndexOutOfBoundsException if the position is off the grid + */ + 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). */ + boolean isGoalCell(int row, int col); +} diff --git a/src/main/java/com/frc2713/mazesolver/MazeSolver.java b/src/main/java/com/frc2713/mazesolver/MazeSolver.java index 488e4de..ce5a737 100644 --- a/src/main/java/com/frc2713/mazesolver/MazeSolver.java +++ b/src/main/java/com/frc2713/mazesolver/MazeSolver.java @@ -1,44 +1,35 @@ package com.frc2713.mazesolver; /** - * 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. + * A maze-solving algorithm — this is what a student writes. * - *
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): - *
- * NORTH = 1 (0b0001) - * SOUTH = 2 (0b0010) - * EAST = 4 (0b0100) - * WEST = 8 (0b1000) - *- * A cell's value is the bitwise OR of every open side, e.g. a cell open to - * the north and east only is {@code NORTH | EAST == 5}. See {@link Direction}. + *
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: * - *
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. + *
{@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();
+ * }
+ * }
+ * }
+ * }
+ *
+ * The path the robot took is available afterwards via {@link Robot#trail()}. */ public interface MazeSolver { /** - * Finds a path through the maze. + * Drive the robot from the start cell to the goal cell. * - * @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 EAST} set, then - * {@code maze[r][c + 1]} has {@code WEST} 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 + * @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} */ - int[][] solve(int[][] maze); + void solve(Robot robot); } diff --git a/src/main/java/com/frc2713/mazesolver/Robot.java b/src/main/java/com/frc2713/mazesolver/Robot.java new file mode 100644 index 0000000..a532597 --- /dev/null +++ b/src/main/java/com/frc2713/mazesolver/Robot.java @@ -0,0 +1,69 @@ +package com.frc2713.mazesolver; + +/** + * A robot that walks through the maze, one cell at a time. + * + *
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: + * + *
{@code
+ * while (!robot.atGoal()) {
+ * if (robot.canMoveRight()) robot.moveRight();
+ * else if (robot.canMoveDown()) robot.moveDown();
+ * else robot.moveUp();
+ * }
+ * }
+ *
+ * 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. + */ +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(); + + /** {@code true} if there is no wall to the right and a cell to move into. */ + boolean canMoveRight(); + + /** + * Step one cell up. Implementations should throw + * {@link IllegalStateException} if {@link #canMoveUp()} is {@code false}, + * so a student learns to check before moving. + */ + void moveUp(); + + /** Step one cell down. See {@link #moveUp()} for the blocked-move rule. */ + void moveDown(); + + /** Step one cell left. See {@link #moveUp()} for the blocked-move rule. */ + void moveLeft(); + + /** Step one cell right. See {@link #moveUp()} for the blocked-move rule. */ + void moveRight(); + + /** {@code true} once the robot reaches the goal cell (bottom-right). */ + boolean atGoal(); + + /** + * 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. + */ + int[][] trail(); +}