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
5 changes: 5 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"enabledPlugins": {
"frc2713-skills@frc2713": true
}
}
20 changes: 20 additions & 0 deletions .github/workflows/deploy-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,26 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# Build the maze-solver library and vendor its jar into site/public/ so
# the Java playground can load the com.frc2713.mazesolver API. Built fresh
# each deploy from the library's main; not committed (see site/.gitignore).
- uses: actions/checkout@v4
with:
repository: FRC2713/maze-solver-java
ref: main
path: maze-solver-java
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
cache: maven
- name: Build maze-solver jar
run: |
mvn -B -q -DskipTests package
cp target/maze-solver.jar "$GITHUB_WORKSPACE/site/public/maze-solver.jar"
working-directory: maze-solver-java

- uses: actions/setup-node@v4
with:
node-version: 20
Expand Down
25 changes: 25 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,28 @@ folder until it's restarted.
- **Components**: `LessonCard` (index listing), `PageNav` (in-lesson page
navigation), `JavaRunner` (the editable/runnable code block UI backed by
`javaRuntime.ts`).
- **Maze round-trip** (`site/src/lib/mazeHarness.ts` + the `'java'` mode of
`MazePlayground`): the `solver: java` maze fence renders an editor where a
student writes a `solve(Robot)` method. `buildMazeHarness` interpolates the
current JS maze in as an `int[][]` literal, splices the student's method into a
`MazeRun` class, and drives the robot; the **Maze Trail** is emitted as a
sentinel-tagged stdout line (`__TRAIL__ [[row,col],…]`), which `parseTrail`
reads back (the single `[row,col]→[x,y]` swap) and `MazePlayground` animates.
`docs/maze-roundtrip.md` documents the seams. `new GridMaze(grid)`, the
`Robot`/`Cell`/`Maze` types, and the `Direction` enum all resolve against
`site/public/maze-solver.jar` — the `FRC2713/maze-solver-java` library, which
ships the concrete `GridMaze`/`GridRobot`/`GridCell` itself (built fresh by CI
on deploy and by `scripts/vendor-maze-solver.sh` locally). The robot API is
sensor/drivetrain-shaped: `robot.readWallSensor(Direction.UP)` (true = wall),
`robot.drive(Direction.RIGHT)` (no-op into a wall), and `robot.facing()`, which
the robot auto-updates on each successful drive; `Direction` is an
`enum {UP,DOWN,LEFT,RIGHT}` with `bit()`/`opposite()`/`clockwise()`/
`counterClockwise()`. Earlier revisions vendored a separate `maze-engine.jar`
shim while the library shipped interfaces only — that shim (and its
`ENGINE_JAR` classpath entry, build script, and `scripts/maze-engine/` sources)
has been removed; the library and site share the contract in the library's
`CONTRACT.md`. The Algorithms section runs four lessons: `algorithms` (visual —
what an algorithm is, three robots), then `algorithms-maze-data` (the maze as a
2D `int[][]`), `algorithms-robot-api` (reading the library's API, writing
`tryMove`), and `algorithms-maze-solver` (the wall follower + the round-trip
playground).
80 changes: 80 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Maze Solver Module

Glossary for the maze-solving module of the FRC 2713 training curriculum — a
series of lessons in which students write Java that solves a maze. This file is
the shared language for that module only; it is not a spec.

## Language

**Maze**:
A fixed puzzle the robot must traverse from Start to Goal. The student's program
is handed the whole maze up front (see _Grid_).

**Grid**:
The god's-eye representation of a Maze the program receives — the entire layout,
known in full before the robot moves, as a rectangular 2D array of Cells. Each
Cell carries its own Wall flags, so the Grid is a `Cell[][]`, not a grid of
blocked/open squares.
_Avoid_: map (reserve "map" for a robot-built model, which this module does not use)

**Cell**:
One square of the Grid — always open and standable. A Cell records a **Wall** on
each of its four sides (up / down / left / right); those flags, not the Cells
themselves, are what block movement. Every Cell is reachable to stand on; what
varies is which of its edges are walled.

**Wall**:
A **thin** barrier on the edge between two adjacent Cells (or on a Cell's outer
edge), blocking movement across that edge. Walls live on edges, not whole Cells:
a Cell is never "a wall". Wall flags are consistent between neighbours — the down
edge of a Cell is the up edge of the Cell below it.

