Maze solver robot with Arduino: build sheet and flood-fill code

A ₹2,300 maze solver with three IR wall sensors, two encoders and a flood-fill sketch, plus sensor calibration and a paper-maze test plan.

About the author · Sources, testing and AI policy

  • Parts₹2,300
  • Bench time6–8 h
  • LevelIntermediate
  • ReviewedOct 2026
Small two-wheel Arduino robot with three IR sensors driving on a paper maze

A ₹2,300 maze solver is a grid robot before it is a clever one: three IR wall sensors, two wheel encoders and a flood-fill map will beat a faster motor every time. The robot learns walls as it drives, keeps a 5×5 map and steps toward the neighbour with the shortest known distance to the goal. This sheet covers the left-hand rule, the flood-fill model, the pin map, sensor calibration, the sketch and a paper-maze test plan.

Scope: intermediate. No gyro, no diagonal moves, no PID. The maze is 5×5 with 200 mm cells and a centre goal. Encoders count wheel rotation, not ground truth, and every pivot adds heading error. Finish a paper maze before attempting a 16×16 course.

Bill of materials

PartQtyUnitTotal
Arduino Nano CH340 clone1₹320₹320
L298N motor driver module1₹180₹180
IR obstacle sensor modules, set of 31₹180₹180
TT gear motors with wheels, pair1₹260₹260
Magnetic encoder kit for TT motors, pair1₹380₹380
2WD acrylic chassis with caster1₹190₹190
2S 18650 holder with protected cells1₹420₹420
5 V 2 A buck converter1₹220₹220
Switch, jumper wires and screws1₹90₹90
Spare IR sensor module1₹60₹60
Parts total₹2,300

Scroll the table sideways to see every column.

Prices checked Oct 2026. Shipping and tools are not included.

The ₹2,300 total excludes the paper maze, which is a consumable. A buck converter gives the Nano and sensors their own 5 V rail; the L298N 5 V output stays disconnected. Prices are typical Indian retail in October 2026, before shipping.

Wall following vs maze solving

A wall follower has no map. It holds one rule, such as “keep the left hand on the wall”, and applies it at every opening. With two sensors, one facing forward and one left, the rule becomes a decision table: turn left when the left side opens, go straight when the front is clear, turn right when it is blocked. It is cheap, quick to write and solves a large class of mazes.

The left-hand rule fails on any maze with a free-standing wall. The wall creates a loop, and the robot can circle the island forever while the goal sits on the other side. A right-hand rule fails on the mirror image. Neither rule remembers where it has been, so neither can notice the loop.

A solver keeps state: the walls it has seen, a distance from every cell to the goal, and a move to the neighbour with the smaller number. When a new wall appears, the distances that used that opening are wrong, so the field is rebuilt. Flood fill is that rebuild: a breadth-first search outward from the goal, blocked by known walls.

A wall follower can be quicker on its first pass. A flood-fill robot may explore dead ends before it solves, but it can solve mazes the rule cannot, and once the map is complete it can run the shortest path.

Why flood fill needs a grid and encoders

Flood fill assigns a number to a cell, not to a point. The maze is divided into square cells and walls live on the lines between them. Each cell stores four wall bits, north, east, south and west, and a wall seen on one side is written to both neighbours because a wall has two faces.

Two hundred millimetres is the minimum for a small two-wheel robot: a 130 mm chassis needs roughly its own diagonal to pivot. Use 250 mm if you have floor space. The cell size also sets sensor range, because a side sensor must see a wall at half a cell without seeing through an opening into the next corridor.

Two side sensors and encoders are enough to map openings, but the centre sensor is what confirms a front wall at a dead end. Without it, the robot can mistake a wall it cannot see for a corridor and drive into it.

The robot also needs its cell and heading. Encoders give distance and stop it at a boundary instead of after a guessed delay; the same 400 ms is a different distance at 8.2 V and at 7.0 V. Dead reckoning still drifts when a wheel slips, so heading is kept in 90° steps and assumed exact. Test ten pivots before trusting a run.

