Destination: Recursion

A retrospective on the saga — where recursion lives in the real world, what our building-block classes gave us (and cost us), and what to carry forward.

← back to Saga Index   •   Islands vs. Mazes →

The big idea: Recursion is not a trick for one puzzle — it is a design strategy that appears everywhere a problem naturally breaks into smaller copies of itself. This page names those patterns, traces the path through this saga, and honestly evaluates the trade-offs of building on a shared class library so you can make better design decisions in your next project.

1. Recursion in the Wild — Where Does This Actually Show Up?

Before you leave this saga, it is worth zooming out. The technique you just watched — try something, recurse deeper, undo if stuck — is one of five major recursion patterns that appear constantly in real software, mathematics, and computer science. Recognizing them is the first step to using them confidently.

🌲 Tree & Graph Traversal

Visit every node in a tree or graph by recursing into each child, then returning when a leaf (no children) is reached.

You see this in:

  • Scanning a file system (each folder may contain folders)
  • Walking the HTML DOM tree
  • Evaluating an expression tree in a compiler
  • Printing an organizational chart
function visit(node) {
  if (!node) return;
  process(node); // do work here
  for (let child of node.children)
    visit(child);
}
🔍 Backtracking Search

Explore a decision tree: make a choice, recurse to see if it leads to a solution, and undo it if it does not. This is exactly what this saga built.

You see this in:

  • Maze generation and solving (this saga)
  • Sudoku and crossword solvers
  • The N-Queens problem (place N queens without conflicts)
  • Generating all permutations or combinations
  • Constraint satisfaction problems in AI
✂️ Divide & Conquer

Split the problem into two (or more) independent halves, solve each half recursively, then combine the results. No backtracking needed — every sub-problem is independent.

You see this in:

  • Merge sort — split list in half, sort each half, merge
  • Binary search — discard half the search space each call
  • Quicksort — partition around a pivot, recurse each side
  • Fast Fourier Transform (FFT) in signal processing
🔷 Fractals & Self-Similar Structures

Draw something that contains smaller copies of itself. The recursive call just reduces a size or angle parameter until the base case (too small to draw) is reached.

You see this in:

  • Sierpiński triangle — triangle containing three smaller triangles
  • Koch snowflake — replace each edge segment with a spike
  • Fractal trees — each branch splits into two smaller branches
  • Recursive landscape and terrain generation in games
⚡ Dynamic Programming

Recursion + memory. If the same sub-problem will be solved many times, store the result the first time and return it instantly on repeat calls. This turns exponential naive recursion into polynomial time.

You see this in:

  • Fibonacci sequence (naïve = O(2n), memoized = O(n))
  • Shortest-path algorithms (Bellman-Ford, Floyd-Warshall)
  • Knapsack and coin-change problems
  • Text diff and edit-distance algorithms (like git diff)

What unites all five?

Every pattern above follows the same contract:

  1. Base case — a version of the problem small enough to answer directly, without recursing.
  2. Recursive case — a version of the problem that can be reduced to one or more smaller instances of itself.
  3. Trust the call stack — you do not manage "where to go back"; the return address is automatically preserved for you.

The island's _grow(), the maze's _generateDFS(), and merge sort all obey exactly this contract. Once you see the pattern, you can apply it anywhere a problem has this “smaller copy of itself” shape.

2. The Saga at a Glance — One Stage, One Idea

The saga was designed so that each stage adds exactly one new concept. Looking back, here is the key idea and the key insight from each stop.

