November 5, 2019
Related post: mazes for programmers intro
Mazes for Programmers starts off with an intro to maze generation, discussing the Binary Tree and Sidewinder algorithms, their implementations and idiosyncracies.1
Chapter 2 is where we dive into actually implementing the algorithms.
Grid
and Cell
class implementations are provided by the book, and
the exercises cleanly differentiate themselves by applying their
particular algorithm to those classes.
Algorithm:
For each cell, toss a coin to choose whether to carve a passage north or east. Rinse and repeat until every cell has been visited.
For Binary Tree, I stayed faithful to the book’s Ruby implementation to get the feel of things, departing only to add a debug facility in order to print intermediate states of the grid during generation, and a seeding facility that allows a deterministic sequence of “randomized” choices to drive generation, producing identical mazes across multiple runs.
Algorithm:
As with Binary Tree, toss a coin to determine whether we carve a passage north or east, considering the eastward-connected series of cells connecting to the current cell a ‘run’. If the toss dictates north, then randomly choose one cell from among the run and carve a passageway north from that cell. Repeat until every cell has been visited.
I implemented Sidewinder without first studying the book’s version,
and despite not having used Ruby in earnest for some time I ended up
with something fairly clean. Having the underlying Cell
class do the
state bookkeeping for links between cells definitely made this easier,
though since these are actual object references with bi-directional
links between them, printing out this data structure for inspection
was a startling experience. I’m used to everything being just data!
Chapter 2 concludes with wiring up the chunky_png
library to write
generated mazes to a PNG file, so you get one of these at the end like
a little treat…
For the Clojure side of this jam, I wanted to leave my options open as far as visualizing output; for sure, I want a fast feedback loop when developing maze generation code, which in Clojure land typically means developing at a REPL – which is, by default, what you’d do, and certainly where I spend the vast majority of my Clojure development time.
But I also thought it’d be neat to wire this work up to a web app later on so I could pursue fancy visualizations like this one, which implies ClojureScript. With a bit more effort, I can achieve that at the same time via cljc, which makes a single codebase usable from both Clojure and ClojureScript compilation paths.
So my Clojure/Script code will be housed in files bearing a .cljc
file extension, and will use reader conditionals to differentiate
CLJ and CLJS codepaths for those bits that differ between JVM and JS
hosts, like so…
That’s it – that’s all you have to do to make a Clojure/Script codebase portable between Clojure and ClojureScript hosts.
To top it off, cljc is also usable directly from NodeJS engines like Lumo, so this choice also accommodates good old-fashioned fast2 command-line access to these programs, which is the mode I got into while developing the Ruby implementations.
My binary-tree implementation used a two-dimensional array of maps as cells, each with a one-way link to either the north or east.
This didn’t surface any surprises in terms of translating between
OO-style and FP-style, with the exception that the debug pattern I
established in the Ruby code of printing intermediate grid states
while generating the maze demanded a much different shape to the code
than I was happy with. In order to facilitate access to those
intermediate states, the pattern of generating the maze with no
external state via nested for
calls to build a 2D array would
probably need to flatten out into a single looping construct, such
that the inner body has access to the entire data structure in-flight
so it can be printed out.
In particular I’d like to avoid the minor but infectious consequence that cells track their own coordinates so that downstream code knows which row they occupy – in the current shape, we avoid having to deal with that by simply iterating over the discrete cell arrays. I’d like to figure out a more elegant way of facilitating debug printing while keeping things clean; I may drive out an alternate implementation. 3
sidewinder was more entertaining: Sidewinder’s mechanism for carving out a north passageway in a random selection from among the east-linked cells of the current run was a fun constraint in keeping things as stateless as possible. Again, our cells are generated in a doubly-nested loop, each containing a set of zero or more one-way links to northern and/or eastern neighbors. Using state here would have been “easier” – tracking cell coordinates and linkages in each cell would have been an obvious choice in OO land, but that would defeat the purpose here, wouldn’t it?
For the Clojure implementations, I skipped outputting to an image; I’ve got more ambitious things in mind for visualization.
I’d like to see other functional implementions of Sidewinder, and was surprised to not find much in this space. I found two, both code work-throughs of the Mazes for Programmers book:
Looking over that Clojure version, it has a more elegant means of handling the interesting bit of Sidewinder – mine strove (perhaps a bit too much) to avoid dealing with positional indexes when inserting north links into runs.
Next up: solving mazes with Dijkstra’s Algorithm and others I explore on the side. This should be fun – not least because once we have maze solving, we can have sweet CLJS visualizations of same.
In particular, the mechanics of these algorithms suffer bias, wherein the corridors generated bear some visual pattern that make the maze easier to solve manually and less aesthetically pleasing. ↩
For admittedly non-insane values of ‘fast’: On my
aging 2015 MacBook Pro, generating and outputting a single maze with
Lumo takes about half a second – just within personal tolerance
– whereas a run with the clj
binary takes about 1.5 seconds,
and I’d expect a lein
invocation to take 3-4 times as long. ↩
Using reductions
with reduce
might do
the job while preserving familiar semantics. reduce
being eager
would also mean I can dispense with the (possibly-confusing)
doall
which serves to force the lazy sequence of cells to
generate while in scope of the dynamic binding that drives random
seed control. ↩