Unknown walls are treated as open when the field is computed. That optimism is deliberate: a robot that assumed unseen walls could conclude no path exists.

Build order and power

  1. Assemble the chassis, motors, wheels and caster. Spin each wheel by hand.
  2. Fit the encoders. The left and right IR modules face sideways; the centre module faces forward.
  3. Wire the L298N to the motors and the Nano, following the pin map. Remove the ENA and ENB jumpers.
  4. Connect the 2S pack through the switch to the L298N +12V and the buck input. Set the buck to 5.0 V before connecting its output.
  5. Feed the Nano 5V pin and the IR modules from the buck. Connect every ground together.
  6. Add 100 µF across the motor supply. Unplug the battery while uploading.

The L298N drops 1.4–2 V, so a 7.4 V pack delivers about 5.5–6 V to the TT motors. The L298N guide covers the drop; the power guide explains why the logic rail should not share the motor regulator.

Pin map

ModuleArduino NanoNote
IR left OUTA0Digital input, not analog
IR centre OUTA1Front wall sensor
IR right OUTA2Digital input
Encoder left A / BD2 / D4A on the interrupt pin, B read in the ISR
Encoder right A / BD3 / D12A on the second interrupt pin
L298N IN1 / IN2D7 / D8Left motor direction
L298N IN3 / IN4D9 / D10Right motor direction
L298N ENA / ENBD6 / D11PWM speed; remove both jumpers
Buck 5 V5V pin and sensor VCCSet to 5.0 V first
All groundsCommon GNDPack, L298N, buck, Nano, sensors
The sensors sit on A0–A2 so that D2 and D3 stay free for the two encoder interrupts. The Nano has only those two external interrupt pins.

Sensor calibration

Most cheap IR obstacle modules have a potentiometer that sets the detection distance and an output that goes LOW when something reflective is in range. Mount all three at the same height, about 35–45 mm above the floor, and square to the wall they face. A module angled down sees the floor.

Calibrate with a ruler, not by driving. Hold a piece of the actual wall at 5, 10, 15 and 20 cm in front of each sensor and turn its pot until the LED changes state at about 12 cm. Record the distance for each module; they will not match. Repeat with a dark grey wall. If the sensor cannot see it at 12 cm, use light walls, because matte black returns very little infrared.

Do this under the lighting you will run in. Direct sunlight and some halogen lamps contain enough infrared to keep a sensor triggered. Print the three states and push the robot through a corridor by hand; the left and right states should change at the same point in the cell.

The flood-fill sketch

// Maze solver: Nano + L298N + 3 IR wall sensors + 2 encoders, 5 x 5 grid.
const int IR_L = A0, IR_C = A1, IR_R = A2;
const int IN1 = 7, IN2 = 8, IN3 = 9, IN4 = 10, ENA = 6, ENB = 11;
const int EL_A = 2, EL_B = 4, ER_A = 3, ER_B = 12;
const int W = 5, H = 5, GX = 2, GY = 2;
const int CELL_TICKS = 940, TURN_TICKS = 470;   // CALIBRATE on your robot
const int DRIVE_PWM = 150, PIVOT_PWM = 140;

uint8_t walls[W][H], dist[W][H];
uint8_t rx = 0, ry = 0, heading = 0;            // 0 N, 1 E, 2 S, 3 W
const int8_t dx[4] = {0, 1, 0, -1}, dy[4] = {1, 0, -1, 0};
volatile long ticksL = 0, ticksR = 0;

void leftISR()  { ticksL += digitalRead(EL_B) ? 1 : -1; }
void rightISR() { ticksR += digitalRead(ER_B) ? 1 : -1; }