Stage 1 — One Cell, One Click
Added: SWGrid as the ocean; SWSquare as the tile; row/column addressing.
💡 The whole saga runs on one coordinate system. Get it right once and never think about it again.
Stage 2 — Growing the Island
Added: Orthogonal-adjacency validity rule; click-to-grow with hint highlights and error flashes.
💡 You did by hand exactly what the algorithm will do automatically in Stage 3. The logic was already complete before the recursion started.
Stage 3 — Animated Recursive Growth
Added: _grow() — the first recursive function. Step queue for animation; randomize toggle.
💡 Removing the human from Stage 2 required one change: replace "wait for a click" with "recurse into each valid neighbor." Everything else stayed the same.
Stage 4 — Ocean Pits
Added: Pit cells (impassable). Click to place or sprinkle a random percentage.
💡 _grow() didn't change at all — the validity check already rejected non-zero cells. Obstacles were free.
Stage 5 — Recursion Cost
Added: Live ADD / BACKTRACK counter; backtrack ratio readout; BFS pre-check safety guard; 150,000-step ceiling.
💡 Pits don't just block paths — they force the algorithm to explore and abandon dead ends, measurably raising algorithmic cost.
Stage 6a — Multiple Islands
Added: N islands (1–8); auto-seed placement; per-island color palette; unified step queue.
💡 Scaling from 1 to N islands required only an outer loop and a richer step-event format. _grow() was untouched.
Stage 6b — Diagonal Separation Rule
Added: 8-direction neighbor check in both _isValidExpansion() and findRandomSeedPosition().
💡 Swapping a 4-direction loop for an 8-direction loop was the only code change needed to upgrade from "no orthogonal touching" to "no touching of any kind."
Stage 7 — OOP Refactoring
Added: Coordinates class; SWIsland class; buildIsland() static factory.
💡 Six standalone helper functions collapsed into one SWIsland.buildIsland() call. The sketch got shorter; the classes got reusable.
Maze 1 — DFS Build & Solve
Added: SWMaze class; wall-carving DFS; DFS solver; BFS shortest-path overlay.
💡 The same recursive skeleton — try a neighbor, recurse, backtrack when stuck — ran in a completely different domain. Same pattern, new costume.
Maze 2 — Resume Junction
Added: One new cell state ('resume'); one orange color; four new lines in the solve step handlers.
💡 Four lines of code turned an invisible algorithm moment — the exact instant the solver picks back up after a dead end — into a first-class visible event.

3. The SketchWave Toolkit — Honest Pros & Cons

This saga was built on five SketchWave utility classes: SWGrid, SWColor, SWSquare, SWPoint, and SWLine. Using a shared class library is a deliberate design choice, not a free lunch. Here is an honest accounting of what each class gave us and what it cost.