**Robot**:
The thing that traverses the Maze by executing a Solution. It has a position on
the Grid (a Cell) and **no orientation** — it does not face a direction, so every
Move is an absolute step, not a turn. Orientation is a property of the robot's
_interface_, not of every algorithm: a wall-follower may choose to *remember*
which way it last stepped (a `facing` variable it owns), but that state lives in
the algorithm, not the robot — the robot still only ever takes absolute Moves.

**Start**:
The cell where the Robot begins.

**Goal**:
The target cell. A Solution **succeeds** if and only if executing it from Start
leaves the Robot standing on the Goal.

**Solution**:
The ordered sequence of Moves the student's program emits. Success is judged
only by whether it finishes the Maze (reaches the Goal). A Move blocked by a Wall
is not a modeled failure — it simply does nothing; nothing is penalized. The
module is about *finishing*, not collision avoidance.

**Maze Trail**:
The ordered sequence of Cells the Robot has occupied while executing its
Solution, from Start onward. It is the *consequence* of a Solution, not the same
thing: a Solution is the Moves the algorithm emits, whereas the Maze Trail is
where the Robot actually ended up standing after each. A Move blocked by a Wall
adds no Maze Trail entry, so an algorithm that drives into a wall leaves a Trail
that simply stays put there.
_Avoid_: path (ambiguous — could mean the open corridors of the Maze itself)

**Maze Battle**:
The module's finale. A student submits one program; it is scored by how many of a
set of *unseen* Mazes it finishes (a gauntlet), tie-broken by total Moves. The
format rewards a general algorithm (which clears the whole set) over a hard-coded
Solution (which finishes only the one Maze it was written for).

**Move** (also **Command**):
One instruction to the Robot. Exactly four exist: **UP**, **DOWN**, **LEFT**,
**RIGHT** — absolute, screen-relative directions. Each Move steps the Robot
**one Cell** in that direction *if that edge is open*; a Move blocked by a Wall
(or the grid edge) does nothing. There are no turns and no orientation to track.
_Avoid_: turn, heading, facing, North/South/East/West, "drive to the next junction"

**Helper** (injected):
Convenience methods the harness puts in scope so students query the Maze without
hand-indexing Wall flags: `robot.moveUp/moveDown/moveLeft/moveRight()`,
`robot.canGoUp/canGoDown/canGoLeft/canGoRight()`, `robot.atGoal()`,
`robot.row()/col()`, plus god's-eye access to the whole `Cell[][]` grid for route
planning.
227 changes: 227 additions & 0 deletions docs/maze-roundtrip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
# The maze round-trip: JS → Java → animated steps

**Status: built.** The round-trip is wired end to end and drives the interactive
"write your own solver" playground (the `solver: java` maze fence, last page of
`lessons/algorithms/README.md`). The glue lives in `site/src/lib/mazeHarness.ts`
(`buildMazeHarness`, `parseTrail`, `retargetErrorLines`) and the `'java'` mode of
`site/src/components/MazePlayground.tsx`. This document describes the seams and
data contracts; a few assumptions in the original spec were corrected against the
real library and are noted inline below.

**Library.** `new GridMaze(grid)` and the `Robot`/`Cell`/`Direction` API resolve
against `site/public/maze-solver.jar` — the `FRC2713/maze-solver-java` library,
built fresh by CI on deploy and by `scripts/vendor-maze-solver.sh` locally. The
library ships the concrete `GridMaze`/`GridRobot`/`GridCell` itself; there is no
longer a separate engine shim. (Earlier revisions vendored a `maze-engine.jar`
while the library shipped interfaces only — that shim has been removed.)

Audience: developers working on the site. This is not student-facing lesson
content.

## What we're building and why

A student writes a maze-solving **algorithm** in Java. They press **Run**, and
the maze currently shown in the playground — generated in JavaScript — is handed
to their algorithm, which drives a `Robot` through it. The path the robot took
is then **animated back** in the same playground.

The motivating win is *removing duplicated logic*. Today
`site/src/components/MazePlayground.tsx` animates a wall follower that is
**re-implemented in TypeScript** (`runSolver`, mode `'wall'`) purely so there's
something to animate. That algorithm already exists in Java and in the
`maze-solver` library. The round-trip makes the **Java side the single source of
truth for the path**: JS generates the maze and plays back a result, but never
re-simulates solving.