void setup() {
  Serial.begin(9600);
  pinMode(IR_L, INPUT); pinMode(IR_C, INPUT); pinMode(IR_R, INPUT);
  for (int p = 6; p <= 11; p++) pinMode(p, OUTPUT);
  for (int p = 2; p <= 4; p++) pinMode(p, INPUT_PULLUP);
  pinMode(ER_B, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(EL_A), leftISR, RISING);
  attachInterrupt(digitalPinToInterrupt(ER_A), rightISR, RISING);
  brake();
  floodFill();
  delay(2000);
}

void loop() {
  readWalls();
  floodFill();
  if (rx == GX && ry == GY) { brake(); while (true) {} }
  int d = bestDirection();
  if (d < 0) { brake(); return; }
  int diff = (d - heading + 4) & 3;
  if (diff == 1) pivot(true);
  else if (diff == 3) pivot(false);
  else if (diff == 2) { pivot(true); pivot(true); }
  moveCell();
  rx += dx[d]; ry += dy[d]; heading = d;
}

void setWall(uint8_t x, uint8_t y, uint8_t d) {
  walls[x][y] |= 1 << d;
  int nx = x + dx[d], ny = y + dy[d];
  if (nx >= 0 && nx < W && ny >= 0 && ny < H)
    walls[nx][ny] |= 1 << ((d + 2) & 3);
}

void readWalls() {
  if (digitalRead(IR_C) == LOW) setWall(rx, ry, heading);
  if (digitalRead(IR_L) == LOW) setWall(rx, ry, (heading + 3) & 3);
  if (digitalRead(IR_R) == LOW) setWall(rx, ry, (heading + 1) & 3);
}

void floodFill() {
  for (uint8_t x = 0; x < W; x++)
    for (uint8_t y = 0; y < H; y++) dist[x][y] = 255;
  uint8_t qx[W * H], qy[W * H], head = 0, tail = 0;
  dist[GX][GY] = 0; qx[tail] = GX; qy[tail] = GY; tail++;
  while (head < tail) {
    uint8_t x = qx[head], y = qy[head]; head++;
    for (uint8_t d = 0; d < 4; d++) {
      if (walls[x][y] & (1 << d)) continue;
      int nx = x + dx[d], ny = y + dy[d];
      if (nx < 0 || nx >= W || ny < 0 || ny >= H) continue;
      if (dist[nx][ny] > dist[x][y] + 1) {
        dist[nx][ny] = dist[x][y] + 1;
        qx[tail] = nx; qy[tail] = ny; tail++;
      }
    }
  }
}

int bestDirection() {
  int best = -1, bestCost = 9999;
  for (int d = 0; d < 4; d++) {
    if (walls[rx][ry] & (1 << d)) continue;
    int nx = rx + dx[d], ny = ry + dy[d];
    if (nx < 0 || nx >= W || ny < 0 || ny >= H) continue;
    int turn = (d - heading + 4) & 3;
    int cost = dist[nx][ny] * 4 + (turn == 0 ? 0 : (turn == 2 ? 3 : 1));
    if (cost < bestCost) { bestCost = cost; best = d; }
  }
  return best;
}

void pivot(bool right) {
  long l0 = ticksL, r0 = ticksR;
  while (labs(ticksL - l0) + labs(ticksR - r0) < 2 * TURN_TICKS) {
    digitalWrite(IN1, right); digitalWrite(IN2, !right);
    digitalWrite(IN3, !right); digitalWrite(IN4, right);
    analogWrite(ENA, PIVOT_PWM); analogWrite(ENB, PIVOT_PWM);
  }
  brake(); delay(150);
}

void moveCell() {
  long l0 = ticksL, r0 = ticksR;
  while (labs(ticksL - l0) + labs(ticksR - r0) < 2 * CELL_TICKS) {
    long l = labs(ticksL - l0), r = labs(ticksR - r0);
    int trim = (int)(l - r) / 4;
    digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW);
    digitalWrite(IN3, HIGH); digitalWrite(IN4, LOW);
    analogWrite(ENA, constrain(DRIVE_PWM - trim, 0, 255));
    analogWrite(ENB, constrain(DRIVE_PWM + trim, 0, 255));
  }
  brake(); delay(150);
}

