Introduction
Solving a maze is fun and will help you build your roboticist skills up- thinking about every instruction that Sparki needs to not just wander around the maze, but actually complete it. At this point, with the things that we have already learned in the previous lessons, we can take advantage of some of Sparki’s sensors to implement some maze solving programs. To understand this lesson, it’s strongly recommended that you have learned the concepts from the Line Following and the Moving the Robot lessons first. So, let’s help Sparki get through the maze!What You’ll Need
- A Sparki.
- A maze. What we will use here is the poster in ArcBotics’ Sparki Materials Packs. You can also make your own with a white cardboard as the base, using black insulating plastic tape to “print” the walls. You can also download a ready to print maze from this link (please note that this maze can be printed in standard plotters, since it’s 90 cm wide). If you’re really into mazes, you can download that maze’s source SVG file, and modify it using the Inkscape open source vector graphics editor (or any other SVG editor of your choice).
How It Works
One of the simplest ways of solving a maze with a robot is by using the Wall Follower algorithm, also know as the left-hand rule (or right-hand rule). Forget about the robot for a while, and suppose that you are a person inside a maze. Finding the exit could be done just by keeping one of your hands always touching a wall. And by “always”, we really mean always here. It might take you a while and you might wind up taking all the wrong paths in the maze before getting to the end but by keeping your left hand on the wall of the maze you wind up taking every single left hand turn (sometimes turning around completely) and, as long as you keep taking steps forward, eventually you’ll get to the end of the maze. Take a look to the following animation, where the red point represents the person: Do you see the trick? It’s important to stick with either the left hand rule or the right hand rule and never switch between them in the middle of the maze. One will get Sparki through the maze faster, but you’ll never know which one unless you’ve seen the maze before. Let’s think about this. What do we have on our Sparki to imitate this behavior in a printed maze? The line following infrared sensors! So, how do we implement the “Left Hand” algorithm with Sparki ? The first thing to do is to ensure that Sparki really understands the lines that he is “viewing” with the infrared sensors. And to do that, we will need to use all of our line and edge sensors. Let’s remember what sensors we have: Soon we’ll see how to use these infrared sensors in our maze solving problem. First, let’s talk a bit about geometry…Maze Geometry and Sparki
We can freely draw a maze and try to program Sparki to escape from it. But sooner or later we will find that not every robot can solve every maze. There are some shape and size constraints that we should think of when leaving a poor robot alone in a maze. Some are easier to spot, like the size of the robot to the maze. Suppose that your robot is too big (or too small) for the maze: The robot will not even be able to see some of the turns because they are underneath it! The sensors also won’t be in the right place in order to sense the walls. Sparki might sense some walls, but they might be a corridor or two over to the right or left since Sparki is so huge in comparison to the maze. The walls of a maze corridor for Sparki need to be just a little bit narrower than the space between Sparki’s two infrared edge sensors. (We’ll go over how Sparki will use these sensors to find the walls in just a moment.) But other constraints are not that obvious, and we may need to figure them out when we are programming the maze solver algorithm on a specific robot. Things like the number of sensors, the distances between them, and the distances from the sensors to the wheels centers may become really important in the maze solving activity. Here are the most important rules to follow if you are creating your own printed maze for this Sparki activity: 1. Line width: Sparki needs to be able to see the walls of the maze and not shoot right over them. This isn’t so important when Sparki is navigating the maze (if Sparki overshoots the walls you could just change your code a little to adjust), but it’s a nice reference for you as a maze builder. So our line’s width here will be 1/3 inch. You can make your walls thicker, but in order to make sure that Sparki’s sensors don’t accidentally pass over any walls make sure that your walls are at least 1/3 inch thick. 2. Width of corridors: Sparki should be able to turn when the wall line forms a corner in a way that all Sparki’s Line Sensors are kept over the white interior of the maze corridor. Sparki’s Edge Right Sensor and its Edge Left Sensors will sense the maze walls to make sure that Sparki keeps the maze walls between them. (In a real maze there would be walls, then you would need to use a different set of sensors to sense the walls, right? Which sensors would you use for that?) To ensure that Sparki has enough room inside of the maze, the maze wall lines have to always be a minimum of about 2.4 inches from each other: 3. 90 degrees corners and branches: As you have seen in all the previous maze pictures, all the corners and branches in the maze are 90 degrees. This is necessary to help Sparki find dead ends and branches. It also makes writing your first maze solving code a little bit easier. Once you feel comfortable with this activity feel free to draw more complicated mazes with different angle turns, it would be a great way to test your new found maze solving skills! Now we have a good idea of which kind of maze is Sparki going to solve, and which sensors Sparki will use. Also, we know that we are going to use the Wall Follower algorithm (or left-hand rule). So let’s see how to code Sparki to use this algorithm! Please remember to check that the batteries are properly connected (and charged!). We are going to use the motors here so please make sure that the On/Off Switch is set to on. Also, make sure you’re not working on a table since a fall could permanently damage your Sparki.The Main Idea
At a higher level, the Wall Follower algorithm consists of these four simple steps:- If you can turn left, do it.
- Else (if you can’t turn left), if you can continue going straight, just go straight.
- Else (if you can’t do either of the previous steps), if you can turn right, do it.
- If you reached a dead end, turn back by turning around (in either direction) 180 degrees.
Getting Sparki’s Bearings
So, our first problem is figuring out how to make Sparki go straight through a maze corridor. We need to make sure that Sparki knows where the wall on the left hand side of the corridor is and that Sparki travels along while keeping that wall on it’s left hand side. This sounds easy, but there’s actually a couple different ways to this and we have to use different sensors for checking the walls drawn on the maze than we would use if there were actual maze walls. (We’re going to use the infrared line and edge sensors, what sensors might Sparki use if there were real walls?) We’ll start with code that’s similar to the way we programmed Sparki to follow a line but we’ll change the code (a lot) so that it fits our needs. Programmers often do this, even though writing code from scratch is a much better way to teach yourself how to code, sometimes it’s nice to work with some code that already does something and then change it a little bit. So we will use that knowledge here, but adapt it to our specific problem. In the Line Following lesson, the robot followed a line that was between the Line Left Sensor and the Line Right Sensor. This time, the lines we want to check are on the outside of the Edge Right Sensor and Edge Left Sensor. The walls of our maze are just a little bit narrower than the width between Sparki’s edge sensors. We’ll assume that Sparki is going to start off kind of pointed straight down a maze corridor, but we’ll need to make sure that the code helps keep it that way. For this particular tutorial’s code you do need to place Sparki a little bit to the right of the center of the maze corridor. (If you’re good with code you’ll be able to easily change this code so that Sparki is ok with being placed to the left of the center of the maze corridor, or even to either side.) The first thing we want to do is create a “calibrating” function that helps Sparki find the wall on it’s left hand side and then orient itself so that it is pointing straight down the corridor. We won’t use this function a lot, but it’s one of the most important functions since we’ll use it to get Sparki pointed in the correct direction when we don’t really know where it is. Let’s think about the steps we want Sparki to take. (this is called writing pseudo-code and is a very important part of writing code) We’ll cover this code bit by bit but you can find the entire program at the end of each section, labeled “Current Code.” 1. Pivot left to find a line with the Edge Left Sensor.
1 |
sparki.moveLeft(1); //turn Sparki to the left a tiny bit |
1 2 3 4 5 6 7 |
#include <Sparki.h> // include the sparki library boolean foundLeftWall = false; boolean calibrated = false; boolean rightWall = false; void setup() |
1 2 3 4 5 6 7 8 9 |
if( foundLeftWall == false && calibrated == false ) //if Sparki hasn't found a left wall and hasn't been calibrated yet { sparki.moveLeft(1); //turn Sparki to the left a tiny bit if ( edgeLeft < threshold ) // if there is a line below left edge sensor { foundLeftWall = true; //Sparki found the left wall } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
if( foundLeftWall == true && calibrated == false ) //if Sparki found a wall but isn't calibrated yet { if( rightWall == false ) { sparki.motorRotate(MOTOR_LEFT, DIR_CCW, 15); //move forward while rotating to the right a little sparki.motorRotate(MOTOR_RIGHT, DIR_CW, 55); //move forward while rotating to the right a little } if( edgeLeft < threshold && edgeRight < threshold ) //if there is a wall beneath both sensors we're at -30 degrees from the wall { //this is also called a "known good" position because we know, no matter where Sparki started, where it is now in relation to the wall rightWall = true; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
void calibrateSparki() { rightWall = false; //variable so we know if Sparki found the right wall if( foundLeftWall == false && calibrated == false ) //if Sparki hasn't found a left wall and hasn't been calibrated yet { sparki.moveLeft(1); //turn Sparki to the left a tiny bit if ( edgeLeft < threshold ) // if there is a line below left edge sensor { foundLeftWall = true; //Sparki found the left wall } } if( foundLeftWall == true && calibrated == false ) //if Sparki found a wall but isn't calibrated yet { if( rightWall == false ) { sparki.motorRotate(MOTOR_LEFT, DIR_CCW, 15); //move forward while rotating to the right a little sparki.motorRotate(MOTOR_RIGHT, DIR_CW, 55); //move forward while rotating to the right a little } if( edgeLeft < threshold && edgeRight < threshold ) //if there is a wall beneath both sensors we're at -30 degrees from the wall { //this is also called a "known good" position because we know, no matter where Sparki started, where it is now in relation to the wall rightWall = true; //turn 28 degrees clockwise and scoot back close to the beginning sparki.moveForward(); delay(1650); //this variable got changed a little sparki.moveRight(30); //this variable got changed a little sparki.moveBackward(); delay(1000); sparki.motorStop(MOTOR_LEFT); sparki.motorStop(MOTOR_RIGHT); calibrated = true; //set calibrate flag to true so we exit calibrated code } } } |
1 2 |
edgeLeft = sparki.edgeLeft(); // measure the left IR sensor edgeRight = sparki.edgeRight(); // measure the left line IR sensor |
1 2 3 4 5 6 7 8 |
boolean rightWall = false; int edgeLeft; int edgeRight; int threshold; void setup() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
#include <Sparki.h> // include the sparki library //for sensors int edgeLeft; int edgeRight; int threshold; //for calibrating boolean foundLeftWall = false; boolean calibrated = false; boolean rightWall = false; void setup() { //indicates to the user that the program started: sparki.beep(440, 300); delay(300); sparki.beep(880, 500); Serial.begin(9600); } void loop() { threshold = 700; readSensors(); calibrateSparki(); //sparki.moveForward(); sparki.clearLCD(); // wipe the screen sparki.print("Edge Left: "); // show left line sensor on screen sparki.println(edgeLeft); sparki.print("Edge Right: "); // show left line sensor on screen sparki.println(edgeRight); sparki.updateLCD(); // display all of the information written to the screen } void readSensors() { edgeLeft = sparki.edgeLeft(); // measure the left IR sensor edgeRight = sparki.edgeRight(); // measure the left line IR sensor //int lineRight = sparki.lineRight(); // measure the right IR sensor } void calibrateSparki() { rightWall = false; //variable so we know if Sparki found the right wall if( foundLeftWall == false && calibrated == false ) //if Sparki hasn't found a left wall and hasn't been calibrated yet { sparki.moveLeft(1); //turn Sparki to the left a tiny bit if ( edgeLeft < threshold ) // if there is a line below left edge sensor { foundLeftWall = true; //Sparki found the left wall } } if( foundLeftWall == true && calibrated == false ) //if Sparki found a wall but isn't calibrated yet { if( rightWall == false ) { sparki.motorRotate(MOTOR_LEFT, DIR_CCW, 15); //move forward while rotating to the right a little sparki.motorRotate(MOTOR_RIGHT, DIR_CW, 55); //move forward while rotating to the right a little } if( edgeLeft < threshold && edgeRight < threshold ) //if there is a wall beneath both sensors we're at -30 degrees from the wall { //this is also called a "known good" position because we know, no matter where Sparki started, where it is now in relation to the wall rightWall = true; //turn 28 degrees clockwise and scoot back close to the beginning sparki.moveForward(); delay(1000); //this variable got changed a little sparki.moveRight(24); //this variable got changed a little sparki.moveBackward(); delay(1000); sparki.motorStop(MOTOR_LEFT); sparki.motorStop(MOTOR_RIGHT); calibrated = true; //set calibrate flag to true so we exit calibrated code } } } |
Going Straight
Next we need to create some code that makes Sparki move straight ahead down a corridor. After we get our function for moving straight ahead set up we’ll back up and create the code that tells Sparki when to move straight ahead, as well as some code to display what Sparki is doing on the LCD display. Ok. Pseudo-code time! There are a bunch of parts to this code-- Declaring some variables or “flags” to help Sparki go straight.
- Adding code to the sensor function to test to see if Sparki found a wall.
- Setting the variables to true or false.
- Adding an “if” statement that makes Sparki actually move in the loop.
- An “else” statement to stop Sparki if it didn’t find a wall.
1 2 3 4 5 6 7 |
int threshold; //for wall finding boolean foundLWall; //set false at the beginnning //for calibrating boolean foundLeftWall = false; |
1 2 3 4 5 6 7 8 9 10 |
void readSensors() { edgeLeft = sparki.edgeLeft(); edgeRight = sparki.edgeRight(); if(edgeLeft < threshold) { foundLWall = true; } } |
1 2 3 4 5 6 7 8 9 10 |
void readSensors() { foundLWall = false; edgeLeft = sparki.edgeLeft(); edgeRight = sparki.edgeRight(); if(edgeLeft < threshold) { foundLWall = true; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
readSensors(); if(calibrated == false) { calibrateSparki(); } else { if(foundLWall) { sparki.moveForward(); } else { sparki.moveStop(); sparki.println("Didn't find wall."); sparki.updateLCD(); } } } |
Sparki Lost the Wall or Came to a Branch in the Maze
Sometimes Sparki will lose the wall, so we really only want Sparki to move forward when there is a wall underneath the Edge Left Sensor. We already took care of that. But, if Sparki doesn’t detect a wall underneath the Edge Left Sensor then that may mean that there is a turn in the maze Sparki needs to take. (Remember, Sparki will always turn left if it can in our algorithm.) It’s time for… you guessed it, pseudo-code. Here are the steps we want to take for staying with the wall or finding the two different types of left hand turn that Sparki might take. (Remember, there’s a 90° left hand turn and a 180° left hand turn, these are different and will require different code for each.) We’ll put all of this code inside of a function called something like wallFinder( ). 1. Check the Edge Left Sensor, if there is no wall underneath it Sparki needs to turn off the straight( ) function and begin a wall finding function. 2. First, assume we just lost the wall somehow and try this pseudo-code- If there is no line underneath the Edge Left Sensor move forward and to the right a little trying to find the line again. If Sparki doesn’t find the wall again that way, back up exactly the same amount in the same direction to get back to where Sparki started searching and try the same thing again, only this time moving forward and to the left. If that doesn’t result in Sparki finding the wall then have Sparki move back to the starting position. If it did result in Sparki finding the wall, Sparki still needs to make sure it is pointed forward again so it doesn’t immediately lose the wall again. 3. So, Sparki didn’t find the wall, that means Sparki is probably at a left hand turn of some sort. To test for a 90° turn we’ll try this pseudo-code- Have Sparki move forward until the wheels are at the last place Sparki knows there was a wall. Now have Sparki turn to the left 90° and check the sensors to see if there is a wall underneath the Edge Left Sensor. 4. Still no wall! This means that Sparki is probably (hopefully) at a 180° turn. Sparki has already turned 90°, so Sparki just needs to turn another 90°, all the while looking for a wall. Hmmm. This pseudo-code sounds almost exactly like the code in section 3. We may be able to reuse some code for this…. 5. At this point we’ve exhausted all the possibilities we can think of for Sparki in a maze. (That’s not exactly true, what about a dead end in a maze? We’ll get to that later.) But often, between code errors and the unpredictability of the real world, situations like this will pop up. Always have a backup plan. There are a couple different options for Sparki now- Sparki can start its wall finding code all over again, it can do something like beep, or Sparki can just sit there waiting for a human to pick it up and help it out. Variables- Let’s think about the variables we’re going to need to achieve the pseudo-code above before launching into writing anything. We’re going to need a variable to keep track of how much Sparki has turned and which portion of our wallFinder code Sparki is currently using. We’ll call these variables wallFindType and turnCount. So, our first move is creating those variables-
1 2 3 4 5 6 7 8 9 10 |
int threshold; //for wall finding boolean foundLWall; int wallFindType = 0; int turnCount = 0; boolean foundWall; void setup(){ |
Also, our conditional is a little different. By using two ampersands (&&) we can check two conditions to see if they are true. Only if foundLWall is false and wallFindType is zero will the code inside of the “else if” code execute.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
void loop() { threshold = 700; readSensors(); if( calibrated == false ) { calibrateSparki(); } else { if( foundLWall ) { wallFindType = 0; sparki.moveForward(); sparki.println("Found wall."); } else if( foundLWall == false && wallFindType == 0 ) { sparki.moveStop(); sparki.println("Didn't find wall."); sparki.updateLCD(); wallFindType = 1; //now we enter the wall finding routine sparki.moveForward(.5); //move forward a little } if( wallFindType > 0 ){ wallFind(); } sparki.updateLCD(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void wallFind(){ switch(wallFindType){ case 1: //Sparki might just be off track a little, search around 5 degrees //our code in the case that wallFindType = 1 goes here break; case 2://we didn't just lose the wall, there was a turn, check for 90 degree turn //our code in the case that wallFindType = 2 goes here break; default: //the default case is here just in case wallFindType is equal to a value we didn't cover with our other cases break; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
void wallFind(){ switch(wallFindType){ case 1: //Sparki might just be off track a little, search around 5 degrees while( foundLWall == false && turnCount < 5 ) { //as long as we haven't found a wall or turned 10 degrees yet sparki.moveRight(1); turnCount ++; readSensors(); sparki.println("Searching right"); } if(turnCount >= 5) { sparki.moveLeft(5);//turn back to original position sparki.println("Done searching right"); } else{ //otherwise Sparki found a wall wallFindType = 0; turnCount = 0; return; } turnCount = 0; break; case 2://we didn't just lose the wall, there was a turn, check for 90 degree turn break; default: break; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
void wallFind(){ switch(wallFindType) { case 1: //Sparki might just be off track a little, search around 5 degrees while( foundLWall == false && turnCount < 5 ){ //as long as we haven't found a wall or turned 10 degrees yet sparki.moveRight(1); turnCount ++; readSensors(); sparki.println("Searching right"); } if( turnCount >= 5 ) { sparki.moveLeft(5);//turn back to original position sparki.println("Done searching right"); } else { //otherwise Sparki found a wall wallFindType = 0; turnCount = 0; return; } turnCount = 0; while( foundLWall == false && turnCount < 5 ) { sparki.moveLeft(1); turnCount ++; readSensors(); sparki.println("Searching left"); } if(turnCount >= 5) { sparki.moveRight(5);//turn back to original position wallFindType = 2;//move on to next wall finding type sparki.println("Done searching left"); } else {//otherwise Sparki found a wall sparki.println(turnCount); wallFindType = 0; turnCount = 0; return; } turnCount = 0; break; case 2://we didn't just lose the wall, there was a turn, check for 90 degree turn break; default: break; } } |
1 2 3 4 5 6 7 8 9 10 11 12 |
break; case 2://we didn't just lose the wall, there was a turn, check for 90 degree turn sparki.moveForward(7); sparki.moveLeft(90); sparki.moveStop(); sparki.println("Search Level 2"); wallFindType = 0; break; default: break; } |
1 2 3 4 5 6 |
int turnCount = 0; boolean foundWall; boolean foundDeadEnd; //the variable void setup(){ |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
void readSensors() { foundLWall = false; foundDeadEnd = false; edgeLeft = sparki.edgeLeft(); edgeRight = sparki.edgeRight(); lineCenter = sparki.lineCenter(); if( edgeLeft < threshold ) { foundLWall = true; } if( edgeLeft < threshold && lineCenter < threshold ) { foundLWall = false; foundDeadEnd = true; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
readSensors(); if( foundDeadEnd && wallFindType == 0 ) { sparki.moveRight(90); sparki.moveForward(); } else if( foundLWall ) { wallFindType = 0; sparki.moveForward(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
void loop(){ threshold = 700; readSensors(); if( calibrated == false ) { calibrateSparki(); } else { if( foundDeadEnd && wallFindType == 0 ) { sparki.moveRight(90); sparki.moveBackward(4); //added this line of code sparki.moveForward(); } else if( foundLWall ) { wallFindType = 0; sparki.moveForward(); } else if( foundLWall == false && wallFindType == 0 ) { sparki.moveStop(); wallFindType = 1; //now we enter the wall finding routine sparki.moveForward(.5); } if( wallFindType > 0 ) { wallFind(); } } } |
Here’s the final maze solving code for download-
Congratulations! Your Sparki is now capable of spectacular maze breakaways.