Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 70 additions & 22 deletions CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|-----------|------|-------|
Expand All @@ -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.
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
41 changes: 41 additions & 0 deletions src/main/java/com/frc2713/mazesolver/Cell.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.frc2713.mazesolver;

/**
* One square of the maze.
*
* <p>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.
*
* <p>Directions are screen-relative, the way the maze is drawn: <b>up</b> is
* toward row 0, <b>down</b> is toward the last row, <b>left</b> is toward
* column 0, <b>right</b> 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();
}
12 changes: 0 additions & 12 deletions src/main/java/com/frc2713/mazesolver/DefaultMazeSolver.java

This file was deleted.

35 changes: 35 additions & 0 deletions src/main/java/com/frc2713/mazesolver/Maze.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.frc2713.mazesolver;

/**
* A maze the student can explore.
*
* <p>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.
*
* <p>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);
}
55 changes: 23 additions & 32 deletions src/main/java/com/frc2713/mazesolver/MazeSolver.java
Original file line number Diff line number Diff line change
@@ -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 <em>student</em> writes.
*
* <p>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):
* <pre>
* NORTH = 1 (0b0001)
* SOUTH = 2 (0b0010)
* EAST = 4 (0b0100)
* WEST = 8 (0b1000)
* </pre>
* 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}.
* <p>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 <em>how</em> — that
* decision is the algorithm. A simple "wall follower" might look like:
*
* <p>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.
* <pre>{@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();
* }
* }
* }
* }</pre>
*
* <p>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);
}
69 changes: 69 additions & 0 deletions src/main/java/com/frc2713/mazesolver/Robot.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.frc2713.mazesolver;

/**
* A robot that walks through the maze, one cell at a time.
*
* <p>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
* <em>can</em> move a direction, and if so you tell it to move — for example:
*
* <pre>{@code
* while (!robot.atGoal()) {
* if (robot.canMoveRight()) robot.moveRight();
* else if (robot.canMoveDown()) robot.moveDown();
* else robot.moveUp();
* }
* }</pre>
*
* <p>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();
}
Loading