

A few months ago I wrote about my simple test to see how both Bard and ChatGPT 3.5 did with a simple programmer interview question (summary: neither one passed) and since Bard has had its innards recently replaced (with Google Gemini LLM) which is supposedly an improvement over its original PaLM LLM AI, I thought I’d rerun the test for Bard to see if it did any better.
The prompt I submitted back in September was:
create java 8 code/class that provides a macro replacement capability. specifically a string can contain zero or more macro references where a reference is something like “$name” (the $ char introduces the alphanumeric macro name), and the processing of the string requires a map and each $name is replaced with the corresponding value in the map. one special case is that if the $ char is encountered within a single-quote string, then it’s not taken as a macro
and so I submitted it exactly as above to the improved Bard. This is what is produced (though I did add the import java.util.*
and the main()
method):
import java.util.*;
public class MacroReplacer {
private final Map<String, String> macros;
public MacroReplacer(Map<String, String> macros) {
this.macros = macros;
}
public String replaceMacros(String text) {
StringBuilder builder = new StringBuilder();
int pointer = 0;
while (pointer < text.length()) {
int dollarIndex = text.indexOf('$', pointer);
if (dollarIndex == -1) {
builder.append(text.substring(pointer));
break;
}
// Check if dollar is inside a single-quote string
int prevQuoteIndex = -1;
int quoteIndex = text.indexOf('\'', pointer);
while (quoteIndex != -1 && quoteIndex < dollarIndex) {
prevQuoteIndex = quoteIndex;
quoteIndex = text.indexOf('\'', quoteIndex + 1);
}
if (prevQuoteIndex != -1 && prevQuoteIndex % 2 == 1) {
// Dollar inside single-quote string, append as-is
builder.append(text.substring(pointer, dollarIndex + 1));
pointer = dollarIndex + 1;
continue;
}
// Extract macro name
int nextSpaceIndex = text.indexOf(' ', dollarIndex + 1);
int macroNameLength = nextSpaceIndex != -1 ? nextSpaceIndex - dollarIndex - 1 : text.length() - dollarIndex - 1;
String macroName = text.substring(dollarIndex + 1, dollarIndex + 1 + macroNameLength);
// Replace macro with value from map
String macroValue = macros.get(macroName);
if (macroValue != null) {
builder.append(macroValue);
} else {
builder.append('$');
builder.append(macroName);
}
pointer = dollarIndex + 1 + macroNameLength;
}
return builder.toString();
}
/**
* had to add (copied from ChatGPT's code)
*/
public static void main(String... args)
{
Map<String, String> macroMap = Map.of(
"name", "John",
"age", "30"
);
MacroReplacer bard = new MacroReplacer(macroMap);
String input = "Hello $name! You are $age years old. Do not replace ' $age' in single quotes.";
String output = bard.replaceMacros(input);
System.out.println(output);
}
}
and when I ran it I got
$name!30$age' in single quotes.
which, sadly, is much worse than original Bard.
Maybe GPT4 is better (I don’t want to pay to test that) but, then again, maybe I’m justified in still not worrying about them taking my job.
Late last year (2022) it seemed like the programming world freaked out about the capability of ChatGPT and, a bit later, Google’s Bard–it seemed like there were messages of doom everywhere about how programmers would be out of a job soon. There were many headlines about how people could create apps in just a few minutes.
After a while the hype settled down and the reality of what those two LLMs could be with respect to programming became more understood–it seemed that maybe after all, people like me wouldn’t be (yet) replaced by AI.
When I give interviews to candidate engineers, I have a standard set of questions/tasks that I ask them, somewhat to be able to normalize across engineers and somewhat because the questions I’ve asked seem to do a reasonable job of identifying what I view as strengths and weaknesses.
One of the questions I routinely ask candidates is a coding task that asks them to write Java code that does macro replacement. (For those readers who don’t know what this means, think of form emails/letters that start with something like “Dear Scott”, where the “Scott” part is taken from some database of people–behind this is something that has some template like “Dear firstname” — that replacement of firstname with “Scott” is basically a macro replacement).
I happened to interview a candidate recently for one of my contracts and was intrigued by the approach the candidate took to this task–he focused on optimization and ended up using what’s called a stack to try to make it as fast as possible (I don’t know that his approach would have been all that optimized…). That got me to thinking about whether or not my own answer to that interview question was faster (more optimized) than the usual approach that I’ve seen interview candidates respond with. (Yes, prior to asking candidates this question, I created a solution–in fact it was the solution that prompted me to include the problem as an interview question.)
For whatever reason I decided to test out ChatGPT and Bard to see how they both would do with solving this pretty trivial programming problem, mostly to compare the efficiency of my approach (which is to use a Finite State Machine–see https://en.wikipedia.org/wiki/Finite-state_machine).
I know there’s somewhat of an art to submitting the optimal “prompt” to these LLM AIs, but I’m not the sort to try to figure out how to make a supposedly smart AI be really smart, so I submitted the following prompt to both ChatGPT (3.5–the public version) and Bard (still public, but continually updated based on the alerts I see when I use it):
create java 8 code/class that provides a macro replacement capability. specifically a string can contain zero or more macro references where a reference is something like “$name” (the $ char introduces the alphanumeric macro name), and the processing of the string requires a map and each $name is replaced with the corresponding value in the map. one special case is that if the $ char is encountered within a single-quote string, then it’s not taken as a macro
From each of them I got Java code that compiled successfully. I didn’t trust the code at all to work so I started reviewing each result and realized that each one had bugs. For those of you who are programmers you might want to review the following code to see if you can spot the errors
This is what ChatGPT came back with given the above prompt
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Chatgpt {
public static String process(String input, Map<String, String> macroMap) {
String pattern = "\\$(\\w+)";
Pattern r = Pattern.compile(pattern);
StringBuilder result = new StringBuilder();
boolean inSingleQuotes = false;
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
if (c == '\'') {
inSingleQuotes = !inSingleQuotes;
result.append(c);
} else if (!inSingleQuotes && c == '$') {
Matcher matcher = r.matcher(input.substring(i));
if (matcher.find()) {
String macroName = matcher.group(1);
String macroValue = macroMap.getOrDefault(macroName, "");
result.append(macroValue);
i += matcher.group(1).length() - 1;
} else {
result.append(c);
}
} else {
result.append(c);
}
}
return result.toString();
}
public static void main(String[] args) {
Map<String, String> macroMap = Map.of(
"name", "John",
"age", "30"
);
String input = "Hello $name! You are $age years old. Don't replace ' $age' in single quotes.";
String output = process(input, macroMap);
System.out.println(output);
}
}
(Note: I modified the test input string based on what I believed would be a problem after reviewing both ChatGPT- and Bard-generated code; the specific modification I did was to add white space in between the ‘ and the $ characters).
And this is what running it produces:
Hello Johne! You are 30e years old. Don't replace ' 30e' in single quotes.
which is clearly not what is intended as the last character of the macros (name
and age) are incorrectly being included.
This is what Google’s Bard came back with (with an edit of mine to include the main
method with a string that I believed would cause it to fail):
import java.util.*;
public class Bard {
private final Map<String, String> macros;
public Bard(Map<String, String> macros) {
this.macros = macros;
}
public String replaceMacros(String str) {
StringBuilder sb = new StringBuilder();
int i = 0;
while (i < str.length()) {
char c = str.charAt(i);
if (c == '$') {
// Check if the '$' character is within a single-quote string.
if (i > 0 && str.charAt(i - 1) == '\'') {
sb.append(c);
i++;
} else {
// Find the end of the macro name.
int end = i + 1;
while (end < str.length() && Character.isAlphabetic(str.charAt(end))) {
end++;
}
String macroName = str.substring(i + 1, end);
// Replace the macro name with the corresponding value in the map.
String macroValue = macros.get(macroName);
if (macroValue != null) {
sb.append(macroValue);
} else {
sb.append("$");
sb.append(macroName);
}
i = end;
}
} else {
sb.append(c);
i++;
}
}
return sb.toString();
}
/**
* had to add (copied from ChatGPT's code)
*/
public static void main(String... args)
{
Map<String, String> macroMap = Map.of(
"name", "John",
"age", "30"
);
Bard bard = new Bard(macroMap);
String input = "Hello $name! You are $age years old. Do not replace ' $age' in single quotes.";
String output = bard.replaceMacros(input);
System.out.println(output);
}
}
and when it’s run it produces
Hello John! You are 30 years old. Do not replace ' 30' in single quotes.
and that’s clearly wrong as it still replaces the macro even though it’s “protected” by single quotes.
I don’t want to draw this out by trying to figure out either why they both failed or how to make them produce correct code. I just want to show that a they both fail–miserably in my mind–to create correctly functioning code given a coding prompt. And I say “miserably” because the bugs that each solution has isn’t immediately obvious and shows up only with actual testing
My personal message on all this is if anyone thinks that the current versions of ChatGPT and Bard can do anything but produce some code that has to be reviewed, tested, and very likely modified, then they need to actually try out these LLMs. They just aren’t going to come close to replacing non-trivial programming at this time (and, my personal opinion is, after being in the industry for almost 45 years) is that it will be a very long time before we programmers have to worry about our jobs.
This is companion piece to “how to solve a maze”.
I’ve spent far too much time playing with mazes, both when I was young, and now later in life. I’ve wondered if this would fall under a “special interest” and I think there’s some argument that it could.
While I was playing with code for solving a maze, I had to have some maze for it to solve. Initially I just had the code take out walls at random, knowing that while this wasn’t the best approach by any means, it was probably good enough to test the solver code (assuming enough walls were taken out). I also resorted to hardcoding a set of walls to remove on a small maze.
But that wasn’t good enough, and I wanted to create good and fun mazes, though those attributes are hard to define.
Now, if a maze has a single path from start to finish, then a deterministic algorithm as described in the other post will find it. Unfortunately the creation of a good and fun maze isn’t so deterministic as the generation of it must incorporate some amount of randomness that ends up presenting a good number of long-ish wrong paths. Additionally maze creation has a few constraints that make it a bit less straightforward: all cells should be reachable from the start, and there should (ideally) be exactly one path through it.
Sometimes I rush into coding without thinking enough. I think this happens when I think the problem is easier than it really is, and because I’ve coding long enough that I think I don’t need to stop and think. In any case, that’s what I did at first–I didn’t think enough and ended up going down some blind alleys, which is totally apropos solving mazes.
My first attempt was to use recursion to create a path (I guess recursion was on my brain). My code notes describe the idea:
/**
* The real maze maker recursive algorithm. Not sure if it will work
* but the idea is that each invocation of this method the current
* position is a newly entered square and we have to randomly decide
* which direction to go (if we move at all), subject to the following
* conditions:
*
* * each cell must have at least 1 wall
* * we can't open up a wall to an adjacent cell that's already been visited
*
* We will rotate through the 4 directions and each time we have
* a low probability of opening up a wall in that direction. Once
* we open up a wall, we recurse into this method.
*/
It kept track of the “ends” of the path and if the processing of the current “end” didn’t end up reaching the finish of the maze, it reprocessed it. I graded it as a failure because it left some cells isolated and it produced more than one path through the maze (at least that specific implementation did that–it might have been able to be modified to avoid one or both of those conditions, but I didn’t pursue that).
When it was clear that that approach didn’t work, I changed the code to choose cells at random, checking if they had at least one wall missing (meaning they were connected to the start cell through some path) and then opening up one of the remaining walls at random. The code would then, using my solver, check if there was a path from start to finish, and if there was, then the first phase of the construction was done. The second phase consisted of finding all the isolated cells (those that had 4 walls still), and opening up one of those walls at random, repeating until there were no isolated cells left.
Unfortunately that didn’t work all that well either for similar reasons.
I don’t have enough notes in code left to reconstruct the different attempts between the recursive non-solution and the queue-based one that actually does a pretty good job so I’m just going to skip to describing this workable solution.
One of the issues noted above is how the recursive algorithm(s) left some cells isolated and one of the reasons for this happening is that there’s nothing that keeps track of what to try next other than the details in the stack of recursive calls and that information is lost bit by bit when a recursive call returns.
The way around this is to explicitly keep track of the cells that are still open to being extended, where this means specifically that it’s possible to open up another wall in the cell, where opening up a wall is allowed only if there’s a cell on the other side of the wall (i.e., not off the edge of the maze) and that cell has all four walls in place. This idea of keeping a cell “in play” if it’s possible to open up another wall is key to creating a maze that doesn’t leave any isolated cells.
Now, it’s possible to keep track of these in-play cells by examining each and every cell each time it’s time to try to open up another cell/wall, but there are some downsides to this:
The idea of the queue-based solution is to retain in the queue only those cells that might be able to have another wall opened up. We first push the start cell onto the queue, as it’s by definition it has all four walls intact, then we go into a loop that does the following:
That will produce a maze with a path from start to finish. And, interestingly, it doesn’t have to check if it’s solvable as it will always be solvable.
The details of step 4 determine if the maze is interesting or not, so it’s important to look at the possibilities in step 4. Given that there are two new items to push onto the queue (the just-examined cell–call this “current”–and the just-opened cell–call this “target”) and two ends to push them onto (which is possible using Java’s Queue class), the following enumerates the raw combinations:
There are duplicates: (4) and (6), and (2) and (8), so there are six unique ways of pushing these two cells in step 3 (this ignores the strategy of inserting both at random positions in the queue).
Since the algorithm pulls the next cell to try to extend from the head of the queue, the maze created depends on what gets pushed onto the head (if anything). If we re-push to the head the cell just taken off, then the algorithm basically expands out the cell as much as possible before moving to the next one, which has a different effect than if the just-opened cells are expanded before reprocessing.
My code allows me to choose the extension strategy so I can evaluate the effects of them on the final maze. Given that each generated maze is different (due to the use of randomness) it’s not always easy to tell what’s good or not, but in general there are some patterns: (2) and (3) produce mazes that have fairly direct and straight paths from start to finish; (7) and (8) produce slightly more visually complicated mazes than (2) and (3) but whose solutions are equally simple; (1) produces mazes with long paths from start to finish that wind up and down, which look like they’d be hard to solve but really aren’t as there aren’t many deep branches along the way; (4) looks like it produces complicated mazes (more on this below); (5) is similar to (1); and (6) is similar to (1).
Here are a few simple character-based renderings of some of the various strategies (10×10 maze):
_________________________________________
| | | | | | | | | | |
| → → → → → → ↓ | → F |
|_ _|___|___|_ _|_ _|_ _|_ _|___|_ _|___|
__ ___________ ___ ___ ___ _______ ______
| | | | | | | | | | |
| ↑ | | | | | | ↓ | ↑ | |
|_ _|_ _|_ _|___|___|___|_ _|___|_ _|_ _|
__ ___ ___ _______________ _______ ___ __
| | | | | | | | | | |
| ↑ ← ← | | ↓ ← | ↑ ← |
|_ _|___|_ _|_ _|___|_ _|_ _|___|___|_ _|
__ _______ ___ _______ ___ ___________ __
| | | | | | | | | | |
| | ↑ ← | ↓ | | | ↑ |
|_ _|___|___|_ _|___|_ _|___|___|_ _|_ _|
__ ___________ _______ ___________ ___ __
| | | | | | | | | | |
| | | ↑ | ↓ | → ↑ |
|_ _|___|_ _|_ _|___|_ _|___|___|_ _|___|
__ _______ ___ _______ ___________ ______
| | | | | | | | | | |
| | → ↑ | → ↓ | ↑ |
|_ _|___|_ _|___|___|___|_ _|___|_ _|___|
__ _______ _______________ _______ ______
| | | | | | | | | | |
| | ↑ | | | | → → ↑ |
|_ _|___|_ _|_ _|_ _|_ _|___|_ _|_ _|___|
__ _______ ___ ___ ___ _______ ___ ______
| | | | | | | | | | |
| | ↑ ← ← ← | | | |
|_ _|___|_ _|_ _|___|_ _|_ _|___|_ _|_ _|
__ _______ ___ _______ ___ _______ ___ __
| | | | | | | | | | |
| | | | ↑ ← | | |
|_ _|___|___|___|___|___|_ _|___|___|_ _|
__ _______________________ ___________ __
| | | | | | | | | | |
| | S | |
|___|___|___|___|___|___|___|___|___|___|
and another
_________________________________________
| | | | | | | | | | |
| F | ↓ ← | | |
|_ _|___|___|_ _|_ _|___|_ _|_ _|___|_ _|
__ ___________ ___ _______ ___ _______ __
| | | | | | | | | | |
| ↑ ← ← ← | ↑ ← | | | |
|___|___|___|___|___|_ _|_ _|_ _|___|___|
______________________ ___ ___ __________
| | | | | | | | | | |
| | ↑ | | | |
|_ _|___|___|___|_ _|_ _|_ _|___|_ _|_ _|
__ _______________ ___ ___ _______ ___ __
| | | | | | | | | | |
| | → → ↓ | | ↑ ← | | |
|_ _|_ _|___|_ _|___|___|_ _|_ _|___|_ _|
__ ___ _______ ___________ ___ _______ __
| | | | | | | | | | |
| | ↑ | | ↓ | → ↓ | ↑ | | |
|_ _|_ _|_ _|_ _|_ _|_ _|_ _|_ _|_ _|___|
__ ___ ___ ___ ___ ___ ___ ___ ___ ______
| | | | | | | | | | |
| | ↑ | | → ↑ | → ↑ | | |
|_ _|_ _|_ _|___|___|_ _|___|_ _|___|_ _|
__ ___ ___ ___________ _______ _______ __
| | | | | | | | | | |
| | ↑ | | | | | |
|_ _|_ _|___|___|_ _|___|_ _|_ _|_ _|_ _|
__ ___ ___________ _______ ___ ___ ___ __
| | | | | | | | | | |
| ↑ ← ← | | | |
|___|___|___|_ _|___|_ _|_ _|___|___|_ _|
______________ _______ ___ ___________ __
| | | | | | | | | | |
| | → ↑ | | | | | |
|_ _|___|_ _|___|_ _|_ _|___|_ _|_ _|_ _|
__ _______ _______ ___ _______ ___ ___ __
| | | | | | | | | | |
| ↑ ← ← ← S | |
|___|___|___|___|___|___|___|___|___|___|
While both of the above mazes have a solution that looks interesting, only the second one is what I’d call a maze that’s not trivial to solve, as the first one doesn’t have many “deep” branches. The second one used strategy (4) while the first one used strategy (1).
This is where more (objective) analysis of the strategies is useful but I haven’t done much on that. I can say that there appears to be a balance between the branching factor (basically a normalized number of branches for each cell along the solution path), branch depth (the maximum path length of each incorrect branch), and branch volume (how many cells each incorrect branch occupies in total). I can only say that after eyeballing a number of mazes created by each strategy that strategy (4) creates satisfying mazes–ones that provide ample opportunities to take a wrong turn.
[Pulled from journal archives . Perhaps it could be useful to someone, but on rereading sounds a blt trite in certain areas.]
There’s nothing that can prepare you for the experience. No amount of reading of another person’s account—even this one—that will help you understand what you might go through.
I was a fool, thinking that I could attempt my first experience having just read about it, and without a guide who had done it before, and for thinking I could handle the larger amount. I don’t know how someone could just go to the mountains and ingest the mushroom without support. After my experience that seems somewhat suicidal.
I took too much. I experienced astoundingly beautiful images and had insights on things that were important to me. I also thought I had overdosed and could really have died—my heart stopping, really just an extension of the slight facial muscle paralysis or weakening on my left side. I rolled around on the floor with nausea and almost-shock from lack of food from probably foolishly attempting it in the morning after a long fast. Perceptions altered, short term memory somewhat unable to function as it normally does to weave moment to moment into a more coherent story. The in-between wake and dream state extended into an hour or more. I was desperate at times to want it to stop. I wanted to know if the drug had peaked so I could convince myself that my heart wasn’t going to stop, that I didn’t overdose.
Downstairs in the darkened room with the meditation music going, I had clearly started on the pleasant journey after close to two hours after the first mushroom stalk and head. Absolutely exquisitely beautiful images in my mind morphing as the music played. Millions of points of prismatic pins of light shaping the outlines of castles and plants,
iridescing into brilliantly lit other-colors. Simply no words can describe what was in my head, my mind. I didn’t know where those images came from—the music was an important part, guiding in some sense the transformation from moment to moment. When the sounds subsided between “songs” I ached for the music to come back to guide the images some more.
For most of my pleasant side of my journey I was also grounded by the physical body. I felt weak and hungry and slightly nauseous while experiencing the grand images of my mind. The awareness of the physical and the non-physical was certainly a theme in the overall experience—I was at times intensely aware of my mortality yet was off in an intensely non-physical experience.
Around three hours after first ingesting I went upstairs—I was feeling quite sick and needed something, food, water, sugar, anything to help me feel better. That was the beginning of when I started to seriously wonder if I had taken too much and was in real danger of dying. I felt stupid for having put me—no, Helen, really—in that position. I wasn’t okay with dying and wasn’t okay with having her have to be the the one to call 911 or to tell anyone what had happened.
I can’t recall the sequence of where I was when what happened—I know I was on the couch with Helen talking with me and I was feeling less scared about dying. It was clear that I still had paralysis/slowness from the mushroom, but I felt at some point that I was past the critical am-I-going-to-die point and so Helen and I went back downstairs to the music and the glorious glorious mind images.
“Break me down” is what said out loud and to myself when I first started experiencing the effects. I was doing this “trip” for a purpose, dammit. I asked that my subconscious give me insights on my (conscious) fear of death, and on relationships. I wanted to break myself down, to get rid of the “ego” that incessantly talks (and listens). Later on, toward the end after I knew the effects were fading and I’d live, I also asked to be healed. I breathed “break me down” and “heal me”, with tears slowly streaming down each cheekbone.
Somewhere in the middle of the experience my insight was “life is precious”. I don’t remember why that came up, only that it was important, very important. I now take that to mean my life and the lives of others, and that by important I, or whatever it was that was talking and listening, mean that experiencing life is important, that the physical body is important (keep it healthy). Life is a gift, although that word wasn’t used in the message at that point in the experience. There was no morality attached to things, which to me in this moment could be interpreted as “do what you want” selfishness, simply a message that life is precious.
After most effects had faded and I was trying to come to grips with my original question about fear of death, I realized that “life is precious” is the answer to my fear of death. Possibly. I’m still trying to make sense of it all.
It’s been several hours since I knew the effects were mostly gone. I couldn’t conjure up those beautiful images in my mind if I tried now. My left hand isn’t typing well—it seems like the left-side semi-paralysis dropped from my face to my hands. I trust that will pass in time. I’m trying to sort things out still. I know that it felt like both an important spiritual experience and a letdown as I didn’t have new profound insights. It’s clearly too early to know of long term effects on my outlook and behavior, but right now I’m feeling like it’s easier for me to help people in their own journeys.
When I was really young, maybe ten years old, I loved mazes, both solving them and creating them. That was decades ago, prior to personal computers (I’m dating myself with that disclosure) and so the only mazes we could really solve were in small books my friend and I bought (or could get a parent to buy). The mazes in those books were often done in shapes of dinosaurs or dragons or what have you, and the mazes were (sometimes) made more difficult because of those shapes. We learned to draw mazes, in both the original between the lines and in the style of on the lines. Depending on details such as curves, hatch marks, and size, these mazes we made were easy or hard.
When I got my first “personal computer” (a TRS-80 Model I, with 16K of RAM with a display that showed 16 rows of 64 columns of uppercase-only text, with the ability to switch into graphics mode that allowed 48 rows of 128 columns of blocky pixels), I (somehow) found a BASIC program that I typed in that would create and solve mazes; at least I’m pretty sure I didn’t create the algorithms by myself, but it’s been so long ago (45 years) that maybe I actually did create it myself. If so, I apologize to my smarter younger self.
Then, for decades I forgot about all that until we purchased the game quoridor at a local bookstore , where the goal of the game is to get a pawn from your side to the opposite side where the path to the other side can be (will be) blocked by walls that you and your opponent put up between the squares. After playing it I thought about writing a program that would play it better than humans could (even though the branching factor on each move is well over 100–more than chess has, if I recall correctly).
One of the rules of the game–which is what prompted my recall of all the maze-making and -solving days in my past–is that no placement of a wall can totally block a player from reaching the other side. This translates to making sure that there’s some path from the current position to the finish, which is essentially the same as solving a maze.
There are significant differences between a well-crafted maze (i.e., a single non-trivial path from start to finish) and the path problem presented by quoridor rules (i.e., sparse wall placement, allowing many paths from start to finish), but the general problem is the same.
So I started a quick prototype of a computer game and got sidetracked on the movement rule that disallows a complete block to the finish, and after a false start on a generalized maze-solving algorithm, I got a simple algorithm fully working, and it’s this algorithm that I’m covering in this post, mostly because it brings back a lot of good memories of just working through a completely abstract problem.
From a conceptual perspective (with a big, big, lean toward computer science concepts), a maze is just a tree with the start position as the root of the tree and some leaf node (or nodes) being the finish. This is packed into a visual 2D form that is intended to make it difficult to see the path from root to leaf. (Note: if the maze is not a tree, then there is at least one node that has two distinct paths to it, which can make it easier to solve the maze.)
So in essence solving a maze is equivalent to walking a tree structure, and the algorithm presented here does indeed traverse a tree structure in a typical recursive fashion.
I’m not sure who will end up reading this post, so I don’t know what to expect in terms of knowledge of computers and programming, so I’ll cover the concept of recursion very briefly.
To recurse is for some operation to refer back to itself only with simpler information. The prototypical mathematical function that’s defined in terms of recursion is the fibonacci number:
fib(n) = fib(n-1) + fib(n-2) ; for n > 1
fib(0) = 1
fib(1) = 1
In some sense recursion breaks down a problem into a simpler one until the problem size gets to an easily solved (or defined) case.
If the goal of solving a maze is equivalent to finding a path from the root of a tree to a specific leaf node(s), then we just need to walk the tree keeping track of just how we got from root to leaf. There are two general ways of tree walking: breadth-first and depth-first. The basic difference between the two is that depth-first walking usually uses recursion and a parallel stack to keep track of the (current) path while breadth-first uses a queue to visit all the tree nodes.
As this post is implying that recursion is fun, it should be clear that for this problem that recursion was chosen as the way to solve a maze.
For our purposes, let’s say the maze is on a square grid of size n, meaning that there are n^2 “cells”. Let’s further say that it’s possible–subject to wall placement–to move only in one of four directions from a given cell: up, left, down, right. Walls between adjacent cells disallow a move between the cells, and a move from one side of the grid directly to the opposite side (via “wrapping around”) is never allowed.
Additionally, let’s say the start position is given as start and can be any cell on the edge, and finish is also any cell on the edge (things get more interesting if a few things are relaxed: if start and finish are not on the edges and the maze is not a tree, then I believe it might not be always solvable as the algorithm could end up going in circles).
Finally, let’s say that the maze maker did the proper job and the maze is actually solvable, meaning that there really is a path from the root of the tree to the finish leaf–if that’s not the case then there wouldn’t be a single tree that represents the problem.
With the above, we’re now ready to jump into the recursive algorithm…
As mentioned above, the idea of recursion is to break the problem down to a smaller problem and the apply the same algorithm to the smaller problem. In our case, the “smaller problem” is looking for a path that’s one move shorter than where we currently are in the maze–basically we’ll see if we can take a step in the right direction and then try to solve the maze from that just-stepped-into position in the maze.
Let’s assume (with good reason) that the current position (cell) in the maze that we’re currently at is a valid position, meaning very specifically that there’s a path from the start position to the current position. This is trivially true if we start at the start position–there’s a path (of zero steps) from the start to there. From that point on, if we only take valid steps then there will still be a path from the start to the current position. So we just have to define what a valid move is, which is pretty easy: a valid move is one that meets the following conditions:
The final point is to check if we’ve actually solved the maze–that we’ve reached the finish. This is equivalent to defining what fib(0)
and fib(1)
are above. So, if our current cell is the finish, then we’re done–we’ve found a path from start to finish.
With that in mind, the basic algorithm in sort-of-code for solve() is
solve(position)
IF at-finish(position)
RETURN true
visited[position] = true
IF ((can-move(position, UP) AND solve(move(position, UP)) OR
(can-move(position, RIGHT) AND solve(move(position, RIGHT)) OR
(can-move(position, DOWN AND solve(move(position, DOWN)) OR
(can-move(position, LEFT AND solve(move(position, LEFT)))
success = true
visited[position] = false
RETURN success
The core algorithm really is that simple–and it doesn’t matter what order the UP, RIGHT, DOWN, LEFT are done in, as long as all four directions are tried. I’ve left out the definition of “can-move” but it really just tests the three conditions above.
While it’s true that the algorithm to find a path from start to finish is as simple as what’s shown above, the path from start to finish is held only in the recursion info of which particular line of code above that’s doing the can-move(position, direction) AND solve(move(position, direction))
— each correct step (can-move
line) is encoded by which one of the four lines was chosen.
In order to be able to extract the path in a more convenient form, it’s necessary to use a stack, where a position is pushed to the top of the stack when it’s visited, and then removed from the top when we’re done visiting it. On success at finding the finish, we just don’t remove anything from the stack, and then the path is the whole set of cells from the stack.
Here’s some (verbose) Java code that puts all the above together (and that is a bit more generalized in that it can find all solutions):
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class Visitor
{
private int[][] visited;
private int size;
private Grid grid;
/**
* Non-static class so it has access to visited grid
*/
private class VisitContext
{
private List<List<Cell>> solutions = new ArrayList<>();
private Stack<Cell> path = new Stack<>();
private int depth;
private int maxDepth;
private Cell finish;
private boolean stopAtFirst;
VisitContext(boolean stopAtFirst)
{
this.stopAtFirst = stopAtFirst;
}
void enter(Cell pos)
{
path.push(pos);
depth++;
maxDepth = Math.max(depth, maxDepth);
visited[pos.getCol()][pos.getRow()]++;
}
void exit(Cell pos, boolean solved)
{
visited[pos.getCol()][pos.getRow()]--;
if (!solved)
pos = path.pop();
depth--;
}
boolean addSolution()
{
solutions.add(new ArrayList<Cell>(path));
return stopAtFirst;
}
boolean finished(Cell pos)
{
return ((pos.getCol() == finish.getCol()) &&
(pos.getRow() == finish.getRow()));
}
Stack<Cell> getPath()
{
return path;
}
int getMaxDepth()
{
return maxDepth;
}
}
/**
* Grid is what has the walls defined to make the maze
*/
public Visitor(Grid grid)
{
this.grid = grid;
size = grid.getSize();
visited = new int[size][size];
}
/**
* Find a path(s) from start to finish, returning true if a path was found
*/
public boolean findFinish(Cell start, Cell finish)
{
VisitContext context = new VisitContext(true);
boolean solved;
context.finish = finish;
solved = solve(start, DirVec.RIGHT, context);
if (!solved)
{
System.out.println("No solution found");
}
else
{
for (List<Cell> soln : context.solutions)
System.out.println("solution: " + soln);
grid.setPath(context.solutions.get(0));
}
return solved;
}
/**
* The recursive algorithm
*/
private boolean solve(Cell pos, DirVec dir, VisitContext ctx)
{
boolean solved = false;
if (ctx.finished(pos))
return ctx.addSolution();
ctx.enter(pos);
if (canMove(pos, dir) && solve(dir.apply(pos), dir, ctx))
solved = true;
else if (canMove(pos, (dir = dir.rotateLeft())) &&
solve(dir.apply(pos), dir, ctx))
solved = true;
else if (canMove(pos, (dir = dir.rotateLeft())) &&
solve(dir.apply(pos), dir, ctx))
solved = true;
else if (canMove(pos, (dir = dir.rotateLeft())) &&
solve(dir.apply(pos), dir, ctx))
solved = true;
ctx.exit(pos, solved);
return solved;
}
private boolean canMove(Cell pos, DirVec dir)
{
return (grid.canMove(pos.getCol(), pos.getRow(), dir.asBits()) && notVisited(dir.apply(pos)));
}
private boolean notVisited(Cell pos)
{
return visited[pos.getCol()][pos.getRow()] == 0;
}
}
January 20, 10:07pm
I thought I had mushrooms all figured out—I would eat breakfast and wait for 5 hours and make a tea from the 3.5g, thinking that a fuller digestive system and no actual mushroom tidbits would lessen the nausea. So I ate breakfast at 8am, waited until 12:30 to prepare tea by crushing it all and putting the powder in a tea bag and brewing it on a slow boil for five minutes with a little bit of lemon juice would work. I drank probably a bit over half at 1pm, thinking that I would drink the rest at 1:20 or 1:30. I was downstairs in the darkened family room, fireplace was going and meditation music was playing. By 1:30 it was clear that it would best that I not take any more. By that time my muscles were starting to tense and shake and I could feel that this wasn’t going to be like the first time with a very pleasant beginning.
I started by wanting to know more about my missing experiences in life, and wanted a non-dual experience. Instead I realized through the nausea and shaking that mind and body are not separable in any possible way, at least for me. I kept asking “why am I so different” meaning both with my reaction to mushrooms and why in general in the world—the lack of experiences, my fucking food intolerances, my body sensitivity, and all that. The music I picked this time was different—not the Native American flute as it was the first time but a more modern set that brought different images to my mind. More science fiction-y and fewer in general. I was aware of human faces or humans in my images but no interaction with any entity (again).
it was harder this time. and Helen wasn’t here. I went upstairs earlier than the first time and tried to eat a little, drink a little, anything to help relieve the nausea. I lay down by the french doors again and opened them to get the cold air on me, to make me shiver and quake, hoping that would help me. I was deep in the distortion and disassociation phase with no signs of letup on it. I alternated between going downstairs and lying down on the carpet, hands wide, thinking of the crucifixion, closing my eyes and letting images and sickness take me, and then going back upstairs to try to feel better.
The ends of the tree branches were animated and morphing; later they looked like they were enveloped in a cottony fog—my brain was pulling down the effect of the clouds to the trees. Dreamlike state as I walked through the house, going to the bathroom.
At one point the only thing I could do to keep myself going was to set a goal of going downstairs and turning off the fireplace. Then my next goal was to call Helen. She wasn’t able to talk long as she was in the car driving with Karen and their cousin Michael. Then I had a goal of telling S.—anyone, really—what the details were of the tea and the lemon juice and how much I had taken. S. offered to come up and I said yes. I was still on the floor by the back doors when he came in 15 minutes later. it was almost 5pm at that time—about 3.5 hours of mostly constant nausea and it wasn’t going away. I wanted to black out but I realized that the mushrooms just amplify whatever my mind was feeling and I knew that if I blacked out, if I let go, that i’d just experience the sick feeling even more, so I tried hard, so hard, to keep awake and to distract myself.
S. called a friend who said that alcohol stops a mushroom trip so I had some amaretto liqueur; I don’t know if it really does stop it as later searches strongly implied that people mixed them to good effect, but within 20-30 minutes I was feeling enough better to eat a bit more, which helped me feel even better.
By 6pm I was still experiencing some disassociation with space still being wrong but I was mostly over it. By 7pm I was just hungry and tired and feeling off but nothing like what I felt before.
Lessons learned: NEVER EVER do this again—my body is very sensitive to it in a negative way (2g tea not after fasting lasted 4+ hours, almost all of that time I felt quite sick), that body and mind can’t be separated, that music became reality (at the first of the trip) which made me think that generally what we focus on becomes reality for us.
January 21, 2019 7:50am
My sense of full reality hasn’t yet fully returned—dimensions are still off and I’m still a bit disassociated from things. Things are both familiar and not familiar. It could be from the ½ a pill I took last night to help me sleep.
I don’t mind this mild state too much—and I can see that feeling non-nauseous while experiencing this could be interesting but I expected it to be all over by this morning.