The library's job in this is narrow and deliberate: it turns the raw bitmask
grid into a friendly `Maze`/`Robot`/`Cell` API so the *student* writes an
algorithm, not file I/O or bitmask arithmetic. See `CONTEXT.md` for the domain
vocabulary (Maze, Grid, Cell, Robot, Move, Solution, **Maze Trail**).

## Data flow

```mermaid
sequenceDiagram
participant JS as MazePlayground (JS)
participant RT as javaRuntime.runJava
participant H as Harness (generated Java)
participant Stu as student solve(Robot)
participant Lib as maze-solver library

JS->>JS: generateMaze() → int[][] bitmask grid
JS->>RT: runJava(harnessSource) // grid interpolated as literal + solve() spliced in
RT->>H: compile + run (CheerpJ)
H->>Lib: new GridMaze(grid); maze.robot()
H->>Stu: solve(robot)
Stu->>Lib: robot.readWallSensor/drive/atGoal() // library records the Maze Trail
H->>H: print "__TRAIL__ [[row,col],…]" (sentinel line)
RT-->>JS: RunOutcome.output (captured stdout)
JS->>JS: scan for sentinel, JSON.parse, [row,col]→[x,y]
JS->>JS: replay Trail — place robot dot at each cell
```

## The seams, one at a time

### 1. Generate the maze (JS) — *exists*

`generateMaze(SIZE)` in `MazePlayground.tsx` produces the `number[][]` N/S/E/W
bitmask grid (`N=1, S=2, E=4, W=8`; a set bit means that side is **open**). This
is unchanged.

### 2. Pass the maze down (new)

`runJava(code)` accepts **only a source string** — there is no data channel. So
the current grid is carried in by *interpolating it into the harness source* as
an `int[][]` literal, reusing the exact format `serializeToJava` already emits
(the bitmask grid plus `startRow/startCol/goalRow/goalCol`). No VFS file, no
Java-side parsing.

`serializeToJava` currently targets the clipboard ("Copy for Java"); the new glue
factors its literal-building out so the harness builder can call it directly.

### 3. Deserialize (Java / library) — *exists in the library*

The harness constructs the friendly API from the literal:

```java
Maze maze = new GridMaze(grid);
Robot robot = maze.robot(); // positioned at Start
```

The student never sees the bitmask grid or `main`.

### 4. Execute the student's algorithm (Java)

The harness calls the student's `solve(Robot)`. The student drives the robot with
the library's sensor/drivetrain API — `robot.readWallSensor(Direction)`,
`robot.drive(Direction)`, `robot.facing()`, `robot.atGoal()`. Each successful
drive extends the **Maze Trail** the library records; a drive blocked by a wall
does nothing and adds no Trail entry (so a buggy algorithm that drives into a
wall shows the robot *actually stuck there* when animated).

### 5. Emit the steps (new)

After `solve` returns, the **harness** — not the student — prints the Trail as a
single **sentinel-tagged line**:

```
__TRAIL__ [[0,0],[0,1],[1,1],[1,2]]
```

- The `__TRAIL__` prefix makes the line unambiguously extractable even when the
student's own `System.out.println` debugging is interleaved in stdout.
- The payload is a **JSON array of `[row, col]` pairs**, in `row`-major
(Java-native) coordinates — see the coordinate note below.
- It is the full `robot.trail()`, including the Start cell as the first entry, so
the animation has a complete path from `trail[0]`.

### 6. Parse the steps back (new)

The JS glue takes `RunOutcome.output`, finds the line beginning with the
sentinel, and `JSON.parse`s the remainder. `runJava` already returns captured
stdout in `RunOutcome.output`, and already flags compile/runtime failure via
`RunOutcome.ok` — the parser only runs on `ok === true` and simply reports "no
trail found" if the sentinel line is absent.

### 7. Animate the steps (new wiring)

`MazePlayground` replays the parsed Trail by placing the robot dot at each cell
in sequence (the same interval-driven loop shape as today's `runSolver`, but
*reading* cells instead of *computing* moves). Because the Trail is exactly what
happened in Java, the JS side needs **no wall logic at all** — it does not check
openings, it just plays the cells back.

## The wire contract (authoritative)

| Direction | Payload | Encoding |
| --- | --- | --- |
| JS → Java | current maze grid + start/goal | `int[][]` literal interpolated into harness source (`serializeToJava` format) |
| Java → JS | Maze Trail | one line: `__TRAIL__ ` + JSON `[[row,col],…]` on stdout |