SWGrid The ocean. A coordinate-aware grid canvas.
✅ Simplified the journey ⚠️ Added complexity
One-call coordinate conversion. screenToUser() and userToScreen() let every algorithm work in row/column math coordinates. The y-axis inversion (screen y grows down, math y grows up) was solved once inside SWGrid and was invisible everywhere else. Abstraction hiding effects. When a coordinate printed wrong or a cell appeared in the wrong row, the inversion was the first suspect and required understanding the grid's internal model to debug. Reading "row 0 is the top row" in the API took deliberate learning.
Background drawn in one call. The filled grid background with all cell borders rendered with a single grid.draw(). Without SWGrid, every sketch would have duplicated the nested loop and cell-size math. Cell sizing was manual. SWSquare instances had to be sized to match exactly one grid cell, which required asking the grid for its cell dimensions and passing that value to each square constructor. It was a two-step setup that a tighter API could have automated.
Label support built in. userToScreen() made drawing row/column labels and hint overlays straightforward with no extra coordinate math. Required upfront API investment. None of the grid functionality worked until the coordinate model was understood. Students who skipped the Stage 1 explanation struggled to place even a single cell correctly.
SWColor Color wrapper using HSB (hue 0–360, saturation / brightness / alpha 0–100).
✅ Simplified the journey ⚠️ Added complexity
HSB is intuitive for dynamic color. Generating a palette of N islands meant stepping the hue by 360 / N degrees while keeping saturation and brightness fixed — one variable, clean result. In RGB you would have to solve for complementary colors manually. Global color-mode state. p5.js requires colorMode(HSB, 360, 100, 100, 100) to be called before any SWColor fills work correctly. Forgetting it caused all colors to render as washed-out grays with no obvious error message.
Semantic color families. "Same hue, lower saturation" = washed out. "Same hue, higher brightness" = lighter. These relationships are direct in HSB; in RGB they require mixing three channels simultaneously. Non-standard scale. Most web color references specify saturation and brightness as 0–100%, but show hue as 0–360° with alpha as 0–1 or 0–255. SWColor's 0–100 range for alpha meant every copied web example needed conversion before it would work.
Encapsulated fill + stroke pairing. Passing one SWColor object for fill and one for stroke kept each cell's visual specification self-contained and easy to swap. Requires pushStyle / popStyle discipline. Setting a color calls p5's fill() / stroke() which are global state. Drawing one component with the wrong color active required hunting for a missing push/pop pair.
SWSquare The island/ocean cell tile. Draws itself on a grid.
✅ Simplified the journey ⚠️ Added complexity
drawOnGrid(grid) handles positioning. Each SWSquare knows its SWPoint center in user coordinates. Calling drawOnGrid() converts that internally to screen pixels — the sketch never performed pixel math for cells. Appearance and position are coupled at construction. Creating a square required knowing the cell size at create time, not at draw time. When grid dimensions changed (e.g., resizing the canvas), all squares had to be rebuilt, not just redrawn.
Clean separation of concerns. SWSquare knows appearance (fill, stroke, size). SWPoint knows position (row, column). The island array stores position; the display logic reads appearance. Each concern lives in exactly one place. Each animated step replaced the entire square object. Changing a cell's color during animation (e.g., green ADD → red BACKTRACK) meant constructing a new SWSquare with different colors, not just calling setColor(). This created more object churn than a mutable approach would have.
SWPoint Coordinate container: (x, y) in user-space.
✅ Simplified the journey ⚠️ Added complexity
Signals intent. Using new SWPoint(col, row) instead of an anonymous {x: col, y: row} object made it unambiguous that a value represented a grid position, not an arbitrary pair of numbers. Code that receives an SWPoint knows exactly what it is. Thin wrapper with little behavior. SWPoint in this saga was mostly a coordinate bag: there was rarely a method to call on it beyond reading .x and .y. A plain object literal would have worked mechanically, though it would have been less self-documenting.
Consistent parameter passing. Because SWSquare always receives an SWPoint, the interface is typed by convention: if it accepts a point, you know what to pass. This avoided a whole class of argument-order bugs (row vs. column, x vs. y) that are common with raw number pairs. Required learning the x = column, y = row mapping. SWPoint uses (x, y) which maps to (column, row) in grid terms — not the (row, column) order that Java's Coordinates class uses. Students moving between the two representations needed to track which convention was in play at any given time.
SWLine A line segment between two SWPoint endpoints.
✅ Simplified the journey ⚠️ Added complexity
Available when you need it. SWLine is designed for drawing named, stored line segments — connection paths, arrows, annotated corridors. When a future stage needs to draw the BFS shortest path as a highlighted corridor, or trace a recursion arrow in an explanation, SWLine.drawOnGrid(grid) would do the job without any new coordinate math. Not used — and deliberately so. The maze walls are not stored line objects. They are boolean flags (walls.N/S/E/W) on each cell, drawn in a single nested loop with one shared stroke style set before the loop begins. Creating a new SWLine(new SWPoint(...), new SWPoint(...)) for each of ~280 cells × 4 sides × 60fps would add object-allocation pressure with zero design benefit.
Illustrates toolkit design thinking. The fact that SWLine exists and follows the same drawOnGrid() pattern as SWSquare demonstrates a consistent API surface: any shape class in the library draws itself the same way. That consistency makes the library easier to learn as a whole. The walls are condition-driven, not object-driven. Each wall is drawn only if its boolean is still true, and the animWalls shadow array controls which walls are revealed frame by frame during the build animation. That tight coupling between the decision logic and the draw call is exactly what object encapsulation would obscure, not help.
Right tool, right job. A class is the right choice when you have state to encapsulate and behavior to reuse. SWSquare encapsulates position + fill + stroke and is constructed once per cell. The maze walls have no persistent identity — they are transient geometry read from a data structure. Raw line() calls are shorter, faster, and clearer here. "Load everything" vs. "load what you use." SWLine is not even loaded in the maze HTML files — it was correctly omitted. The early island stages did load it as part of a library bundle without using it. Larger projects benefit from loading only what is actually imported; the habit of auditing your script tags is worth building early.
The design question to ask every time: “Am I storing this geometry, or just drawing it?” If you are storing it — naming it, reusing it, animating it independently — a class object earns its place. If you are drawing it in a tight loop from a data array, raw drawing commands are almost always cleaner. SWLine would be the right call if maze walls were animated individually (e.g., falling away or changing color on hover); for a batch, condition-driven wall pass, it is the wrong abstraction.

Overall verdict: the classes earned their place

The SketchWave classes did what a good library is supposed to do: they moved repetitive, error-prone work — coordinate conversions, color-mode setup, pixel math — out of the sketch code and into tested, reusable containers. Every stage benefited from not rewriting that infrastructure.

The costs were real but manageable: each class required upfront API learning, global state awareness, and occasional workarounds for cases the library wasn't designed for. That trade-off — cost of learning vs. cost of reinventing — is the central question of every library or framework decision you will ever make.

4. OOP — Breaking Big Problems into Small, Modular Ones

The journey from Stage 1 to Stage 7 is equally a story about objects. Watch what happened to the sketch as we added classes:

