Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
13 changes: 13 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,16 @@ 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)` resolves
against `site/public/maze-engine.jar` — a concrete engine (GridMaze/GridRobot/
GridCell implementing the library interfaces) vendored on the CheerpJ classpath
while the `maze-solver` library still ships interfaces only; built by
`site/scripts/build-maze-engine.sh` and committed like `tools.jar`. Delete it
and the `ENGINE_JAR` classpath entry once the library ships its own `GridMaze`.
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.
226 changes: 226 additions & 0 deletions docs/maze-roundtrip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
# 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 shim.** The `maze-solver` library ships only interfaces (Maze/Robot/
Cell) — no concrete `GridMaze` — so `new GridMaze(grid)` resolves against
`site/public/maze-engine.jar`, a small concrete engine the site vendors on the
classpath (built by `site/scripts/build-maze-engine.sh`, sources in
`site/scripts/maze-engine/`, committed like `tools.jar`). Delete the jar, its
sources, the build script, and the `ENGINE_JAR` classpath entry in
`javaRuntime.ts` once the library ships its own `GridMaze`.

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.canMove*/move*/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 absolute-move API — `robot.canMoveUp/Right/Down/Left()`,
`robot.moveUp/…()`, `robot.atGoal()`. Each successful move extends the **Maze
Trail** the library records; a move 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` (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` is not in the library yet — see the library-shim note at the
top; it's supplied by `maze-engine.jar` with Start = (0,0) and Goal =
(rows-1, cols-1).
- The move/query methods are **`canMoveUp/Down/Left/Right`** and
**`moveUp/Down/Left/Right`** (the lesson's spelling, not `CONTEXT.md`'s `canGo*`).
Loading