From 50745bbb32321076833fcf1820749b2abd3b6e8d Mon Sep 17 00:00:00 2001 From: Ty Tremblay Date: Mon, 27 Jul 2026 10:45:24 -0400 Subject: [PATCH 1/5] Add maze module: generator playground, wall-follower, maze-solver integration New lesson "Navigating a maze" and its interactive tooling, plus the plumbing to run FRC2713/maze-solver-java in the browser (builds on the ordering refactor). - MazePlayground: renders a generated maze (correct recursive-backtracking, since the maze-generator npm package's shuffle is broken), with a drivable robot, start/goal markers, an animated wall-follower, and "Copy for Java" serialization to an int[][] bitmask literal. - Lesson pages build from "what a maze is" -> robot navigation -> algorithms (sense/decide/move) -> the wall follower -> the same algorithm in raw Java -> the same algorithm against the maze-solver library's Robot/Cell API. - Site wiring: `maze` fence (lessons.ts/LessonView), maze-solver.jar on the CheerpJ classpath, a CI step + vendor script that build the jar (gitignored). - CONTEXT.md glossary for the maze module. Co-Authored-By: Claude Opus 4.8 --- .claude/settings.json | 5 + .github/workflows/deploy-pages.yml | 20 ++ CONTEXT.md | 68 ++++++ lessons/35-maze-generator/README.md | 310 ++++++++++++++++++++++++ site/.gitignore | 3 + site/package-lock.json | 7 + site/package.json | 4 +- site/scripts/vendor-maze-solver.sh | 23 ++ site/src/components/MazePlayground.tsx | 318 +++++++++++++++++++++++++ site/src/lib/javaRuntime.ts | 7 +- site/src/lib/lessons.ts | 17 ++ site/src/maze-generator.d.ts | 9 + site/src/routes/LessonView.tsx | 19 +- 13 files changed, 805 insertions(+), 5 deletions(-) create mode 100644 .claude/settings.json create mode 100644 CONTEXT.md create mode 100644 lessons/35-maze-generator/README.md create mode 100755 site/scripts/vendor-maze-solver.sh create mode 100644 site/src/components/MazePlayground.tsx create mode 100644 site/src/maze-generator.d.ts diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..f56dd76 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "frc2713-skills@frc2713": true + } +} diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index b0ea505..881f1af 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -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 diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..05e3e5a --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,68 @@ +# 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. + +**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 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. diff --git a/lessons/35-maze-generator/README.md b/lessons/35-maze-generator/README.md new file mode 100644 index 0000000..289e014 --- /dev/null +++ b/lessons/35-maze-generator/README.md @@ -0,0 +1,310 @@ +--- +title: "Navigating a maze" +goal: "See a maze as a robot's world — a grid it must cross — and understand how an algorithm steps through it: sense, decide, move, repeat, until it reaches the goal." +order: 350 +section: "Extras" +--- + +# A maze is a robot's world + +On the right is a maze. It's fun to solve by hand — but for us it's really a +tiny, simplified version of the problem every FRC robot faces: **"I'm here, I +need to get there, and there are things in my way."** + +Strip a competition field down to its essentials and you get exactly this: a +space divided into a **grid** of cells, some paths open, some blocked by +**walls**, a place you **start**, and a place you're trying to **reach**. + +- The **grid of cells** is the field, chopped into squares the robot can occupy. +- The **walls** are obstacles — you can't drive through them. +- The **green** cell is the **start**; the **red** cell is the **goal**. +- The **blue dot** is the **robot**. + +Before we talk about *how* a robot crosses this, get a feel for it: use the +arrow buttons (or your arrow keys) and drive the robot from green to red. Notice +you can't move through walls — only through the openings. + +```maze +``` + +# The robot only knows what's around it + +Here's the crucial thing about robots — and it's easy to miss because *you* can +see the whole maze at once. **The robot can't.** + +A real robot doesn't get a bird's-eye view. It knows two things: + +1. **Where it is right now** — which cell it's sitting in. That's its + **state**: a single fact, "I am at column 3, row 5." +2. **What's immediately around it** — which of its four sides (up, down, left, + right) are open, and which are walls. That's its **sensor reading** for this + moment. + +That's it. From one cell, the robot can only see its own four walls. It has to +make its next decision using just that — then move, look again, and decide +again. + +When *you* press an arrow, you're being the robot's decision-maker for one step: +you pick a direction, and it either moves (opening) or stays put (wall). Each +press is one **"sense → decide → move"** cycle. + +```maze +``` + +# An algorithm is that loop, written down + +Driving by hand is fine, but robots run **algorithms** — a plan the computer +repeats, the same way every time, until the job is done. And a maze algorithm +is just the loop you've been doing by hand, spelled out: + +```text +start at the green cell +repeat until you reach the red cell: + look at which directions are open (sense) + choose one of them (decide) + step that way (move) +``` + +Everything interesting lives in that middle line — **"choose one."** *How* the +robot chooses is what makes one algorithm smart and another one dumb: + +- **Wall follower** — "always keep your right hand on the wall." Simple, needs + no memory, and it works for a lot of mazes. +- **Depth-first search** — "keep pushing into new cells; when you hit a dead + end, back up to the last spot with an untried opening and try that." + (Fun fact: this maze was *built* by that exact idea, running in reverse.) +- **Breadth-first search** — "explore all the cells one step away, then all the + cells two steps away…" — slower to write, but it finds the *shortest* path. + +They all share the same skeleton — **sense, decide, move, repeat** — and differ +only in the "decide" step. That skeleton is the shape of an enormous amount of +robot code: read your sensors, decide what to do, act, and loop. + +## A closer look at the wall follower + +The wall follower is the simplest of the three, and it's worth understanding in +full because it shows how a *tiny* rule can produce smart-looking behavior. + +**How it works.** Imagine walking the maze with your **right hand** pressed flat +against the wall. You never lift it. At every cell the robot runs through the +same four choices, always in this order, and takes the **first** one that's +open: + +```text +1. turn right — is there an opening to my right? take it. +2. go straight — else, is the way ahead open? take it. +3. turn left — else, is there an opening left? take it. +4. turn around — else, it's a dead end. go back. +``` + +That "right first" order is exactly what keeping your right hand on the wall +does: you hug the wall by always turning toward it when you can, and only peel +away when you must. The robot needs to remember just **one** thing between +steps — the direction it's currently facing — so it can tell which way "right" +is. No map. No list of visited cells. No idea where the goal is. + +**Why it works.** Here's the surprising part. The walls of this maze aren't a +scattering of separate obstacles — they're all **one single connected piece**. +The generator carved the maze by knocking out walls without ever sealing off a +loop, so what's left is one continuous wall with no islands (mazes like this are +called *simply connected*, or "perfect" mazes). + +Now picture tracing your finger along the edge of one connected shape — say, the +outline of a single puzzle piece. Keep going and you always come back around; +you can't get stranded, because there's only one border to follow. The wall +follower does exactly that: it traces the boundary of that one giant wall. Since +the start and the goal both sit on that same connected boundary, faithfully +following it **must** eventually walk the robot from one to the other. It might +wander down dead ends and back out — but it can never get permanently lost. + +The catch is right there in the "why": it only works when the walls are one +connected piece. Add a loop to the maze — a wall island floating in the middle — +and the robot can end up circling that island forever, hugging a border that +never touches the goal. For our perfect mazes, though, it's guaranteed. + +Hit **Run wall follower** below and watch it happen: no map, no memory of where +it's been, just those four choices in order, over and over, until it lands on +the goal. + +```maze +solver: wall +``` + +# The same algorithm, in Java + +Everything so far has been visual. But the whole point is that a maze is just +**data**, and an algorithm is just **code** — and once it's data and code, a +robot can run it. Here's that exact wall follower written in Java, reading a +maze that was serialized straight out of the generator on the previous page. + +The maze is an `int[][]` — a grid of numbers. Each number is the same +**N/S/E/W bitmask** you've been looking at: a set bit means that side of the +cell is open. Reading a wall is one operation: `(maze[row][col] & E) != 0` asks +"is the east side open — can I move right?" + +The loop below is the flowchart from earlier turned into code: while we're not +at the goal, try to turn right, else go straight, else turn left, else turn +around — take the first open direction, step, repeat. Press **Run** and watch it +count its way to the exit. Then try editing the start heading, or paste your own +maze from the **Copy for Java** button and run *that*. + +```java +public class Maze { + // Wall bits: for a cell, a SET bit means that side is open. + static final int N = 1, S = 2, E = 4, W = 8; + + // Headings, clockwise: 0 = up, 1 = right, 2 = down, 3 = left. + // BIT/DR/DC line up with those indexes: which wall to check, and how the + // row/column change when you step that way. + static final int[] BIT = { N, E, S, W }; + static final int[] DR = { -1, 0, 1, 0 }; + static final int[] DC = { 0, 1, 0, -1 }; + static final char[] FACE = { 'U', 'R', 'D', 'L' }; + + public static void main(String[] args) { + // A maze serialized from the generator: each cell is an N/S/E/W bitmask. + int[][] maze = { + {4, 12, 10, 6, 10, 4, 14, 10, 4, 12, 14, 12, 12, 10}, + {6, 10, 5, 9, 5, 12, 9, 5, 12, 12, 9, 6, 12, 9}, + {3, 5, 12, 14, 12, 12, 12, 10, 6, 12, 10, 3, 6, 8}, + {3, 6, 10, 3, 6, 8, 6, 9, 5, 10, 3, 3, 5, 10}, + {5, 9, 3, 3, 7, 12, 13, 12, 10, 1, 3, 5, 10, 3}, + {6, 8, 3, 3, 5, 8, 6, 10, 5, 10, 3, 6, 9, 3}, + {3, 6, 9, 5, 10, 6, 9, 3, 4, 13, 9, 5, 12, 11}, + {3, 5, 12, 10, 5, 9, 2, 5, 12, 12, 12, 14, 10, 3}, + {7, 14, 8, 5, 10, 4, 13, 12, 12, 14, 10, 3, 1, 3}, + {3, 3, 6, 10, 5, 10, 6, 12, 12, 9, 3, 5, 12, 9}, + {1, 3, 3, 5, 12, 9, 3, 6, 12, 10, 3, 2, 6, 10}, + {6, 11, 3, 4, 12, 14, 9, 3, 2, 5, 9, 7, 9, 3}, + {3, 1, 3, 6, 10, 3, 6, 9, 5, 12, 12, 13, 8, 3}, + {5, 12, 13, 9, 5, 9, 5, 12, 12, 12, 12, 12, 12, 9} + }; + int startRow = 0, startCol = 0; + int goalRow = 13, goalCol = 13; + + int row = startRow, col = startCol; + int dir = 1; // start facing right + int steps = 0; + int maxSteps = maze.length * maze[0].length * 4; + StringBuilder path = new StringBuilder(); + + System.out.println("Start at (" + row + ", " + col + "), goal is (" + + goalRow + ", " + goalCol + ")"); + + while (!(row == goalRow && col == goalCol) && steps < maxSteps) { + // Right-hand rule: try right (+1), straight (0), left (+3), + // back (+2) — take the first heading whose wall is open. + for (int turn : new int[] { 1, 0, 3, 2 }) { + int d = (dir + turn) % 4; + if ((maze[row][col] & BIT[d]) != 0) { + dir = d; + row += DR[d]; + col += DC[d]; + path.append(FACE[d]); + break; + } + } + steps++; + } + + if (row == goalRow && col == goalCol) { + System.out.println("Reached the goal in " + steps + " steps!"); + System.out.println("Path: " + path); + } else { + System.out.println("Gave up after " + steps + " steps."); + } + } +} +``` + +# Solving with a maze library + +That last program worked, but look at how much of it was *plumbing* — bitmask +constants, `DR`/`DC` offset arrays, `maze[row][col] & BIT[d]`. None of that is +the algorithm. It's bookkeeping you have to get exactly right before you can +even start thinking about *how to solve the maze*. + +That's what a **library** is for. Our team's `maze-solver` library +(`com.frc2713.mazesolver`) wraps all of that up and hands you three friendly +tools: + +- a **`Maze`** you build from the grid, +- a **`Robot`** that walks it — `robot.canMoveRight()`, `robot.moveRight()`, + `robot.atGoal()`, +- and **`Cell`**s you can ask plain questions like `cell.wallRight()`. + +No bitmasks. The exact same wall-follower now reads like the *idea* instead of +the bookkeeping — "if I can turn right, turn right; otherwise go straight, then +left, then back." You write the algorithm; the library handles the maze. + +```java +import com.frc2713.mazesolver.*; + +public class SolveMaze { + public static void main(String[] args) { + int[][] grid = { + {4, 12, 10, 6, 10, 4, 14, 10, 4, 12, 14, 12, 12, 10}, + {6, 10, 5, 9, 5, 12, 9, 5, 12, 12, 9, 6, 12, 9}, + {3, 5, 12, 14, 12, 12, 12, 10, 6, 12, 10, 3, 6, 8}, + {3, 6, 10, 3, 6, 8, 6, 9, 5, 10, 3, 3, 5, 10}, + {5, 9, 3, 3, 7, 12, 13, 12, 10, 1, 3, 5, 10, 3}, + {6, 8, 3, 3, 5, 8, 6, 10, 5, 10, 3, 6, 9, 3}, + {3, 6, 9, 5, 10, 6, 9, 3, 4, 13, 9, 5, 12, 11}, + {3, 5, 12, 10, 5, 9, 2, 5, 12, 12, 12, 14, 10, 3}, + {7, 14, 8, 5, 10, 4, 13, 12, 12, 14, 10, 3, 1, 3}, + {3, 3, 6, 10, 5, 10, 6, 12, 12, 9, 3, 5, 12, 9}, + {1, 3, 3, 5, 12, 9, 3, 6, 12, 10, 3, 2, 6, 10}, + {6, 11, 3, 4, 12, 14, 9, 3, 2, 5, 9, 7, 9, 3}, + {3, 1, 3, 6, 10, 3, 6, 9, 5, 12, 12, 13, 8, 3}, + {5, 12, 13, 9, 5, 9, 5, 12, 12, 12, 12, 12, 12, 9} + }; + + // The library turns the raw grid into a maze and a robot at the start. + Maze maze = new GridMaze(grid); + Robot robot = maze.robot(); + + // Heading, clockwise: 0 = up, 1 = right, 2 = down, 3 = left. + int heading = 1; // start facing right + int cap = maze.rows() * maze.cols() * 4; // safety stop + + while (!robot.atGoal() && robot.trail().length <= cap) { + // Right-hand rule: prefer turning right, then straight, then left, + // then back — the first heading the robot can actually move. + for (int turn : new int[] { 1, 0, 3, 2 }) { + int dir = (heading + turn) % 4; + if (canMove(robot, dir)) { + heading = dir; + move(robot, dir); + break; + } + } + } + + if (robot.atGoal()) { + System.out.println("Reached the goal in " + (robot.trail().length - 1) + " steps!"); + } else { + System.out.println("Gave up."); + } + } + + // Ask the robot, in one absolute direction, whether it can move. + static boolean canMove(Robot robot, int dir) { + switch (dir) { + case 0: return robot.canMoveUp(); + case 1: return robot.canMoveRight(); + case 2: return robot.canMoveDown(); + default: return robot.canMoveLeft(); + } + } + + // Move the robot one step in that absolute direction. + static void move(Robot robot, int dir) { + switch (dir) { + case 0: robot.moveUp(); break; + case 1: robot.moveRight(); break; + case 2: robot.moveDown(); break; + default: robot.moveLeft(); break; + } + } +} +``` diff --git a/site/.gitignore b/site/.gitignore index a547bf3..f1734a1 100644 --- a/site/.gitignore +++ b/site/.gitignore @@ -12,6 +12,9 @@ dist dist-ssr *.local +# Built by CI on deploy / scripts/vendor-maze-solver.sh locally, not committed. +public/maze-solver.jar + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/site/package-lock.json b/site/package-lock.json index c427862..bbf0ca6 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -19,6 +19,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.23.0", + "maze-generator": "^0.1.2", "radix-ui": "^1.6.1", "react": "^19.2.7", "react-dom": "^19.2.7", @@ -3894,6 +3895,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/maze-generator": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/maze-generator/-/maze-generator-0.1.2.tgz", + "integrity": "sha512-pO2vFebwwRsKnztceR0VcgLMNaYynUO6XSNJepke8t8zJewQbB3D8Mltf+ZSDOFxhXba+4aeleM/nR3pDbxxeQ==", + "license": "MIT" + }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", diff --git a/site/package.json b/site/package.json index 1a78c47..6c47cc3 100644 --- a/site/package.json +++ b/site/package.json @@ -7,7 +7,8 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "oxlint", - "preview": "vite preview" + "preview": "vite preview", + "vendor:maze-solver": "bash scripts/vendor-maze-solver.sh" }, "dependencies": { "@codemirror/lang-java": "^6.0.2", @@ -21,6 +22,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.23.0", + "maze-generator": "^0.1.2", "radix-ui": "^1.6.1", "react": "^19.2.7", "react-dom": "^19.2.7", diff --git a/site/scripts/vendor-maze-solver.sh b/site/scripts/vendor-maze-solver.sh new file mode 100755 index 0000000..4d86c17 --- /dev/null +++ b/site/scripts/vendor-maze-solver.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Build the FRC2713/maze-solver-java jar and vendor it into public/ so the +# Java playground can load its com.frc2713.mazesolver API locally. +# +# Production doesn't use this script — the Pages workflow builds the jar fresh +# on every deploy (see .github/workflows/deploy-pages.yml). Run this once for +# local `npm run dev`/`npm run build`. Requires a JDK (>= 8, capable of +# --release 8) and Maven on your PATH. +set -euo pipefail + +REPO="${MAZE_SOLVER_REPO:-https://github.com/FRC2713/maze-solver-java}" +REF="${MAZE_SOLVER_REF:-main}" +here="$(cd "$(dirname "$0")/.." && pwd)" + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +echo "Cloning $REPO@$REF ..." +git clone --depth 1 --branch "$REF" "$REPO" "$tmp/lib" +echo "Building jar ..." +( cd "$tmp/lib" && mvn -B -q -DskipTests package ) +cp "$tmp/lib/target/maze-solver.jar" "$here/public/maze-solver.jar" +echo "Vendored public/maze-solver.jar ($(wc -c < "$here/public/maze-solver.jar") bytes)" diff --git a/site/src/components/MazePlayground.tsx b/site/src/components/MazePlayground.tsx new file mode 100644 index 0000000..e790d4a --- /dev/null +++ b/site/src/components/MazePlayground.tsx @@ -0,0 +1,318 @@ +import { useCallback, useEffect, useRef, useState } from 'react' + +// NOTE: the `maze-generator` npm package is broken — its internal `shuffle` is +// written as `for (...; ...; update) return o`, so it returns on the first +// iteration and the Math.random in the update slot never runs. The direction +// order is therefore always ['N','E','S','W'], producing the identical +// degenerate maze every call (and it schedules via a Node-only `setImmediate` +// on top of that). So we generate the same N/S/E/W bitmask grid it was meant +// to produce with a correct recursive-backtracking walk. + +// Cell bitmask: which sides of a cell have been carved open. +const N = 1 +const S = 2 +const E = 4 +const W = 8 +const DX: Record = { E: 1, W: -1, N: 0, S: 0 } +const DY: Record = { E: 0, W: 0, N: -1, S: 1 } +const BIT: Record = { N, S, E, W } +const OPP: Record = { E: W, W: E, N: S, S: N } +const DIRS = ['N', 'E', 'S', 'W'] + +function generateMaze(size: number): number[][] { + const grid = Array.from({ length: size }, () => Array(size).fill(0)) + const seen = Array.from({ length: size }, () => Array(size).fill(false)) + const stack: [number, number][] = [[0, 0]] + seen[0][0] = true + while (stack.length) { + const [x, y] = stack[stack.length - 1] + const dirs = [...DIRS].sort(() => Math.random() - 0.5) + let moved = false + for (const d of dirs) { + const nx = x + DX[d] + const ny = y + DY[d] + if (nx >= 0 && nx < size && ny >= 0 && ny < size && !seen[ny][nx]) { + grid[y][x] |= BIT[d] + grid[ny][nx] |= OPP[d] + seen[ny][nx] = true + stack.push([nx, ny]) + moved = true + break + } + } + if (!moved) stack.pop() + } + return grid +} + +const SIZE = 14 // maze is SIZE x SIZE cells +const CELL = 22 // px per cell +const PAD = 12 + +const START: [number, number] = [0, 0] +const GOAL: [number, number] = [SIZE - 1, SIZE - 1] + +// Which wall bit must be open to step in each direction, keyed by arrow. +const MOVES: Record<'up' | 'down' | 'left' | 'right', { bit: number; dx: number; dy: number }> = { + up: { bit: N, dx: 0, dy: -1 }, + down: { bit: S, dx: 0, dy: 1 }, + left: { bit: W, dx: -1, dy: 0 }, + right: { bit: E, dx: 1, dy: 0 }, +} + +// Center of a cell in svg coordinates. +const cx = (x: number) => PAD + x * CELL + CELL / 2 +const cy = (y: number) => PAD + y * CELL + CELL / 2 + +// Serialize the maze as a ready-to-paste Java `int[][]` literal. Each cell is +// the same N/S/E/W bitmask the grid already uses, so Java reads it with the +// identical constants (N=1, S=2, E=4, W=8) and `cell & N` wall checks. +function serializeToJava(grid: number[][]): string { + const rows = grid.map((row) => ' {' + row.join(', ') + '}').join(',\n') + return `// Maze as an N/S/E/W bitmask grid. For each cell, a set bit means that +// side is OPEN (you can move that way); a clear bit is a wall. +// N = 1 (up) S = 2 (down) E = 4 (right) W = 8 (left) +// Example check: (maze[row][col] & E) != 0 --> can move right. +final int N = 1, S = 2, E = 4, W = 8; + +int[][] maze = { +${rows} +}; + +// Where the robot starts and where it's trying to get to (row, col): +int startRow = ${START[1]}, startCol = ${START[0]}; +int goalRow = ${GOAL[1]}, goalCol = ${GOAL[0]};` +} + +// Headings for the wall follower, clockwise: 0=up, 1=right, 2=down, 3=left. +const DIR4 = [ + { bit: N, dx: 0, dy: -1 }, // up + { bit: E, dx: 1, dy: 0 }, // right + { bit: S, dx: 0, dy: 1 }, // down + { bit: W, dx: -1, dy: 0 }, // left +] + +export function MazePlayground({ solver = false }: { solver?: boolean }) { + const [grid, setGrid] = useState(() => generateMaze(SIZE)) + const [robot, setRobot] = useState<[number, number]>(START) + const [running, setRunning] = useState(false) + const timer = useRef | null>(null) + const [copied, setCopied] = useState(false) + + const copyForJava = () => { + const java = serializeToJava(grid) + navigator.clipboard?.writeText(java).then( + () => { + setCopied(true) + setTimeout(() => setCopied(false), 1500) + }, + () => {}, + ) + } + + const stopSolver = useCallback(() => { + if (timer.current) clearInterval(timer.current) + timer.current = null + setRunning(false) + }, []) + + // "Right-hand rule": at each step try to turn right, else go straight, else + // turn left, else turn around — taking the first direction that's open. In a + // perfect maze (no loops) this always reaches the goal. + const runWallFollower = useCallback(() => { + stopSolver() + let x = START[0] + let y = START[1] + let dir = 1 // start heading right + let steps = 0 + const maxSteps = SIZE * SIZE * 4 + setRobot([x, y]) + setRunning(true) + timer.current = setInterval(() => { + if ((x === GOAL[0] && y === GOAL[1]) || steps++ > maxSteps) { + stopSolver() + return + } + for (const nd of [(dir + 1) % 4, dir, (dir + 3) % 4, (dir + 2) % 4]) { + const { bit, dx, dy } = DIR4[nd] + if (grid[y][x] & bit) { + dir = nd + x += dx + y += dy + break + } + } + setRobot([x, y]) + }, 180) + }, [grid, stopSolver]) + + const generate = () => { + stopSolver() + setGrid(generateMaze(SIZE)) + setRobot(START) + } + + const move = useCallback( + (dir: keyof typeof MOVES) => { + stopSolver() + setRobot(([x, y]) => { + const { bit, dx, dy } = MOVES[dir] + // Can only move through a carved opening (no wall on that side). + if (grid[y][x] & bit) return [x + dx, y + dy] + return [x, y] + }) + }, + [grid, stopSolver], + ) + + useEffect(() => { + setGrid(generateMaze(SIZE)) + setRobot(START) + }, []) + + // Arrow keys drive the robot too. + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + const map: Record = { + ArrowUp: 'up', + ArrowDown: 'down', + ArrowLeft: 'left', + ArrowRight: 'right', + } + const dir = map[e.key] + if (dir) { + e.preventDefault() + move(dir) + } + } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + }, [move]) + + // Stop any running animation when the component goes away. + useEffect(() => stopSolver, [stopSolver]) + + const w = SIZE * CELL + PAD * 2 + const stroke = 'var(--foreground, #e5e5e5)' + const solved = robot[0] === GOAL[0] && robot[1] === GOAL[1] + + const arrow = (dir: keyof typeof MOVES, label: string) => ( + + ) + + return ( +
+ + {/* Start (green) and goal (red) cells. */} + + + + {grid.map((row, y) => + row.map((cell, x) => { + const px = PAD + x * CELL + const py = PAD + y * CELL + const line = (x1: number, y1: number, x2: number, y2: number, k: string) => ( + + ) + // Draw a wall on every side that was NOT carved open. + return ( + + {!(cell & N) && line(px, py, px + CELL, py, 'n')} + {!(cell & S) && line(px, py + CELL, px + CELL, py + CELL, 's')} + {!(cell & W) && line(px, py, px, py + CELL, 'w')} + {!(cell & E) && line(px + CELL, py, px + CELL, py + CELL, 'e')} + + ) + }), + )} + + {/* The robot. */} + + + +

+ start   + goal   + robot + {solved && — solved! 🎉} +

+ + {/* Directional pad. */} +
+
+ {arrow('up', '↑')} +
+ {arrow('left', '←')} + {arrow('down', '↓')} + {arrow('right', '→')} +
+ +
+ {solver && ( + + )} + + +
+
+ ) +} diff --git a/site/src/lib/javaRuntime.ts b/site/src/lib/javaRuntime.ts index 828b9df..b793410 100644 --- a/site/src/lib/javaRuntime.ts +++ b/site/src/lib/javaRuntime.ts @@ -12,7 +12,12 @@ const CHEERPJ_LOADER_URL = 'https://cjrtnc.leaningtech.com/4.3/loader.js' // CheerpJ mounts the site's HTTP origin at /app/, so the vendored compiler // lives under the Vite base path (/software_training/ in production). const TOOLS_JAR = `/app${import.meta.env.BASE_URL}tools.jar` -const CLASS_PATH = `${TOOLS_JAR}:/files/` +// FRC2713/maze-solver-java, built into a jar and vendored the same way. Gives +// lesson snippets the com.frc2713.mazesolver API (Maze/Cell/Robot/MazeSolver) +// so students write solving algorithms against it instead of raw bitmasks. The +// jar is produced by CI on deploy and by scripts/vendor-maze-solver.sh locally. +const SOLVER_JAR = `/app${import.meta.env.BASE_URL}maze-solver.jar` +const CLASS_PATH = `${TOOLS_JAR}:${SOLVER_JAR}:/files/` // Globals installed by loader.js (a classic script, not an ES module). declare global { diff --git a/site/src/lib/lessons.ts b/site/src/lib/lessons.ts index 6724ef9..817f291 100644 --- a/site/src/lib/lessons.ts +++ b/site/src/lib/lessons.ts @@ -144,6 +144,23 @@ export function blocksPreset(markdown: string): string | null { export function stripBlocksFence(markdown: string): string { return markdown .replace(/```blocks\r?\n[\s\S]*?```/g, '') + .replace(/```maze[\s\S]*?```/g, '') .replace(/\n{3,}/g, '\n\n') .trim() } + +// A page can swap the playground for the maze-generator demo with a ```maze +// fence. Like ```blocks it's a directive, not content, so it's stripped from +// the prose. +const MAZE_FENCE = /```maze\r?\n?([\s\S]*?)```/ + +export function hasMaze(markdown: string): boolean { + return MAZE_FENCE.test(markdown) +} + +// The maze fence can carry a `solver:` directive to reveal the wall-follower +// animation button (used only on the algorithm page). +export function mazeShowsSolver(markdown: string): boolean { + const match = MAZE_FENCE.exec(markdown) + return match ? /solver:\s*wall/.test(match[1]) : false +} diff --git a/site/src/maze-generator.d.ts b/site/src/maze-generator.d.ts new file mode 100644 index 0000000..61dfcee --- /dev/null +++ b/site/src/maze-generator.d.ts @@ -0,0 +1,9 @@ +// The `maze-generator` npm package ships no types. It exports a single function +// that returns a grid of cells, each an N/S/E/W bitmask (N=1, S=2, E=4, W=8) +// marking which sides are carved open. +declare module 'maze-generator' { + export default function mazeGenerator( + size: [number, number], + algorithm?: string, + ): number[][] +} diff --git a/site/src/routes/LessonView.tsx b/site/src/routes/LessonView.tsx index 0ddd746..a95344e 100644 --- a/site/src/routes/LessonView.tsx +++ b/site/src/routes/LessonView.tsx @@ -7,6 +7,8 @@ import { blocksPreset, firstJavaSnippet, getLesson, + hasMaze, + mazeShowsSolver, lessonNumber, nextLesson, stripBlocksFence, @@ -14,6 +16,7 @@ import { import { JavaRunner } from '@/components/JavaRunner' import { CodeBlock } from '@/components/CodeBlock' import { BlockPlayground } from '@/components/BlockPlayground' +import { MazePlayground } from '@/components/MazePlayground' import { StatePlayground } from '@/components/StatePlayground' import { isStatePreset } from '@/lib/statePresets' import { PageNav } from '@/components/PageNav' @@ -87,9 +90,11 @@ export function LessonView() { : 0 const currentPage = lesson.pages[pageIndex] const preset = blocksPreset(currentPage.markdown) - const prose = preset ? stripBlocksFence(currentPage.markdown) : currentPage.markdown + const maze = hasMaze(currentPage.markdown) + const prose = + preset || maze ? stripBlocksFence(currentPage.markdown) : currentPage.markdown const javaSnippet = firstJavaSnippet(currentPage.markdown) - const hasPlayground = preset != null || javaSnippet != null + const hasPlayground = preset != null || maze || javaSnippet != null const goTo = (index: number) => navigate(`/lesson/${lesson.slug}/${index + 1}`) @@ -114,7 +119,15 @@ export function LessonView() { {hasPlayground && (
- {preset && isStatePreset(preset) ? ( + {maze ? ( + <> +

▶ Maze

+ + + ) : preset && isStatePreset(preset) ? ( <>

▶ State machine

From aef331597f2d480f53cc6429cdc22a384a22620e Mon Sep 17 00:00:00 2001 From: Ty Tremblay Date: Mon, 27 Jul 2026 12:24:44 -0400 Subject: [PATCH 2/5] Introduce "what is an algorithm" via three maze solvers; move to Algorithms section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reframe the maze lesson around a progressive disclosure of what an algorithm is: a random walk (no plan — unrepeatable), a fixed no-memory rule (a real algorithm, but it ping-pongs between two cells), and the wall follower (an algorithm that works, thanks to one remembered fact: its facing). The sense/decide/move skeleton now lands at the end, after three concrete "decide" steps instead of zero. - MazePlayground: widen `solver` from a boolean to a 'random' | 'naive' | 'wall' mode; generalize the runner and add visible "stuck in a loop" detection for the naive rule (stops once a cell is hit 4x so the oscillation is watchable). - lessons.ts: `mazeShowsSolver` -> `mazeSolver` returning the mode. - Move the lesson into a new "Algorithms" section (order 195), above State Machines; rename its folder 35-maze-generator -> algorithms. - CONTEXT.md: clarify that orientation is the robot's interface constraint, not a ban on an algorithm tracking its own `facing`. Co-Authored-By: Claude Opus 4.8 --- CONTEXT.md | 5 +- .../README.md | 188 ++++++++++++------ site/src/components/MazePlayground.tsx | 119 ++++++++--- site/src/lib/lessons.ts | 15 +- site/src/routes/LessonView.tsx | 4 +- 5 files changed, 237 insertions(+), 94 deletions(-) rename lessons/{35-maze-generator => algorithms}/README.md (61%) diff --git a/CONTEXT.md b/CONTEXT.md index 05e3e5a..cac70fe 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -32,7 +32,10 @@ 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. +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. diff --git a/lessons/35-maze-generator/README.md b/lessons/algorithms/README.md similarity index 61% rename from lessons/35-maze-generator/README.md rename to lessons/algorithms/README.md index 289e014..fcb7d3c 100644 --- a/lessons/35-maze-generator/README.md +++ b/lessons/algorithms/README.md @@ -1,8 +1,8 @@ --- title: "Navigating a maze" -goal: "See a maze as a robot's world — a grid it must cross — and understand how an algorithm steps through it: sense, decide, move, repeat, until it reaches the goal." -order: 350 -section: "Extras" +goal: "See a maze as a robot's world — a grid it must cross — and learn what makes something an algorithm by watching three robots try to solve it: a random walk (no plan), a fixed rule that loops (an algorithm, but a bad one), and the wall follower (sense, decide, move, repeat)." +order: 195 +section: "Algorithms" --- # A maze is a robot's world @@ -51,85 +51,157 @@ press is one **"sense → decide → move"** cycle. ```maze ``` -# An algorithm is that loop, written down +# The dumbest possible robot: move at random -Driving by hand is fine, but robots run **algorithms** — a plan the computer -repeats, the same way every time, until the job is done. And a maze algorithm -is just the loop you've been doing by hand, spelled out: +You just drove the maze by hand, making a real choice at every cell. So here's a +fair question — what if the robot *didn't* choose? What if, at each step, it just +picked one of the open directions **at random** and went? + +Hit **Move at random** and watch. + +```maze +solver: random +``` + +Sometimes it stumbles onto the goal. Sometimes it wanders forever. And here's the +part that matters: **run it again and it does something completely different.** +There's no plan — just a coin flip at every cell. + +That's exactly why this is *not an algorithm*. An algorithm is something you can +write down, hand to someone else, and have them get the **same result you did**. +You can't write down "flip a coin" and call that a plan. Notice, too, that +nothing guarantees it ever finishes — it could bounce around and simply never +happen to land on red. + +(Random isn't worthless — real robots sometimes add a pinch of it on purpose. But +as *the whole strategy*, it's nothing you can count on, and that's the point.) + +# A rule you can repeat + +Let's give the robot an actual rule — the simplest one imaginable. At every cell, +always try the same directions in the same order, and take the first one that's +open: ```text -start at the green cell -repeat until you reach the red cell: - look at which directions are open (sense) - choose one of them (decide) - step that way (move) +at each cell, try these in order and take the first open one: + up, then left, then down, then right ``` -Everything interesting lives in that middle line — **"choose one."** *How* the -robot chooses is what makes one algorithm smart and another one dumb: +That's the entire rule. No randomness, no cleverness. Press **Run the naive +rule**: -- **Wall follower** — "always keep your right hand on the wall." Simple, needs - no memory, and it works for a lot of mazes. -- **Depth-first search** — "keep pushing into new cells; when you hit a dead - end, back up to the last spot with an untried opening and try that." - (Fun fact: this maze was *built* by that exact idea, running in reverse.) -- **Breadth-first search** — "explore all the cells one step away, then all the - cells two steps away…" — slower to write, but it finds the *shortest* path. +```maze +solver: naive +``` -They all share the same skeleton — **sense, decide, move, repeat** — and differ -only in the "decide" step. That skeleton is the shape of an enormous amount of -robot code: read your sensors, decide what to do, act, and loop. +Two things to notice. -## A closer look at the wall follower +First, it's **completely repeatable**. Run it again on this same maze and the +robot retraces the *exact* same steps — every time. That's what random didn't +have, and it's what makes this an **algorithm**: a precise rule that produces the +same result on every run. -The wall follower is the simplest of the three, and it's worth understanding in -full because it shows how a *tiny* rule can produce smart-looking behavior. +Second… it barely gets anywhere. Watch it take a step and then immediately walk +**right back** the way it came, jittering between two cells forever. (A rule with +*no memory* that returns to a cell it's already seen will make the identical +choice it made last time — so it's provably doomed to loop there. We let it +bounce a few times so you can see it, then stop it.) -**How it works.** Imagine walking the maze with your **right hand** pressed flat -against the wall. You never lift it. At every cell the robot runs through the -same four choices, always in this order, and takes the **first** one that's -open: +Why does it trap itself so fast? Because "up" is first in its list, and once +it steps down into a cell, the very next thing it does is look up — back where it +came from — and take it. It has **no memory** of having just been there. It +doesn't know which way it was heading, and it doesn't know the goal exists. + +So being an algorithm doesn't make a rule *good*. This one is perfectly precise +and perfectly repeatable — and perfectly useless for crossing the maze. The bar +for "algorithm" is just *definiteness and repeatability*; **whether it actually +works is a separate question**, and it's the interesting one. Give this robot +just *one* fact to remember, and something surprising happens. + +# An algorithm that actually crosses the maze + +Here's the surprise: the **wall follower** is almost the same rule as the naive +one — a fixed order of directions, take the first open one. It adds exactly **one +remembered fact**: which way the robot is currently facing. And it tries its +directions *relative to that heading* instead of relative to the screen: ```text +at each cell, relative to the way I'm facing, take the first open one: 1. turn right — is there an opening to my right? take it. 2. go straight — else, is the way ahead open? take it. 3. turn left — else, is there an opening left? take it. 4. turn around — else, it's a dead end. go back. ``` -That "right first" order is exactly what keeping your right hand on the wall -does: you hug the wall by always turning toward it when you can, and only peel -away when you must. The robot needs to remember just **one** thing between -steps — the direction it's currently facing — so it can tell which way "right" -is. No map. No list of visited cells. No idea where the goal is. +That's the whole idea behind **"keep your right hand on the wall."** And look at +what it does to the jitter: "turn around" is now **dead last**, so the robot only +reverses at a true dead end — never on the very next step. Because "right" and +"straight" are measured from a heading the robot *remembers* between steps, it +hugs the wall and keeps moving instead of bouncing in place. That single +remembered fact — its facing — is the *only* thing it adds to the naive rule. No +map, no list of visited cells, no idea where the goal is. Press **Run wall +follower**: -**Why it works.** Here's the surprising part. The walls of this maze aren't a -scattering of separate obstacles — they're all **one single connected piece**. -The generator carved the maze by knocking out walls without ever sealing off a -loop, so what's left is one continuous wall with no islands (mazes like this are -called *simply connected*, or "perfect" mazes). +```maze +solver: wall +``` + +## Why it works + +Here's the surprising part. The walls of this maze aren't a scattering of +separate obstacles — they're all **one single connected piece**. The generator +carved the maze by knocking out walls without ever sealing off a loop, so what's +left is one continuous wall with no islands (mazes like this are called *simply +connected*, or "perfect" mazes). Now picture tracing your finger along the edge of one connected shape — say, the -outline of a single puzzle piece. Keep going and you always come back around; -you can't get stranded, because there's only one border to follow. The wall -follower does exactly that: it traces the boundary of that one giant wall. Since -the start and the goal both sit on that same connected boundary, faithfully -following it **must** eventually walk the robot from one to the other. It might -wander down dead ends and back out — but it can never get permanently lost. - -The catch is right there in the "why": it only works when the walls are one -connected piece. Add a loop to the maze — a wall island floating in the middle — -and the robot can end up circling that island forever, hugging a border that -never touches the goal. For our perfect mazes, though, it's guaranteed. - -Hit **Run wall follower** below and watch it happen: no map, no memory of where -it's been, just those four choices in order, over and over, until it lands on -the goal. +outline of a single puzzle piece. Keep going and you always come back around; you +can't get stranded, because there's only one border to follow. The wall follower +does exactly that: it traces the boundary of that one giant wall. Since the start +and the goal both sit on that same connected boundary, faithfully following it +**must** eventually walk the robot from one to the other. It might wander down +dead ends and back out — but it can never get permanently lost. -```maze -solver: wall +That's why the naive rule fails and this one doesn't: it isn't luck, it's the +memory. Facing is just enough state to keep the robot *committed to a wall* +rather than making the same local choice from scratch each time. + +But "works" still isn't "good." The wall follower cheerfully explores every dead +end, and the path it finds is usually far from the **shortest** one. And the +whole guarantee rests on that one assumption — one connected wall. Add a loop to +the maze, a wall island floating in the middle, and the robot can circle that +island forever, hugging a border that never touches the goal. For our perfect +mazes, though, it always gets there. + +## The same skeleton, over and over + +Look back at all three robots and you'll see they're built from the *same loop* — +they only disagree about one step: + +```text +start at the green cell +repeat until you reach the red cell: + look at which directions are open (sense) + choose one of them (decide) + step that way (move) ``` +Everything interesting lives in that middle line — **"choose one."** Random rolls +a die there; the naive rule reads a fixed list; the wall follower consults its +remembered heading. Swap in a smarter "decide" and you get smarter algorithms: + +- **Wall follower** — "keep your right hand on the wall." One remembered fact, + and it clears any perfect maze. +- **Depth-first search** — "keep pushing into new cells; when you hit a dead end, + back up to the last spot with an untried opening and try that." (Fun fact: this + maze was *built* by that exact idea, running in reverse.) +- **Breadth-first search** — "explore all the cells one step away, then all the + cells two steps away…" — more to write, but it finds the *shortest* path. + +Same skeleton — **sense, decide, move, repeat** — every time, differing only in +"decide." That skeleton is the shape of an enormous amount of robot code: read +your sensors, decide what to do, act, and loop. + # The same algorithm, in Java Everything so far has been visual. But the whole point is that a maze is just diff --git a/site/src/components/MazePlayground.tsx b/site/src/components/MazePlayground.tsx index e790d4a..e4478f2 100644 --- a/site/src/components/MazePlayground.tsx +++ b/site/src/components/MazePlayground.tsx @@ -92,10 +92,24 @@ const DIR4 = [ { bit: W, dx: -1, dy: 0 }, // left ] -export function MazePlayground({ solver = false }: { solver?: boolean }) { +// The naive rule's fixed direction priority: up, then left, then down, then +// right. It biases toward the top-left, away from the bottom-right goal, so on +// most mazes the memory-less robot marches into a corner and loops. +const NAIVE_ORDER = [0, 3, 2, 1] + +export type SolverMode = 'random' | 'naive' | 'wall' + +const SOLVER_LABEL: Record = { + random: 'Move at random', + naive: 'Run the naive rule', + wall: 'Run wall follower', +} + +export function MazePlayground({ solver = null }: { solver?: SolverMode | null }) { const [grid, setGrid] = useState(() => generateMaze(SIZE)) const [robot, setRobot] = useState<[number, number]>(START) const [running, setRunning] = useState(false) + const [stuck, setStuck] = useState(false) const timer = useRef | null>(null) const [copied, setCopied] = useState(false) @@ -116,38 +130,81 @@ export function MazePlayground({ solver = false }: { solver?: boolean }) { setRunning(false) }, []) - // "Right-hand rule": at each step try to turn right, else go straight, else - // turn left, else turn around — taking the first direction that's open. In a - // perfect maze (no loops) this always reaches the goal. - const runWallFollower = useCallback(() => { - stopSolver() - let x = START[0] - let y = START[1] - let dir = 1 // start heading right - let steps = 0 - const maxSteps = SIZE * SIZE * 4 - setRobot([x, y]) - setRunning(true) - timer.current = setInterval(() => { - if ((x === GOAL[0] && y === GOAL[1]) || steps++ > maxSteps) { - stopSolver() - return - } - for (const nd of [(dir + 1) % 4, dir, (dir + 3) % 4, (dir + 2) % 4]) { - const { bit, dx, dy } = DIR4[nd] - if (grid[y][x] & bit) { - dir = nd + // Animate one of the three solvers, one step every 180ms: + // random — step to a uniformly-random open neighbour. No rule, so no two + // runs match; it only reaches the goal by luck. + // naive — take the first open direction in a fixed priority order, with no + // memory of where it came from. Deterministic, so the moment it + // re-enters a cell it will forever repeat the same choices — that + // first revisit is a provable infinite loop, and we stop there. + // wall — the right-hand rule: relative to a *remembered* heading, try + // right, straight, left, back. That one remembered fact (facing) + // is the whole difference from `naive`; in a perfect maze it + // always reaches the goal. + const runSolver = useCallback( + (mode: SolverMode) => { + stopSolver() + let x = START[0] + let y = START[1] + let dir = 1 // wall follower's remembered heading (start facing right) + let steps = 0 + const maxSteps = SIZE * SIZE * 4 + const visits = new Map([[`${x},${y}`, 1]]) + setRobot([x, y]) + setRunning(true) + setStuck(false) + timer.current = setInterval(() => { + if ((x === GOAL[0] && y === GOAL[1]) || steps++ > maxSteps) { + stopSolver() + return + } + if (mode === 'wall') { + for (const nd of [(dir + 1) % 4, dir, (dir + 3) % 4, (dir + 2) % 4]) { + const { bit, dx, dy } = DIR4[nd] + if (grid[y][x] & bit) { + dir = nd + x += dx + y += dy + break + } + } + } else if (mode === 'naive') { + for (const nd of NAIVE_ORDER) { + const { bit, dx, dy } = DIR4[nd] + if (grid[y][x] & bit) { + x += dx + y += dy + break + } + } + } else { + const opens = [0, 1, 2, 3].filter((nd) => grid[y][x] & DIR4[nd].bit) + const { dx, dy } = DIR4[opens[Math.floor(Math.random() * opens.length)]] x += dx y += dy - break } - } - setRobot([x, y]) - }, 180) - }, [grid, stopSolver]) + setRobot([x, y]) + // A memory-less rule that revisits a cell is doomed to loop forever + // (same cell -> same choice). We don't stop on the *first* revisit + // though — we let the robot visibly bounce a few times so the loop is + // something you can watch, then call it once a cell has been hit 4×. + if (mode === 'naive' && !(x === GOAL[0] && y === GOAL[1])) { + const key = `${x},${y}` + const n = (visits.get(key) ?? 0) + 1 + visits.set(key, n) + if (n >= 4) { + setStuck(true) + stopSolver() + } + } + }, 180) + }, + [grid, stopSolver], + ) const generate = () => { stopSolver() + setStuck(false) setGrid(generateMaze(SIZE)) setRobot(START) } @@ -155,6 +212,7 @@ export function MazePlayground({ solver = false }: { solver?: boolean }) { const move = useCallback( (dir: keyof typeof MOVES) => { stopSolver() + setStuck(false) setRobot(([x, y]) => { const { bit, dx, dy } = MOVES[dir] // Can only move through a carved opening (no wall on that side). @@ -276,6 +334,9 @@ export function MazePlayground({ solver = false }: { solver?: boolean }) { goal   robot {solved && — solved! 🎉} + {stuck && !solved && ( + — stuck in a loop 🔁 + )}

{/* Directional pad. */} @@ -292,10 +353,10 @@ export function MazePlayground({ solver = false }: { solver?: boolean }) { {solver && ( )}
- {solver && ( + {solver && !isJava && ( )} - + {!isJava && ( + + )}
+ + {isJava && ( +
+
+ +
+ + +
+ {javaOutput !== null && ( +
+                {javaOutput}
+              
+ )} +
+
+ )}
) } diff --git a/site/src/lib/javaRuntime.ts b/site/src/lib/javaRuntime.ts index b793410..c3c8d6f 100644 --- a/site/src/lib/javaRuntime.ts +++ b/site/src/lib/javaRuntime.ts @@ -17,7 +17,14 @@ const TOOLS_JAR = `/app${import.meta.env.BASE_URL}tools.jar` // so students write solving algorithms against it instead of raw bitmasks. The // jar is produced by CI on deploy and by scripts/vendor-maze-solver.sh locally. const SOLVER_JAR = `/app${import.meta.env.BASE_URL}maze-solver.jar` -const CLASS_PATH = `${TOOLS_JAR}:${SOLVER_JAR}:/files/` +// Temporary shim: the maze-solver library ships only interfaces, so snippets +// that call `new GridMaze(grid)` need a concrete implementation on the +// classpath. maze-engine.jar (built by scripts/build-maze-engine.sh, committed +// like tools.jar) supplies GridMaze/GridRobot/GridCell. It comes after +// SOLVER_JAR so the library's own GridMaze wins once it ships one — at which +// point this jar and its classpath entry can be deleted. +const ENGINE_JAR = `/app${import.meta.env.BASE_URL}maze-engine.jar` +const CLASS_PATH = `${TOOLS_JAR}:${SOLVER_JAR}:${ENGINE_JAR}:/files/` // Globals installed by loader.js (a classic script, not an ES module). declare global { diff --git a/site/src/lib/lessons.ts b/site/src/lib/lessons.ts index b6fcf3f..c3d1c06 100644 --- a/site/src/lib/lessons.ts +++ b/site/src/lib/lessons.ts @@ -158,16 +158,18 @@ export function hasMaze(markdown: string): boolean { return MAZE_FENCE.test(markdown) } -// The maze fence can carry a `solver:` directive to reveal an animated -// solver button. Three modes drive the "what is an algorithm?" progression: +// The maze fence can carry a `solver:` directive to reveal a solver control. +// The three canned modes drive the "what is an algorithm?" progression: // random — move to a random open neighbour (not an algorithm: unrepeatable) // naive — fixed direction priority, no memory (an algorithm, but it loops) // wall — the wall follower (an algorithm that works, if not optimally) -export type SolverMode = 'random' | 'naive' | 'wall' +// The fourth, `java`, swaps the canned button for an editor where the student +// writes their own solver in real Java, run through the maze round-trip. +export type SolverMode = 'random' | 'naive' | 'wall' | 'java' export function mazeSolver(markdown: string): SolverMode | null { const match = MAZE_FENCE.exec(markdown) if (!match) return null - const m = /solver:\s*(random|naive|wall)/.exec(match[1]) + const m = /solver:\s*(random|naive|wall|java)/.exec(match[1]) return m ? (m[1] as SolverMode) : null } diff --git a/site/src/lib/mazeHarness.ts b/site/src/lib/mazeHarness.ts new file mode 100644 index 0000000..b9daa82 --- /dev/null +++ b/site/src/lib/mazeHarness.ts @@ -0,0 +1,94 @@ +// The JS → Java → animated-steps maze round-trip (see docs/maze-roundtrip.md). +// +// A student writes a `solve(Robot)` algorithm in the maze playground. To run it +// we build a full Java compilation unit here: the current maze is interpolated +// in as an `int[][]` literal, the student's method is spliced into the class +// body, and a `main` drives the robot then prints the Maze Trail as a single +// sentinel-tagged line for the JS side to parse and animate back. +// +// `new GridMaze(grid)` resolves against maze-engine.jar — a concrete engine the +// site vendors on the classpath while the maze-solver library still ships only +// interfaces (see lib/javaRuntime.ts). Nothing maze-specific is bundled into +// the harness source itself. + +// The maze as a ready-to-embed Java `int[][]` literal body (just the rows, no +// surrounding `int[][] x = {...}`). Each cell is the same N/S/E/W bitmask the JS +// grid uses — a set bit means that side is OPEN — so Java reads it with the +// identical constants. This is the one source of truth for the literal format; +// both the harness and "Copy for Java" build on it. +export function mazeGridLiteral(grid: number[][]): string { + return grid.map((row) => ' {' + row.join(', ') + '}').join(',\n') +} + +// Everything the harness puts *above* the student's spliced-in code. Kept as its +// own string so we can measure its line count and translate compile-error line +// numbers back to what the student sees in their editor. +function harnessPrefix(grid: number[][]): string { + return `import com.frc2713.mazesolver.*; + +public class MazeRun { + static final int[][] GRID = { +${mazeGridLiteral(grid)} + }; + + public static void main(String[] args) { + Maze maze = new GridMaze(GRID); + Robot robot = maze.robot(); + + new MazeRun().solve(robot); + + // Emit the Maze Trail as one sentinel-tagged JSON line of [row,col] pairs. + int[][] trail = robot.trail(); + StringBuilder sb = new StringBuilder("__TRAIL__ ["); + for (int i = 0; i < trail.length; i++) { + if (i > 0) sb.append(','); + sb.append('[').append(trail[i][0]).append(',').append(trail[i][1]).append(']'); + } + System.out.println(sb.append(']')); + } + + // ===== your algorithm ===== +` +} + +// The harness closes the class after the student's spliced-in code. +const HARNESS_SUFFIX = '\n}\n' + +// How many lines the harness inserts above the student's first line. Compile +// errors come back as "line N: ..." counted in the assembled file; subtracting +// this maps them to the line the student actually typed. +export function studentLineOffset(grid: number[][]): number { + return harnessPrefix(grid).split('\n').length - 1 +} + +// Assemble the full compilation unit: harness prefix + student code + close. +export function buildMazeHarness(grid: number[][], studentCode: string): string { + return harnessPrefix(grid) + studentCode.trimEnd() + HARNESS_SUFFIX +} + +// Rewrite the "line N:" prefixes in a compile-error message so they point at the +// student's editor lines instead of the assembled harness. Runtime errors have +// no line numbers under CheerpJ, so they pass through untouched. +export function retargetErrorLines(output: string, offset: number): string { + return output.replace(/^line (\d+):/gm, (_m, n) => `line ${Math.max(1, Number(n) - offset)}:`) +} + +export interface TrailResult { + // Cells in [x, y] = [col, row] order (the swap from Java-native [row, col] + // happens here and nowhere else), including the Start cell first. + cells: [number, number][] +} + +// Find the __TRAIL__ sentinel line in captured stdout and parse it. Returns null +// if no sentinel line is present (treat as "algorithm produced no trail"). +export function parseTrail(output: string): TrailResult | null { + const line = output.split('\n').find((l) => l.startsWith('__TRAIL__ ')) + if (!line) return null + try { + const rowCol = JSON.parse(line.slice('__TRAIL__ '.length)) as [number, number][] + // The one conversion point: Java is [row, col]; the robot state is [x, y]. + return { cells: rowCol.map(([row, col]) => [col, row]) } + } catch { + return null + } +} From 9c22d148f574a65e3801fbc2dc2626ded6a5a1e4 Mon Sep 17 00:00:00 2001 From: Ty Tremblay Date: Sat, 1 Aug 2026 15:21:34 -0400 Subject: [PATCH 5/5] Restructure the maze module and drop the engine shim Address the PR review by splitting the dense single maze lesson into a sequence and moving to the maze-solver library's real interactive API. - Delete the maze-engine shim (jar, build script, sources, ENGINE_JAR classpath entry); GridMaze/Robot/Cell/Direction now come from maze-solver.jar itself. - Retarget the harness, the solver: java starter, and "Copy for Java" to the new API: readWallSensor(Direction)/drive(Direction)/facing(), with Direction turns for the wall follower. - Lessons: trim "Navigating a maze" (195) to the visual concept, and add algorithms-maze-data (196, 2D arrays), algorithms-robot-api (197, reading the API + writing tryMove), and algorithms-maze-solver (198, the wall follower + the round-trip playground). Java snippets use a small 5x5 maze. - Update docs/maze-roundtrip.md and CLAUDE.md. Requires FRC2713/maze-solver-java branch maze-interactive-api to be merged so CI builds a matching jar. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 24 ++- docs/maze-roundtrip.md | 41 ++--- lessons/algorithms-maze-data/README.md | 114 ++++++++++++++ lessons/algorithms-maze-solver/README.md | 119 ++++++++++++++ lessons/algorithms-robot-api/README.md | 147 ++++++++++++++++++ lessons/algorithms/README.md | 138 +--------------- site/public/maze-engine.jar | Bin 3355 -> 0 bytes site/scripts/build-maze-engine.sh | 32 ---- .../com/frc2713/mazesolver/GridCell.java | 53 ------- .../com/frc2713/mazesolver/GridMaze.java | 62 -------- .../com/frc2713/mazesolver/GridRobot.java | 83 ---------- site/src/components/MazePlayground.tsx | 61 ++++---- site/src/lib/javaRuntime.ts | 17 +- site/src/lib/mazeHarness.ts | 5 +- 14 files changed, 460 insertions(+), 436 deletions(-) create mode 100644 lessons/algorithms-maze-data/README.md create mode 100644 lessons/algorithms-maze-solver/README.md create mode 100644 lessons/algorithms-robot-api/README.md delete mode 100644 site/public/maze-engine.jar delete mode 100755 site/scripts/build-maze-engine.sh delete mode 100644 site/scripts/maze-engine/com/frc2713/mazesolver/GridCell.java delete mode 100644 site/scripts/maze-engine/com/frc2713/mazesolver/GridMaze.java delete mode 100644 site/scripts/maze-engine/com/frc2713/mazesolver/GridRobot.java diff --git a/CLAUDE.md b/CLAUDE.md index 10547f9..1f299f8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -173,9 +173,21 @@ folder until it's restarted. `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`. + `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). diff --git a/docs/maze-roundtrip.md b/docs/maze-roundtrip.md index 5856fc1..d64e03b 100644 --- a/docs/maze-roundtrip.md +++ b/docs/maze-roundtrip.md @@ -8,13 +8,12 @@ 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`. +**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. @@ -54,7 +53,7 @@ sequenceDiagram 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 + 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] @@ -94,11 +93,11 @@ 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). +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) @@ -199,8 +198,9 @@ public class MazeRun { - `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`). +- 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):** @@ -219,8 +219,9 @@ Resolved against the vendored interface jar's bytecode while wiring this up: 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*`). + `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()`. diff --git a/lessons/algorithms-maze-data/README.md b/lessons/algorithms-maze-data/README.md new file mode 100644 index 0000000..b25d447 --- /dev/null +++ b/lessons/algorithms-maze-data/README.md @@ -0,0 +1,114 @@ +--- +title: "The maze as data" +goal: "See a maze the way a program does — a grid of numbers — and learn to read a 2D array (an array of arrays) with maze[row][col], the shape almost every robot's-eye view of the field takes." +order: 196 +section: "Algorithms" +--- + +# A maze is just a grid of numbers + +You've driven the maze by hand and watched three robots cross it. Now for the +move that lets a *computer* do it: seeing the maze as **data** — plain numbers a +program can hold in a variable. + +Back in the [Arrays](/lesson/12-arrays) lesson, an array was a row of boxes: a +single line of values, one after another. A maze isn't a single row, though — +it's a **grid**, with rows *and* columns. So we reach for the natural next step: +an array **whose elements are themselves arrays**. One array holds the rows; +each row is an array of cells. That's a **2D array**, and in Java its type is +`int[][]` — "an array of `int` arrays." + +Here's a small 5×5 maze written exactly that way: + +```java +int[][] maze = { + {4, 12, 12, 12, 10}, + {6, 12, 10, 6, 9}, + {5, 10, 3, 3, 2}, + {6, 9, 3, 5, 11}, + {5, 8, 5, 12, 9} +}; + +System.out.println("The maze has " + maze.length + " rows."); +System.out.println("Row 0 has " + maze[0].length + " columns."); +``` + +Read it top to bottom and it lays out as five rows of five cells — the same shape +as the mazes you've been crossing. The outer `{ }` holds the whole maze; each +inner `{ }` is one row. `maze.length` is the number of rows; `maze[0].length` is +how many columns are in the first row. Press **Run** and check those counts. + +# Reaching one cell: maze[row][col] + +A single index reached into a plain array — `scores[2]` was the third score. A +2D array takes **two** indices, in a fixed order that's worth burning into +memory: **row first, then column.** + +```java +int[][] maze = { + {4, 12, 12, 12, 10}, + {6, 12, 10, 6, 9}, + {5, 10, 3, 3, 2}, + {6, 9, 3, 5, 11}, + {5, 8, 5, 12, 9} +}; + +// maze[row][col] — row first, then column. Both start counting at 0. +System.out.println("Top-left cell: " + maze[0][0]); +System.out.println("Top-right cell: " + maze[0][4]); +System.out.println("Bottom-right cell: " + maze[4][4]); +System.out.println("Middle cell: " + maze[2][2]); +``` + +`maze[0][0]` is the **start** (top-left — the green cell in the maze pictures). +`maze[4][4]` is the **goal** (bottom-right, the red cell) — the last row, the +last column. +Row `0` is the top and column `0` is the left, so as the row index grows you move +*down* and as the column index grows you move *right*. Getting `[row][col]` the +right way round is the single most common place people trip: `maze[4][0]` is the +bottom-left cell, but `maze[0][4]` is the top-right — swap them and you're in a +completely different corner. + +# Walking the whole grid + +To *look at every cell*, you nest one loop inside another: the outer loop walks +the rows, the inner loop walks the columns of that row. This double loop is the +bread-and-butter way to touch every square of a grid, and you'll write it +constantly. + +```java +int[][] maze = { + {4, 12, 12, 12, 10}, + {6, 12, 10, 6, 9}, + {5, 10, 3, 3, 2}, + {6, 9, 3, 5, 11}, + {5, 8, 5, 12, 9} +}; + +for (int row = 0; row < maze.length; row++) { + for (int col = 0; col < maze[row].length; col++) { + System.out.print(maze[row][col] + "\t"); + } + System.out.println(); // newline at the end of each row +} +``` + +Run it and the output is the maze laid back out as a grid — because the loops +visit the cells in exactly the order they're stored: all of row 0 left to right, +then all of row 1, and so on. + +## What do the numbers *mean*? + +Fair question — why is the start cell a `4` and the goal a `9`? Each number packs +in **which sides of that cell are open** (carved through) versus walled. The +start cell is `4`, which happens to mean "only the right side is open" — which +fits: the green start cell has just one way out, to the right. + +Here's the good news, and it's the whole point of the next lesson: **you will +never do that decoding by hand.** Squeezing four walls into one number is a +clever trick, but it's a distraction from actually *solving* the maze. So instead +of asking you to do bit-by-bit arithmetic on these values, we'll hand the grid to +a small **library** that reads it for you and gives you a friendly robot to +drive. The maze stays this same `int[][]` — that's how it travels between the +website and your Java code — but from here on you get to think in *"is there a +wall to my right?"*, not in numbers. diff --git a/lessons/algorithms-maze-solver/README.md b/lessons/algorithms-maze-solver/README.md new file mode 100644 index 0000000..d7c09fb --- /dev/null +++ b/lessons/algorithms-maze-solver/README.md @@ -0,0 +1,119 @@ +--- +title: "Writing a maze solver" +goal: "Turn the wall follower from an idea into working Java: use the robot's facing() and the Direction turns to try right, straight, left, then back, and watch your own algorithm drive a robot out of a freshly generated maze." +order: 198 +section: "Algorithms" +--- + +# From a helper to an algorithm + +You have a robot you understand and a `tryMove` helper that moves it one cell +*if it can*. Time to put them in a loop and cross the maze — the **wall +follower** from the visual lesson, now as real code. + +The rule, in words, was: *relative to the way I'm facing, take the first opening +in this order — right, straight, left, then (only as a last resort) back.* The +one fact it leans on is the robot's **heading**, and that's exactly the fact the +robot already remembers for you: `robot.facing()`. Because `drive` updates +`facing()` every time the robot actually moves, you never have to track the +heading yourself. + +To turn "right, straight, left, back" into real `Direction`s, ask the heading to +rotate. `Direction` offers three turns: + +```text +Direction.RIGHT.clockwise() → Direction.DOWN (a right turn) +Direction.RIGHT.counterClockwise() → Direction.UP (a left turn) +Direction.RIGHT.opposite() → Direction.LEFT (turn around) +``` + +So if the robot is `facing()` right, its own right-hand side is `clockwise()` of +that (down), straight ahead is the heading itself, its left is +`counterClockwise()`, and back is `opposite()`. Build that list of four and +`tryMove` the first one that's open: + +```java +import com.frc2713.mazesolver.*; + +public class WallFollower { + public static void main(String[] args) { + int[][] grid = { + {4, 12, 12, 12, 10}, + {6, 12, 10, 6, 9}, + {5, 10, 3, 3, 2}, + {6, 9, 3, 5, 11}, + {5, 8, 5, 12, 9} + }; + Maze maze = new GridMaze(grid); + Robot robot = maze.robot(); + + int maxSteps = 1000; // a safety stop, so a bad rule can't loop forever + for (int i = 0; i < maxSteps && !robot.atGoal(); i++) { + Direction ahead = robot.facing(); + // Right, straight, left, back — relative to the way we're facing. + Direction[] order = { + ahead.clockwise(), ahead, ahead.counterClockwise(), ahead.opposite() + }; + for (Direction dir : order) { + if (tryMove(robot, dir)) break; // took the first opening + } + } + + if (robot.atGoal()) { + System.out.println("Reached the goal in " + (robot.trail().length - 1) + " steps!"); + } else { + System.out.println("Gave up after " + maxSteps + " steps."); + } + } + + // Drive one step in dir if that side is open; report whether we moved. + static boolean tryMove(Robot robot, Direction dir) { + if (!robot.readWallSensor(dir)) { + robot.drive(dir); + return true; + } + return false; + } +} +``` + +Press **Run**: the robot hugs the right-hand wall all the way from the green cell +to the red one and prints how many steps it took. Everything the visual lesson +promised is here in a dozen lines — and the only state the algorithm keeps is the +heading, which the robot hands you for free. + +# Now you try: your solver in a real maze + +You just read a whole program you could run but not *change*. Now it's your turn, +and this time the maze is real — freshly generated, and different every time. + +Notice how much of that program was the same boilerplate every run: `main`, +building the `Maze`, the safety cap, printing the result. The only part that's +truly *the algorithm* is the decision inside the loop — so that's the only part +the playground asks you for. It hands you a `Robot` standing at the start and +asks for one method, `solve`: + +The maze on the right is handed to your Java `solve` method, your algorithm drives +the `Robot`, and the exact path it took is animated right back here — the full +round-trip. You have the same API you've been using all along: + +- `robot.readWallSensor(Direction.UP/DOWN/LEFT/RIGHT)` — is that side a wall? +- `robot.drive(Direction.UP/DOWN/LEFT/RIGHT)` — drive one cell (a wall stops you). +- `robot.facing()` — the direction it last drove; `Direction` also has + `clockwise()`, `counterClockwise()`, and `opposite()`. +- `robot.atGoal()`, `robot.row()`, `robot.col()` — where am I, am I done? + +The starter is the **wall follower** you just studied. Run it first and watch it +solve. Then make it yours: + +- Hit **Generate new maze** and run again — a good algorithm clears *any* maze, + not just one. +- Break it on purpose: swap `clockwise()` and `counterClockwise()` (a *left*-hand + follower — it still works!), or delete the wall check and drive blindly into + walls, and watch where your robot gets stuck. +- Throw out the wall follower entirely and write your own rule. Anything that + reaches red counts. + +```maze +solver: java +``` diff --git a/lessons/algorithms-robot-api/README.md b/lessons/algorithms-robot-api/README.md new file mode 100644 index 0000000..a140e33 --- /dev/null +++ b/lessons/algorithms-robot-api/README.md @@ -0,0 +1,147 @@ +--- +title: "Reading the robot's API" +goal: "Meet the maze-solver library — the Maze, Robot, and Direction it hands you — and practice the real-world skill of reading an unfamiliar API by writing a small helper method, tryMove, out of the robot's sense and drive abilities." +order: 197 +section: "Algorithms" +--- + +# Meet the robot + +Last lesson you saw the maze as an `int[][]` — and promised yourself you'd never +decode those numbers by hand. This is where that promise pays off. We hand the +grid to a small **library** (a bundle of ready-made code someone else wrote for +you), and it gives back two friendly objects: + +```java +Maze maze = new GridMaze(grid); // turn the raw grid into a maze +Robot robot = maze.robot(); // a robot standing on the start cell +``` + +That's it — no bitmasks. From here you think about a **robot**, and a robot can +do exactly what the visual lessons described: **sense** what's around it and +**move**. Learning precisely what it can do means reading its **API** — the list +of methods it offers. Reading an API you didn't write is one of the most useful +skills in all of programming (it's most of what working with real robot code +*is*), so here's the robot's, in full: + +```text +robot.readWallSensor(Direction.UP) is that side a wall? → true / false +robot.drive(Direction.RIGHT) drive one cell that way (does nothing if walled) +robot.facing() the Direction it last drove +robot.atGoal() standing on the goal (red) cell? +robot.row(), robot.col() where it is now +``` + +`Direction` is the four ways it can face or move: `Direction.UP`, +`Direction.DOWN`, `Direction.LEFT`, `Direction.RIGHT`. Two things are worth +underlining before you use them: + +- `readWallSensor(dir)` reports a **wall**: it's `true` when that side is + *blocked*. So an *opening* is when it comes back `false`. +- `drive(dir)` is honest about the walls: if you aim it at a wall, the robot just + stays put. And when it *does* move, it remembers that direction — so right + after `robot.drive(Direction.UP)`, `robot.facing()` is `Direction.UP`. A robot + that just drove up is now facing up. + +Here's the robot actually doing these things. Read the code, predict the output, +then press **Run**: + +```java +import com.frc2713.mazesolver.*; + +public class MeetTheRobot { + public static void main(String[] args) { + int[][] grid = { + {4, 12, 12, 12, 10}, + {6, 12, 10, 6, 9}, + {5, 10, 3, 3, 2}, + {6, 9, 3, 5, 11}, + {5, 8, 5, 12, 9} + }; + Maze maze = new GridMaze(grid); + Robot robot = maze.robot(); + + // What's around the robot at the start? + System.out.println("Wall above me? " + robot.readWallSensor(Direction.UP)); + System.out.println("Wall to right? " + robot.readWallSensor(Direction.RIGHT)); + + // Drive one cell to the right, then look again. + robot.drive(Direction.RIGHT); + System.out.println("Drove right. Now at row " + robot.row() + ", col " + robot.col()); + System.out.println("Facing: " + robot.facing()); + } +} +``` + +At the start the robot is boxed in on every side but the right — that lone +opening is why the start cell's number was `4`. Try changing that first `drive` +to `Direction.UP` (into a wall) and re-run: the robot doesn't move, because the +drivetrain can't push through a wall. + +# Build a helper: tryMove + +Look at the two abilities together and a small annoyance appears. To *safely* +move a direction you always do the same two-step dance: **check the sensor, and +only drive if it's open.** Drive without checking and you might just grind into a +wall. + +That pattern — "move that way *if you can*, and tell me whether you did" — is +worth bottling into one method of your own. Call it `tryMove`: + +```text +tryMove(robot, dir): + if that side is NOT a wall: + drive that way + return true (yes, I moved) + otherwise: + return false (no, blocked) +``` + +It's only a few lines, but writing it is the exercise: you're turning the raw API +into a tool shaped like the way you actually think about the problem. Here it is +in code. **Read the body of `tryMove` and make sure you can explain each line** — +then, to really learn it, delete the body and rewrite it from the pseudocode +above without looking. + +```java +import com.frc2713.mazesolver.*; + +public class TryMove { + public static void main(String[] args) { + int[][] grid = { + {4, 12, 12, 12, 10}, + {6, 12, 10, 6, 9}, + {5, 10, 3, 3, 2}, + {6, 9, 3, 5, 11}, + {5, 8, 5, 12, 9} + }; + Maze maze = new GridMaze(grid); + Robot robot = maze.robot(); + + // Try each direction once and report what happened. + Direction[] toTry = { Direction.UP, Direction.RIGHT, Direction.DOWN, Direction.LEFT }; + for (Direction dir : toTry) { + boolean moved = tryMove(robot, dir); + System.out.println("tryMove " + dir + " -> " + moved + + " (now at row " + robot.row() + ", col " + robot.col() + ")"); + } + } + + // Drive one step in dir if that side is open; report whether we moved. + static boolean tryMove(Robot robot, Direction dir) { + if (!robot.readWallSensor(dir)) { // not a wall → there's an opening + robot.drive(dir); + return true; + } + return false; + } +} +``` + +Run it and read the trace: from the start only `RIGHT` comes back `true`, so +that's the only line where the robot's column changes. Every other direction is a +wall, so `tryMove` reports `false` and the robot holds its position — no crashing +into walls, exactly as designed. + +You now have a robot you understand and one helper of your own. That's everything +you need to write a *real* maze-solving algorithm — which is the next lesson. diff --git a/lessons/algorithms/README.md b/lessons/algorithms/README.md index 4458795..94b056d 100644 --- a/lessons/algorithms/README.md +++ b/lessons/algorithms/README.md @@ -202,135 +202,9 @@ Same skeleton — **sense, decide, move, repeat** — every time, differing only "decide." That skeleton is the shape of an enormous amount of robot code: read your sensors, decide what to do, act, and loop. -# The same algorithm, in Java - -Everything so far has been visual. But the whole point is that a maze is just -**data**, and an algorithm is just **code** — and once it's data and code, a -robot can run it. Here's that exact wall follower written in real Java. - -The maze starts as an `int[][]` — a grid of numbers, each the same **N/S/E/W -bitmask** you've been looking at (a set bit means that side of the cell is -open). But you don't want to be doing bitmask arithmetic while you're trying to -think about *solving* the maze. That's what a **library** is for. Our team's -`maze-solver` library (`com.frc2713.mazesolver`) turns that raw grid into three -friendly tools: - -- a **`Maze`** you build from the grid — `new GridMaze(grid)`, -- a **`Robot`** that walks it — `robot.canMoveRight()`, `robot.moveRight()`, - `robot.atGoal()`, -- and **`Cell`**s you can ask plain questions like `cell.wallRight()`. - -The library handles the *maze* — no bitmasks, no `maze[row][col]` arithmetic, no -tracking coordinates as the robot steps. What's left is just the algorithm, and -the algorithm keeps exactly **one** piece of its own state: `facing`, the -direction the robot is currently pointing. That's the single remembered fact -from the last page — the whole reason the wall follower beats the naive rule. -The maze is the library's job; the facing is the part that's genuinely *yours*. - -The loop below is the flowchart from earlier turned into code: relative to the -way it's facing, the robot tries right, else straight, else left, else back — -takes the first open direction, remembers that as its new facing, and repeats. -Press **Run** and watch it count its way to the exit. - -```java -import com.frc2713.mazesolver.*; - -public class SolveMaze { - public static void main(String[] args) { - // A maze serialized from the generator: each cell is an N/S/E/W bitmask. - int[][] grid = { - {4, 12, 10, 6, 10, 4, 14, 10, 4, 12, 14, 12, 12, 10}, - {6, 10, 5, 9, 5, 12, 9, 5, 12, 12, 9, 6, 12, 9}, - {3, 5, 12, 14, 12, 12, 12, 10, 6, 12, 10, 3, 6, 8}, - {3, 6, 10, 3, 6, 8, 6, 9, 5, 10, 3, 3, 5, 10}, - {5, 9, 3, 3, 7, 12, 13, 12, 10, 1, 3, 5, 10, 3}, - {6, 8, 3, 3, 5, 8, 6, 10, 5, 10, 3, 6, 9, 3}, - {3, 6, 9, 5, 10, 6, 9, 3, 4, 13, 9, 5, 12, 11}, - {3, 5, 12, 10, 5, 9, 2, 5, 12, 12, 12, 14, 10, 3}, - {7, 14, 8, 5, 10, 4, 13, 12, 12, 14, 10, 3, 1, 3}, - {3, 3, 6, 10, 5, 10, 6, 12, 12, 9, 3, 5, 12, 9}, - {1, 3, 3, 5, 12, 9, 3, 6, 12, 10, 3, 2, 6, 10}, - {6, 11, 3, 4, 12, 14, 9, 3, 2, 5, 9, 7, 9, 3}, - {3, 1, 3, 6, 10, 3, 6, 9, 5, 12, 12, 13, 8, 3}, - {5, 12, 13, 9, 5, 9, 5, 12, 12, 12, 12, 12, 12, 9} - }; - - // The library turns the raw grid into a maze and a robot at the start. - Maze maze = new GridMaze(grid); - Robot robot = maze.robot(); - - // The robot's one remembered fact: which way it's facing. - // 0 = up, 1 = right, 2 = down, 3 = left. Start facing right. - int facing = 1; - int cap = maze.rows() * maze.cols() * 4; // safety stop - - while (!robot.atGoal() && robot.trail().length <= cap) { - // Try right, straight, left, back — relative to the way we're facing - // — and take the first one that's open, remembering it as the new facing. - int[] order = { (facing + 1) % 4, facing, (facing + 3) % 4, (facing + 2) % 4 }; - for (int dir : order) { - if (tryMove(robot, dir)) { - facing = dir; - break; - } - } - } - - if (robot.atGoal()) { - System.out.println("Reached the goal in " + (robot.trail().length - 1) + " steps!"); - } else { - System.out.println("Gave up."); - } - } - - // Move one step in an absolute direction if that side is open; report - // whether we actually moved. (0 = up, 1 = right, 2 = down, 3 = left.) - static boolean tryMove(Robot robot, int dir) { - if (dir == 0 && robot.canMoveUp()) { robot.moveUp(); return true; } - if (dir == 1 && robot.canMoveRight()) { robot.moveRight(); return true; } - if (dir == 2 && robot.canMoveDown()) { robot.moveDown(); return true; } - if (dir == 3 && robot.canMoveLeft()) { robot.moveLeft(); return true; } - return false; - } -} -``` - -# Now you try: write your own solver - -You just read the whole program — building the maze, getting the robot, the -loop, checking the goal. You could run it, but not *change* it. Now it's your -turn. - -Notice how much of that program was the same every time: `main`, building the -`Maze`, the safety cap, printing the result. The only part that was really *the -algorithm* was the decision inside the loop. So that's the only part we'll ask -you for. The playground below hands you a `Robot` already standing at the start -and asks for one method — `solve` — the interesting part. - -The maze on the right is a real one, freshly generated. Write an algorithm that -drives the robot from the **green** cell to the **red** one, press **Run in the -maze**, and watch *your* robot walk the path your code produced. This is the full -round-trip: the maze you see is handed to your Java `solve` method, your -algorithm drives the `Robot`, and the exact path it took is animated right back -here. You have: - -- `robot.canMoveUp()`, `robot.canMoveDown()`, `robot.canMoveLeft()`, - `robot.canMoveRight()` — is that side open? -- `robot.moveUp()`, `robot.moveDown()`, `robot.moveLeft()`, `robot.moveRight()` - — step one cell (a move into a wall does nothing). -- `robot.atGoal()`, `robot.row()`, `robot.col()` — where am I, am I done? - -The starter code is the **wall follower** from the last two pages. Run it first -to see it solve the maze. Then make it yours: - -- Hit **Generate new maze** and run again — a good algorithm clears *any* maze, - not just one. -- Break it on purpose: change the starting `facing` to `0`, or drive the robot - straight into a wall, and watch it get stuck exactly where your code went - wrong. -- Throw out the wall follower entirely and write your own rule. Anything that - reaches red counts. - -```maze -solver: java -``` +So far this has all been visual — you drove the robot, you watched the three +robots run. But the whole promise of an algorithm is that it's a plan precise +enough to *hand to a computer*. Over the next few lessons you'll do exactly that: +first see the maze as plain **data** a program can hold, then meet the **robot** +your Java code gets to drive, and finally write the wall follower yourself and +watch *your* code solve a maze. diff --git a/site/public/maze-engine.jar b/site/public/maze-engine.jar deleted file mode 100644 index 8749112e11a013a22f536f4b9e59d2fc59435818..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3355 zcmai%2{_dG8^8x6$FOcn?naIrGst~rjAM**RE(1QZWu?%HP%_KRE}lEI3q`nFwT}- zwTxp4O-d+Baz;k%*xhGo|EvG|Jiq7pKF{yHzTfZrzQ51U3_$~A1OVs(fEstQE#QbS z0Du6b9!f{r&;%~K*AD=gQB*kr^z`)qqsmD!`$rY2V`2!`vqZ@t;p@m3&y5k%GDFM= zX~}^XgS8frQTaD>SQ#Uvlp%5`psdaUA~zyWlt&R)dIyn6Nh9g;*}0iHYDz3Rdhf!< zyQ&n=%9Q6I_J5}dq`Y;+c*`EI!SuI=3)WFi2`c{`O`d;gdfSIM`(eBSow0u!{rBcY zk2liCx;yDQdwIz?dfEH=#aLW1aKqaEGIU%80 zD^H|AGE&V>Xra}og1-o^#PDzsyXYb!wfdE}YARyPj7OT}{odzf-E@iA|MCUY!Fjca zRR0N(vMn3s{-D?Q!`MX6!aO%A~7Cs=x&eXY2i&HqO zpK~8Akmp^S7n=10^9Jb#E|LPToJepzC7^J<)HZMByz78??}Z{0T*QkGrrhV}#N%`3 znk8Pr+Y_m1Z}@D@FN5&X0hncISAQ(@dSQG;Z~l)n(kEP-tFF;^GD(!Ee9B&a_5vTO z+pQbxkiyLqbElq#X@T(RJi2(?Mv0`WW>h2Rq29dVdxy-1M=Q=xSFLH|Z+ADVycIhM zf4*-ObHZB8j5LiOcRHQyDl4#znS5H-v|w>I+>5Y0i3=J$kyJA*SJ6tlF4SVOvS_{k z5=4q^MS_sn8xs>>U~NkZ3>+)x?ZmY$9&N|roCj5Mgh{Vwf%_1MF*{s)G1^!)SzC0U zEkablQhkyu#oTuJR*%FOVTN&SmqSEr@P^Dll(OYU%3B62O;Jp&ho~yKPVkz|9AAxM zP;%WxRsH4Do z!O8L6JwZ$B1-I62}nHP;0iLTcEl<(`o>HTgyZ#y6Bmc@)) zvDj>!3=(49L@2bqk}n9$cwaP|~n`bjq%R;P}Aix-Y3>8IvZF$49*D zu=Tm0j||geYMl-Ixt0*Qk`pGmqfxT(G9Xah3flWa@SBsL)BU>J?70%urpp`=U|Knr z(P^xt$(bm&L@Z_yh@I>{04;vbWe(!HD^3(aj}K2W>@`zL36UwK|89H+haF86PGNml_hML8nJhP`+P;{ zBrC7Q-(u#d0LuA0sitkH6jdjhw}4^XIP}grp&?Nytig{Xr?dse0ptI7furm# zIWBQ($Tr*GZ4?%cABz&<@?_Vy%;WZgaG66C*=Y5Iy?dN%fAL6jRs@-l^Y=ULh_d0O z^0Y2q0Xt1vWg|DIrKVCRI-KTPGsw#-pIZDrO-1Z|U18`L-#VB@yde_+I1f6pA#kHGvxnMOh%=OiU!omjko=>cgbVV zrWn2PN?EnjKRo3c4?ZK*pSU!B$05@}m6@4_`p30K{=w^<^S0yLO%4sMz+p3Ga$T}; z4x3p6GX`<3ls(OMwrkKor}O>2UPfTIWKU_?@U(~^6#q_LCY8J3s@R%W5qbXEs}O1) zXz^PRe+EPb5<*>tB8E$|{+j;0Nv@!M)R)#;Zm5noNu|ayu_1&Zrv%K-qnm5mA}KBF z*%-Jxl;~il({m}4H9i|&{`-=RX@lrf)lbL;)Y55i^+)TewhpV;nPbq-JWI_FmFvaN zRxtsbYVWufi+Wbksz!X{U$<*oo90%Ln8nOaeq1!^s(<2%+iQc?<35*!EY8(_1gUcS z+G(%zS;KH%Pq=5o42(Nz2xb-VWE2n{qCC@ZCG^>AuT!s+5mqWZV=A@v*oWefT#d(Q zZ29a|i=|jG>e+Q`mK#5vPA7N0bn|(J3?3R&z0R9k0pTN32lt4|oWMhD=dU>*~b{(rawfYoS$2 zW^>;1z*VeH-89&w3ZqLGUyHwpu1hAW-_?rjJ$I8BWH7%E3;77h(qNjeT7K)b$HD~P zH+m#dM@%0>{gj3vpA7$;TTPwf-U3MKrm{$b@qOJu+`%(Fqk|@m_%qfwM|f~ERzxbS zLK?b5$tS~jsyF+-)<)xVKbvnEmS0#83eOBkt{%7>;B3GKF@DJg{&3SLfzy1D&Sqfb zg{xi`9^G*==^=5{>BjHQycv(NyUeotuVea$3}h$uk^LUj5@m3cuba{en@XkqYSx5( zo$(bIH#Du~*C>}?R)=!86AHE+A|I@+x_6g2YA{^qW8XkTSu_GF#P;URsZ5_2aKbv))hmpd&V9^Ts!2Y^SnWxLl#KV7{Odct2mPAhc^& zbNOo$_J)>b@haWJxt{$$sZllZ)Wv2R06>R=v}On@YDU0+bK?Qtek=bF>|d};Ma>B~ zB)+3CdVsr!a*ne6$MgvNelPg9p-U(p{zBN}(>;LLL+PU|4=fIm_IS$!s6CW*%JQh? zH;DV~mXCtN0UjKRIMuOEzD3@HGdOyRM`!i7E|)1+c1&IeE)V(R-!6~d_rZ`qlz|i9 bb$Mjun;~c^x(9zj^Z;?nr-Vc)835o9wCXpo diff --git a/site/scripts/build-maze-engine.sh b/site/scripts/build-maze-engine.sh deleted file mode 100755 index 87f8ff5..0000000 --- a/site/scripts/build-maze-engine.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -# Build the concrete maze engine (GridMaze/GridRobot/GridCell) into -# public/maze-engine.jar. This is a TEMPORARY shim: the maze-solver library -# ships only the interfaces (Maze/Robot/Cell), so lesson snippets that call -# `new GridMaze(grid)` need a concrete implementation on the classpath to run in -# the browser JVM (CheerpJ). Delete this script, scripts/maze-engine/, the jar, -# and the ENGINE_JAR classpath entry once the library ships its own GridMaze. -# -# The jar is committed (like public/tools.jar), so you only need to run this -# after editing the engine sources. Output must be Java 8 bytecode (class -# version 52) because CheerpJ runs OpenJDK 8. Requires a JDK capable of -# --release 8 and the interface jar at public/maze-solver.jar -# (run scripts/vendor-maze-solver.sh first). -set -euo pipefail - -here="$(cd "$(dirname "$0")/.." && pwd)" -src="$here/scripts/maze-engine" -solver_jar="$here/public/maze-solver.jar" - -if [ ! -f "$solver_jar" ]; then - echo "Missing $solver_jar — run scripts/vendor-maze-solver.sh first." >&2 - exit 1 -fi - -tmp="$(mktemp -d)" -trap 'rm -rf "$tmp"' EXIT - -echo "Compiling maze engine (--release 8) ..." -javac --release 8 -cp "$solver_jar" -d "$tmp" \ - "$src"/com/frc2713/mazesolver/*.java -jar cf "$here/public/maze-engine.jar" -C "$tmp" . -echo "Built public/maze-engine.jar ($(wc -c < "$here/public/maze-engine.jar") bytes)" diff --git a/site/scripts/maze-engine/com/frc2713/mazesolver/GridCell.java b/site/scripts/maze-engine/com/frc2713/mazesolver/GridCell.java deleted file mode 100644 index a2514f9..0000000 --- a/site/scripts/maze-engine/com/frc2713/mazesolver/GridCell.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.frc2713.mazesolver; - -/** One square of a {@link GridMaze}. Wall flags come from the cell's bitmask. */ -class GridCell implements Cell { - // Bitmask of a cell's OPEN sides (a set bit = you can move that way). - static final int UP = 1, DOWN = 2, RIGHT = 4, LEFT = 8; - - 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); - } - - public int row() { - return row; - } - - public int col() { - return col; - } - - public boolean wallUp() { - return (mask() & UP) == 0; - } - - public boolean wallDown() { - return (mask() & DOWN) == 0; - } - - public boolean wallLeft() { - return (mask() & LEFT) == 0; - } - - public boolean wallRight() { - return (mask() & RIGHT) == 0; - } - - public boolean isStart() { - return row == 0 && col == 0; - } - - public boolean isGoal() { - return maze.isGoalCell(row, col); - } -} diff --git a/site/scripts/maze-engine/com/frc2713/mazesolver/GridMaze.java b/site/scripts/maze-engine/com/frc2713/mazesolver/GridMaze.java deleted file mode 100644 index b4f1cfa..0000000 --- a/site/scripts/maze-engine/com/frc2713/mazesolver/GridMaze.java +++ /dev/null @@ -1,62 +0,0 @@ -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 (0,0); Goal is the - * bottom-right cell (rows-1, cols-1) — the convention the whole maze module - * uses. - * - *

This is a temporary shim: the {@code maze-solver} library currently ships - * only the interfaces, so the site vendors this small implementation as - * {@code maze-engine.jar} to make {@code new GridMaze(grid)} runnable in the - * browser. Delete it once the library ships its own {@code GridMaze}. - */ -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]; - } - - int goalRow() { - return goalRow; - } - - int goalCol() { - return goalCol; - } - - public int rows() { - return rows; - } - - public int cols() { - return cols; - } - - public Cell cellAt(int row, int col) { - return new GridCell(this, row, col); - } - - public boolean isGoalCell(int row, int col) { - return row == goalRow && col == goalCol; - } - - public Robot robot() { - return bot; - } -} diff --git a/site/scripts/maze-engine/com/frc2713/mazesolver/GridRobot.java b/site/scripts/maze-engine/com/frc2713/mazesolver/GridRobot.java deleted file mode 100644 index 2cd7405..0000000 --- a/site/scripts/maze-engine/com/frc2713/mazesolver/GridRobot.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.frc2713.mazesolver; - -import java.util.ArrayList; - -/** - * A {@link Robot} that walks a {@link GridMaze} with absolute moves. It records - * a Maze Trail: every cell it has stood on, Start first. A move 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. - */ -class GridRobot implements Robot { - private final GridMaze maze; - private int row = 0; - private int col = 0; - private final ArrayList path = new ArrayList<>(); - - GridRobot(GridMaze maze) { - this.maze = maze; - path.add(new int[] { row, col }); - } - - private int mask() { - return maze.mask(row, col); - } - - private void step(int nextRow, int nextCol) { - row = nextRow; - col = nextCol; - path.add(new int[] { row, col }); - } - - public int row() { - return row; - } - - public int col() { - return col; - } - - public Cell cell() { - return maze.cellAt(row, col); - } - - public boolean canMoveUp() { - return (mask() & GridCell.UP) != 0; - } - - public boolean canMoveDown() { - return (mask() & GridCell.DOWN) != 0; - } - - public boolean canMoveLeft() { - return (mask() & GridCell.LEFT) != 0; - } - - public boolean canMoveRight() { - return (mask() & GridCell.RIGHT) != 0; - } - - public void moveUp() { - if (canMoveUp()) step(row - 1, col); - } - - public void moveDown() { - if (canMoveDown()) step(row + 1, col); - } - - public void moveLeft() { - if (canMoveLeft()) step(row, col - 1); - } - - public void moveRight() { - if (canMoveRight()) step(row, col + 1); - } - - public boolean atGoal() { - return maze.isGoalCell(row, col); - } - - public int[][] trail() { - return path.toArray(new int[0][]); - } -} diff --git a/site/src/components/MazePlayground.tsx b/site/src/components/MazePlayground.tsx index fc510f8..95fda12 100644 --- a/site/src/components/MazePlayground.tsx +++ b/site/src/components/MazePlayground.tsx @@ -78,24 +78,23 @@ const MOVES: Record<'up' | 'down' | 'left' | 'right', { bit: number; dx: number; const cx = (x: number) => PAD + x * CELL + CELL / 2 const cy = (y: number) => PAD + y * CELL + CELL / 2 -// Serialize the maze as a ready-to-paste Java `int[][]` literal. Each cell is -// the same N/S/E/W bitmask the grid already uses, so Java reads it with the -// identical constants (N=1, S=2, E=4, W=8) and `cell & N` wall checks. +// Serialize the maze as a ready-to-paste Java `int[][]` literal, wired into the +// maze-solver library so it runs without any bitmask arithmetic. Each cell is +// the same N/S/E/W bitmask the grid uses, but the library reads it for you. function serializeToJava(grid: number[][]): string { const rows = mazeGridLiteral(grid) - return `// Maze as an N/S/E/W bitmask grid. For each cell, a set bit means that -// side is OPEN (you can move that way); a clear bit is a wall. -// N = 1 (up) S = 2 (down) E = 4 (right) W = 8 (left) -// Example check: (maze[row][col] & E) != 0 --> can move right. -final int N = 1, S = 2, E = 4, W = 8; + return `import com.frc2713.mazesolver.*; -int[][] maze = { +// The maze as a grid of numbers (each cell an N/S/E/W bitmask). Hand it to a +// GridMaze and you get a robot on the start cell — no bitmask math needed. +int[][] grid = { ${rows} }; -// Where the robot starts and where it's trying to get to (row, col): -int startRow = ${START[1]}, startCol = ${START[0]}; -int goalRow = ${GOAL[1]}, goalCol = ${GOAL[0]};` +Maze maze = new GridMaze(grid); +Robot robot = maze.robot(); +// Now drive it: robot.readWallSensor(Direction.UP), robot.drive(Direction.RIGHT), +// robot.facing(), robot.atGoal(), robot.row(), robot.col().` } // Headings for the wall follower, clockwise: 0=up, 1=right, 2=down, 3=left. @@ -123,37 +122,31 @@ const SOLVER_LABEL: Record = { } // The editor's starting point for `solver: java`: a right-hand wall follower, -// the same algorithm lesson "Navigating a maze" builds up to — translated to -// the library's absolute-move API with a remembered `facing` heading. -const DEFAULT_JAVA_SOLVE = `// Drive the robot from start (green) to goal (red). -// You have: robot.canMoveUp/Down/Left/Right(), robot.moveUp/Down/Left/Right(), -// robot.atGoal(), robot.row(), robot.col(). +// the same algorithm the maze lessons build up to — written against the +// library's Robot API, using the facing() the robot remembers for itself. +const DEFAULT_JAVA_SOLVE = `// Drive the robot from start (green) to goal (red). Your robot can: +// robot.readWallSensor(Direction.UP/DOWN/LEFT/RIGHT) — is that side a wall? +// robot.drive(Direction.UP/DOWN/LEFT/RIGHT) — move one cell that way +// robot.facing() — the way it last drove +// robot.atGoal(), robot.row(), robot.col() // // This starter is the "keep your right hand on the wall" follower. void solve(Robot robot) { - // Heading: 0 = up, 1 = right, 2 = down, 3 = left. Start facing right. - int facing = 1; int maxSteps = 1000; for (int i = 0; i < maxSteps && !robot.atGoal(); i++) { - // Try right, straight, left, back — relative to the way we're facing. - int[] order = { (facing + 1) % 4, facing, (facing + 3) % 4, (facing + 2) % 4 }; - for (int dir : order) { - if (tryMove(robot, dir)) { - facing = dir; + // Relative to the way we're facing, try right, straight, left, then + // back — and drive the first opening. drive() updates facing() for us. + Direction ahead = robot.facing(); + Direction[] order = { + ahead.clockwise(), ahead, ahead.counterClockwise(), ahead.opposite() + }; + for (Direction dir : order) { + if (!robot.readWallSensor(dir)) { + robot.drive(dir); break; } } } -} - -// Move one step in an absolute direction if that side is open; report whether -// we actually moved. -boolean tryMove(Robot robot, int dir) { - if (dir == 0 && robot.canMoveUp()) { robot.moveUp(); return true; } - if (dir == 1 && robot.canMoveRight()) { robot.moveRight(); return true; } - if (dir == 2 && robot.canMoveDown()) { robot.moveDown(); return true; } - if (dir == 3 && robot.canMoveLeft()) { robot.moveLeft(); return true; } - return false; }` export function MazePlayground({ solver = null }: { solver?: SolverMode | null }) { diff --git a/site/src/lib/javaRuntime.ts b/site/src/lib/javaRuntime.ts index c3c8d6f..29a4aca 100644 --- a/site/src/lib/javaRuntime.ts +++ b/site/src/lib/javaRuntime.ts @@ -13,18 +13,13 @@ const CHEERPJ_LOADER_URL = 'https://cjrtnc.leaningtech.com/4.3/loader.js' // lives under the Vite base path (/software_training/ in production). const TOOLS_JAR = `/app${import.meta.env.BASE_URL}tools.jar` // FRC2713/maze-solver-java, built into a jar and vendored the same way. Gives -// lesson snippets the com.frc2713.mazesolver API (Maze/Cell/Robot/MazeSolver) -// so students write solving algorithms against it instead of raw bitmasks. The -// jar is produced by CI on deploy and by scripts/vendor-maze-solver.sh locally. +// lesson snippets the com.frc2713.mazesolver API — the Direction enum, the +// Maze/Cell/Robot interfaces, the concrete GridMaze students build with +// `new GridMaze(grid)`, and MazeSolver — so they write solving algorithms +// against it instead of raw bitmasks. The jar is produced by CI on deploy and +// by scripts/vendor-maze-solver.sh locally. const SOLVER_JAR = `/app${import.meta.env.BASE_URL}maze-solver.jar` -// Temporary shim: the maze-solver library ships only interfaces, so snippets -// that call `new GridMaze(grid)` need a concrete implementation on the -// classpath. maze-engine.jar (built by scripts/build-maze-engine.sh, committed -// like tools.jar) supplies GridMaze/GridRobot/GridCell. It comes after -// SOLVER_JAR so the library's own GridMaze wins once it ships one — at which -// point this jar and its classpath entry can be deleted. -const ENGINE_JAR = `/app${import.meta.env.BASE_URL}maze-engine.jar` -const CLASS_PATH = `${TOOLS_JAR}:${SOLVER_JAR}:${ENGINE_JAR}:/files/` +const CLASS_PATH = `${TOOLS_JAR}:${SOLVER_JAR}:/files/` // Globals installed by loader.js (a classic script, not an ES module). declare global { diff --git a/site/src/lib/mazeHarness.ts b/site/src/lib/mazeHarness.ts index b9daa82..117fee9 100644 --- a/site/src/lib/mazeHarness.ts +++ b/site/src/lib/mazeHarness.ts @@ -6,9 +6,8 @@ // body, and a `main` drives the robot then prints the Maze Trail as a single // sentinel-tagged line for the JS side to parse and animate back. // -// `new GridMaze(grid)` resolves against maze-engine.jar — a concrete engine the -// site vendors on the classpath while the maze-solver library still ships only -// interfaces (see lib/javaRuntime.ts). Nothing maze-specific is bundled into +// `new GridMaze(grid)` resolves against the maze-solver library on the +// classpath (see lib/javaRuntime.ts). Nothing maze-specific is bundled into // the harness source itself. // The maze as a ready-to-embed Java `int[][]` literal body (just the rows, no