Stage 6b — Before OOP (sketch-level functions)
// Six separate sketch functions, // all sharing raw global arrays: findRandomSeedPosition(grid, ocn) _isValidExpansion(r, c, ocn, islandId) _getValidNeighbors(r, c, ocn, id) _grow(r, c, target, ocn, id, queue) bfsReachable(r, c, target, ocn) buildAllStepQueues(count, ocn) // ~120 lines + all global state exposed
Stage 7 — After OOP (SWIsland class)
// buildAllStepQueues() in the sketch // now does this: for (let i = 0; i < count; i++) { const island = SWIsland.buildIsland( target, ocn, hue, stepQueue ); if (island) islands.push(island); } // ~8 lines. State lives inside SWIsland.

That compression — six functions down to one call — happened because the class moved the algorithm's state (the grid, the island cells, the step queue) and its behavior (grow, validate, seed) into one place. The sketch's only job became orchestrating results, not managing raw arrays.

Encapsulation

SWIsland owns its grid, cells, and step queue. A sketch that holds an SWIsland object cannot accidentally corrupt the island's internal array — it can only call the methods the class exposes. The object hides complexity behind a simple interface.

Reusability

SWMaze was built by recognizing that the same step-queue pattern from SWIsland could serve a completely different algorithm. The pattern transferred; only the content of the queue events changed. A good class design generalizes.

Java Mirroring

SWIsland was designed to mirror Island.java one-to-one: same method names, same algorithm structure, same toString() output format. When the Java console printed what the JavaScript canvas showed, the connection between the two languages became concrete. Classes are the vocabulary you need to compare implementations across languages.

The object-oriented insight in one sentence

A class is not just a container for code — it is a promise: “give me the inputs I need, call the method I publish, and I will give you the output you want. You do not need to know how I do it, and I will not let you accidentally break me.” The more complex the algorithm, the more valuable that promise becomes.

5. What to Carry Forward

Here are the ideas from this saga that transfer to any future project, any language, and any problem that has the right shape.

1

Recognize when a problem has "smaller copies of itself."

A function that needs to grow an island can call itself on a smaller island with one fewer cell remaining. A function that needs to carve a maze can call itself from the next unvisited cell. When you spot that self-similar structure, recursion is probably your cleanest solution.

2

The call stack does the bookkeeping for you.

You do not have to remember where you came from. Every time you return from a recursive call, you are automatically back where you were, with your local variables still intact. This is why backtracking is almost trivially easy with recursion: just remove what you added and return — the stack does the rest.

3

Measure the cost before assuming it is affordable.

Stage 5 showed that a pitted ocean doesn't just block paths — it multiplies the number of dead ends the algorithm must explore and abandon. Any recursive backtracking algorithm can become exponentially expensive on adversarial inputs. Always identify your worst case, and design a circuit-breaker (like the 150,000-step ceiling or the BFS pre-check) when it matters.

4

Change one thing at a time and measure the effect.

Stage 6b changed exactly one predicate from 4 directions to 8 directions. Maze 2 added exactly one cell state and four lines. Each of those single changes produced a measurable, visible outcome. This is the discipline of controlled experiments applied to code: once you change two things at once, you lose the ability to know which change mattered.

5

A shared class library is a trade-off, not a free upgrade.

The SketchWave classes removed boilerplate and improved readability, but they demanded upfront learning, introduced global-state risks, and occasionally forced workarounds. Every library decision has this shape. The question is never “is this library perfect?” but “does the value it provides outweigh the cost of learning and depending on it?” For this saga, it did.

6

The algorithm is the same in every language.

Island.grow() in Java and _grow() in JavaScript use the same logic, the same direction arrays, and produce the same output. The syntax differs; the idea is identical. Learning an algorithm well enough to implement it in one language means you can implement it in any language — because algorithms live above syntax.

7

Good classes make the next problem easier.

SWMaze was built faster than SWIsland because the step-queue pattern, the grid rendering infrastructure, and the color system were already solved. Every well-designed class you write is a tool you get to reuse. The payoff for writing clean, encapsulated code is not just the current project — it is every project that comes after it.

🏝️  🧩  🌲  🔷  ⚡

You started with a single orange square on a grid.
You ended with two recursive algorithms, two reusable OOP classes, and a Java–JavaScript bridge that makes the same algorithm visible in two languages simultaneously.

That journey — from one cell to a class library — is what software engineering actually looks like: small, deliberate steps, each one building on the last, until something genuinely complex feels surprisingly simple.