**Coordinate convention — the one conversion point.** Java is row-major
(`maze[row][col]`, `Cell.row()/col()`), but `MazePlayground`'s robot state is
`[x, y]` = `[col, row]`. They are transposed. The wire carries **`[row, col]`**
(Java-native); the JS parser performs the single `[row, col] → [x, y]` swap on
the way in. Do the swap in exactly one place and nowhere else.

## Harness shape (illustrative)

The app builds a full compilation unit; `prepareSource` in `javaRuntime.ts`
already runs a declared class with a `main`, so no changes to the runtime's
wrapping are needed. Roughly:

```java
import com.frc2713.mazesolver.*;

public class MazeRun {
public static void main(String[] args) {
int[][] grid = { /* interpolated from the current JS maze */ };
Maze maze = new GridMaze(grid);
Robot robot = maze.robot();

solve(robot); // <-- student's algorithm runs here

// Emit the Maze Trail as a sentinel-tagged JSON line.
StringBuilder sb = new StringBuilder("__TRAIL__ [");
Cell[] trail = robot.trail();
for (int i = 0; i < trail.length; i++) {
if (i > 0) sb.append(',');
sb.append('[').append(trail[i].row()).append(',').append(trail[i].col()).append(']');
}
System.out.println(sb.append(']'));
}

// ===== student-authored, spliced in by the harness builder =====
static void solve(Robot robot) {
// e.g. wall follower, or whatever the student wrote
}
}
```

## Edge cases the implementation must handle

- **Algorithm never reaches the Goal.** The Trail is still valid — animate the
partial path. `atGoal()` at the end tells you whether it succeeded; the
animation can show "stuck"/"gave up" the same way the current playground does.
- **Student debug output.** Tolerated by design — the sentinel line is found
regardless of other stdout. Show the rest of the output as-is if useful.
- **Compile or runtime error.** Handled by the existing `RunOutcome.ok === false`
path (see `simplifyCompileErrors`/`simplifyRuntimeError`); no Trail is parsed.
- **No sentinel line on success.** Treat as "algorithm produced no trail" and
surface a clear message rather than silently animating nothing.
- **Empty/one-cell Trail.** Robot never left Start — animate nothing / a no-op.

## Exists vs. to-build

**Exists (reuse):**
- `generateMaze` and the bitmask grid (`MazePlayground.tsx`).
- `serializeToJava`'s literal format (`MazePlayground.tsx`).
- `runJava` / `RunOutcome`, stdout capture, compile+run, error simplification,
`prepareSource` class-with-`main` handling (`javaRuntime.ts`).
- The `maze-solver` library API — `GridMaze`, `Robot`, `Cell`, `Direction`
(external repo `FRC2713/maze-solver-java`, vendored as
`site/public/maze-solver.jar`).
- `MazePlayground`'s interval-driven robot animation loop.

**To build (the new glue):**
- A **harness builder** that composes the interpolated grid + spliced `solve`
into the `MazeRun` source (factor the literal out of `serializeToJava`).
- The **`__TRAIL__` sentinel emit** convention in the harness.
- A **trail parser** on the JS side (sentinel scan → `JSON.parse` → `[row,col]→
[x,y]` swap).
- Wiring `MazePlayground` to call `runJava` and replay the parsed Trail (a new
solver "mode" alongside the existing `'random' | 'naive' | 'wall'`).

## Integration details (confirmed against the library)

Resolved against the vendored interface jar's bytecode while wiring this up:
- `robot.trail()` returns **`int[][]`** — already `[row, col]` pairs, not `Cell[]`.
So the harness emits the Trail by iterating the `int[][]` directly (simpler
than the `Cell.row()/col()` the earlier draft assumed).
- `new GridMaze(grid)` returns a `Maze`; `maze.robot()` returns a Start-positioned
`Robot`. `GridMaze` now lives in the library itself, with Start = (0,0) and
Goal = (rows-1, cols-1).
- The sensor/drivetrain methods are **`readWallSensor(Direction)`** and
**`drive(Direction)`**, with `facing()` remembering the last drive; `Direction`
is an `enum { UP, DOWN, LEFT, RIGHT }` with `bit()`, `opposite()`,
`clockwise()`, `counterClockwise()`.
Loading