void brake() {
  digitalWrite(IN1, LOW); digitalWrite(IN2, LOW);
  digitalWrite(IN3, LOW); digitalWrite(IN4, LOW);
  analogWrite(ENA, 0); analogWrite(ENB, 0);
}

The sketch reads walls at each cell centre, rebuilds the field, then turns and drives one cell. The turn penalty stops it zig-zagging between equal-distance cells; because distance is multiplied by four, the penalty never overrides a shorter path. If the printed coordinates jump by two, CELL_TICKS is too low or the interrupt is counting both edges.

Test plan on a paper maze

Build the maze from 3 mm foam board strips about 50 mm high, taped to chart paper. Draw a 5×5 grid at 200 mm spacing and leave the centre cell with one entrance. Keep the walls light and the floor matte; a glossy floor reflects infrared like a wall.

  1. Sensor truth table: hold the robot at the centre of three cells and print the states. Compare with the maze on paper. A mismatch is calibration, not code.
  2. Distance: drive 1 m along a marked line and read the tick count. Divide by five for a starting CELL_TICKS, then tune until five cells land within 10 mm of the mark.
  3. Pivots: command ten 90° turns and measure the final heading. Scale TURN_TICKS by the error.
  4. Corridor: start three cells from a dead end. The robot should stop in the last cell, see the wall and turn without touching the foam.
  5. Junction: start one cell before a T-junction. Confirm it records the side wall and takes the open branch.
  6. Full run: five runs from the same start. Record successes, collisions and time. A robot revisiting the same two cells has a heading error or a missed wall.

What usually goes wrong

SymptomLikely causeFix
Drives past the cell centreCELL_TICKS too high, or one encoder missing edgesRecalibrate over 1 m; check encoder pull-ups and A/B wiring
Turns are always shortTURN_TICKS taken from a listing, not measuredMeasure ten pivots and scale the constant
A wall appears in the map that is not thereSide sensor sees the floor or through an openingRaise the sensor, shorten its range, shade it from sunlight
Misses a dark wallToo little infrared reflectedUse light walls; do not lengthen the range until it sees through openings
Spins in place at a junctionBoth side sensors trigger, or walls are read too earlyRead walls after a settle delay at the centre
Nano resets when motors startLogic rail sagging or no common groundSeparate buck rail, common ground, 100 µF across 5 V
Solves once, fails on the next runWalls not cleared, or start pose not resetClear walls and distance in setup; reset rx, ry and heading

Upgrading for a competition

  • Better sensing: Sharp GP2Y0A41 analog or VL53L0X time-of-flight sensors return a distance, not a threshold. Both need their own calibration.
  • Heading: an MPU6050 gyro corrects accumulated pivot error. Fuse it with the encoders; it drifts too.
  • Speed: add a PI loop on wheel speed and ramp the motors. A faster robot needs more sensor reads per cell.
  • Bigger map: competition mazes are commonly 16×16 with 180 mm cells. Add diagonals only after the orthogonal version is reliable, and check the current rulebook.
  • Mechanics: a low chassis, O-ring tyres and a centred battery improve repeatability more than another tuning session.

After this build, the next problem is a speed run on a known map: shortest-path following with wall alignment, not exploration. The encoder interrupt guide covers the counting edge cases, the battery guide explains why a sagging pack changes CELL_TICKS, and the final-year project guide shows how to present the test log.

Sources and further reading

Last reviewed: October 2026. Reviewed against component datasheets and the Arduino reference; this build has not been bench-tested by this site. The sketch is a starting point, not a guaranteed competition entry.

The Build Note

One short email when something is worth your bench time.

A new build sheet, a part price that moved, or a mistake worth avoiding. No daily drip, no recycled news.

One short email when a build or a price is worth your time. Unsubscribe in one click.