← Índice del curso

Robótica y Automatización · Tema 34

Seguimiento de pared y exploración de laberintos

Un pasillo o un laberinto no tiene una línea pintada: tiene paredes. Seguir una pared a distancia constante es el mismo problema de control del seguidor de línea, con un sensor distinto y sus propias trampas. Después viene la pregunta de fondo: ¿cómo decide un robot qué camino tomar en cada intersección?

01 · Punto de partida

Moverse entre paredes

En los temas 30 y 31 el robot seguía una línea. En los temas 32 y 33 aprendió a evitar obstáculos. Muchos entornos reales no se parecen a ninguno de los dos: un pasillo, un recinto con paredes, un laberinto. Allí lo natural es mantener una distancia constante a una pared y, en cada intersección, decidir por dónde seguir.

El tema tiene dos partes con lógicas distintas:

  • Control continuo de la distancia lateral, con realimentación (como en el tema 31).
  • Decisión discreta en las intersecciones: reglas simples que, bien elegidas, resuelven un laberinto sin mapa, y que fallan de maneras instructivas.

Recordá que todo esto es lazo cerrado sobre sensores: lo que el robot decide es tan bueno como lo que mide (tema 32).

02 · El lazo lateral

Seguir una pared

Supongamos la pared a la izquierda. El sensor mide la distancia d a ella y el objetivo es mantener dref. El error y la ley proporcional son:

e = d − dref    ω = K · e

Si el robot está demasiado lejos (e > 0), gira a la izquierda hacia la pared (ω > 0, convención del tema 20); si está demasiado cerca, se aleja. La velocidad de cada rueda sale de la cinemática inversa (tema 26). Es el mismo esquema del seguidor de línea, y los mismos problemas aparecen, con una diferencia importante.

El sensor perpendicular no amortigua. Si el sensor mira exactamente de costado, mide solo la distancia en el punto donde está el robot. Con el modelo del tema 31 (el «adelanto» es L = 0), el amortiguamiento es ζ = (L/2)·√(K/v) = 0: el lazo es un oscilador sin pérdidas. Con el retardo real de un motor, la oscilación crece hasta que el robot toca la pared. Ninguna ganancia lo arregla: cambiarla solo cambia la frecuencia.

La solución es darle al lazo información de anticipación. Dos formas:

  • Sensor inclinado hacia adelante, por ejemplo a 30°–45° del costado. «Ve» la pared más adelante: equivale a un adelanto L = dref·tan θs. Con dref = 20 cm y 40°, L ≈ 17 cm. Un robot que gira hacia la pared hace que el punto observado se acerque antes que el propio robot, y el control frena la corrección a tiempo.
  • Dos sensores laterales, uno adelante y otro atrás, separados Δx. Con las lecturas df y dt se calcula el ángulo respecto de la pared, α = atan((df − dt)/Δx), y la distancia perpendicular, d = ((df + dt)/2)·cos α. Se controla con ambas: ω = Kd·(d − dref) − Kα·α (con el signo según la pared).

Con el sensor inclinado a 40° el modelo da ζ ≈ 0,84 para K = 0,2 y el robot corrige un desvío de 10 cm en 1,5 s sin oscilar. A 10° el adelanto es de solo 3,5 cm: queda una oscilación que tarda más de 11 s en apagarse. Una ganancia demasiado baja también es lenta: con K = 0,05, 4,8 s. Cuanto más rápido avanza el robot, menos amortigua (tema 31).

Y un detalle práctico: un giroscopio (tema 29) puede sustituir al segundo sensor como medida del ángulo, o servir como «lazo interno» de rumbo.

03 · Cuando la pared cambia

Huecos y esquinas

Una pared no es siempre una recta continua. Tres situaciones rompen el control simple:

Casos de borde del seguimiento de pared.
SituaciónQué lee el sensor lateralQué hacer
Hueco (una puerta, una abertura)El alcance máximo, o ningún eco: un salto bruscoNo creer en el dato: conservar el rumbo (giroscopio) hasta que reaparezca la pared
Esquina externa (la pared termina y dobla hacia el lado de la pared)Lo mismo que un hueco, pero que no terminaSi no reaparece tras avanzar lo necesario, avanzar hasta pasar la esquina y girar 90° hacia donde estaba la pared
Esquina interna (la pared de enfrente)El sensor frontal baja del umbralDetener el avance y girar 90° alejándose de la pared que se seguía

El hueco es el caso más traicionador. Un control proporcional que cree el dato ve un error enorme (150 − 20 = 130 cm), pide el giro máximo hacia la pared que no está y cuando termina el hueco el robot ya apunta contra ella. En la simulación, un hueco de 30 cm hace que el robot choque con la pared. La defensa es una prueba de plausibilidad: un error que supera lo que el robot puede haberse desviado en un ciclo (por ejemplo, 15 cm sobre la referencia) se descarta, y mientras tanto se conserva el rumbo con el giroscopio, que no necesita pared. Con esa defensa, el robot atraviesa el hueco a 20 cm de la pared.

Las esquinas requieren una pequeña máquina de estados: SEGUIR_PARED, GIRO_INTERNO (pared al frente) y ESQUINA_EXTERNA (la pared se perdió y no vuelve), que avanza una distancia fija medida con odometría para que las ruedas pasen la esquina y luego gira. Es la misma arquitectura en capas del tema 33 aplicada a una tarea nueva.

04 · Pensar en celdas

Del pasillo a las celdas

Un laberinto de pasillos de ancho constante se puede ver como una grilla de celdas: cada una puede tener una pared en cada uno de sus cuatro lados (norte, este, sur, oeste). El robot avanza de celda en celda; en cada celda observa tres cosas: ¿hay pared a la izquierda, al frente y a la derecha? La grilla permite razonar con algoritmos simples, y los sensores dan los tres bits de cada celda.

  • Moverse una celda: avanzar la distancia de una celda (odometría, tema 27) manteniéndose centrado con la pared lateral (sección anterior).
  • Girar: 90° a la izquierda, 90° a la derecha o 180°, con el giroscopio o la odometría (temas 28 y 29).
  • Leer las paredes: comparar las distancias izquierda, frontal y derecha con un umbral, con histéresis y filtro (tema 32).

El tamaño de celda de los laberintos de competencia de «micromouse» es de unos 18 cm; en uno educativo puede ser mayor. El robot debe caber con holgura y los sensores deben distinguir paredes a esa distancia. Con la grilla, la decisión en cada celda es discreta: izquierda, frente, derecha o media vuelta. Las reglas que siguen la resuelven.

05 · La regla más simple

La regla de la mano

La regla de la mano izquierda dice: «en cada celda, ir a la izquierda si se puede; si no, al frente; si no, a la derecha; si no, dar la vuelta». La de la mano derecha es la imagen en espejo. No necesita memoria del laberinto ni sensor de posición: solo los tres bits de la celda actual y la orientación.

Equivale a caminar con una mano apoyada en la pared. Funciona porque, si todas las paredes del laberinto están conectadas entre sí y con el exterior (un laberinto simplemente conexo, también llamado perfecto), recorrer el contorno de la pared recorre todo el laberinto y pasa tarde o temprano por la meta. Pero tiene dos fallas conocidas:

  • Islas. Si hay una pared suelta, no conectada con el exterior, y el robot empieza pegado a ella, la sigue por todo su contorno y vuelve al principio: un bucle. Nunca llega a una meta que no esté junto a esa pared.
  • Espacio abierto. Sin pared que tocar, la regla pura gira en círculos. Requiere empezar con una pared del lado de la mano.

Y no es óptima: en un laberinto perfecto de 6 × 6, con la misma salida y la misma meta, la mano izquierda hace 42 pasos y la derecha, 12. Depende de dónde estén la salida y la meta; ninguna es mejor en general. Detectar un bucle es sencillo: si el robot vuelve a una celda con la misma orientación, el comportamiento se repetirá para siempre.

06 · Con memoria

Explorar con marcas y retroceso

Cuando la regla de la mano no alcanza, se agrega memoria: una marca por cada celda visitada y una pila con el camino recorrido. El algoritmo (búsqueda en profundidad, o variante del método de Trémaux) es:

  1. Marcar la celda actual como visitada y apilarla.
  2. Si la meta está aquí, terminar.
  3. Probar los vecinos accesibles no visitados en un orden fijo (por ejemplo, izquierda, frente, derecha); ir al primero.
  4. Si no hay ninguno, retroceder: desapilar y volver a la celda anterior. Si la pila queda vacía, la meta no es alcanzable.

Dos garantías valiosas: si la meta es alcanzable, la encuentra aunque haya islas o bucles (nunca entra a una celda ya marcada, así que no puede ciclar), y si no lo es, lo reconoce y se detiene. A cambio, necesita memoria proporcional al tamaño del laberinto: un bit por celda y una pila (256 celdas de un laberinto de 16 × 16 caben en 32 bytes de marcas y 256 bytes de pila en un Arduino).

En un laberinto con una isla, con salida junto a ella, las manos izquierda y derecha entran en bucle; la exploración con marcas llega a la meta en 28 pasos. En un laberinto cuya meta está aislada por una pared, las dos manos recorren las 27 celdas alcanzables y repiten estados; la exploración recorre las mismas 27 celdas, retrocede por los callejones y concluye que no hay camino.

Una vez explorado, el robot puede construir un mapa y calcular el camino más corto (por ejemplo, con un algoritmo de inundación o de búsqueda en anchura) para una segunda carrera más rápida, que es lo que hacen los robots de competencia. Aquí nos quedamos con la primera etapa.

07 · Elegir el método

Comparación de los métodos

Resultados en laberintos de 6 × 6 celdas. «Pasos» cuenta movimientos de una celda.
MétodoMemoriaPerfectoCon islaMeta aislada
Mano izquierdaNingunaLlega, 42 pasosBucleBucle
Mano derechaNingunaLlega, 12 pasosBucle (gira en el espacio abierto)Bucle
Exploración con marcasUna marca por celda y una pilaLlega, 42 pasosLlega, 28 pasosSin solución, detectada

Observá el comportamiento ante lo imposible: las reglas de la mano nunca se enteran de que no hay solución; solo repiten. La exploración con marcas sí: termina con la pila vacía. Para un robot autónomo, saber que no hay camino es información valiosa.

Todos suponen que los sensores leen bien las paredes y que el robot se mueve exactamente de celda en celda. En la práctica, los errores de odometría se acumulan (tema 27): un robot de laberinto corrige su posición y su orientación aprovechando las propias paredes, centrándose entre ellas y alineándose contra una pared frontal.

08 · Código para la placa

Laberintos en Arduino

El sketch codifica tres laberintos de 6 × 6 celdas con un byte por celda (bits de las paredes) y ejecuta los tres algoritmos —mano izquierda, mano derecha y exploración con marcas— en cada uno, informando el resultado, los pasos y las celdas visitadas. Al final calcula la velocidad angular de la ley de seguimiento de pared con un caso de ejemplo. Es la lógica de decisión; en un robot real, la función libre() sale de los sensores y el movimiento de celda en celda, de la odometría.

// Algoritmos de laberinto sobre una grilla de 6 x 6 celdas (modelo en memoria, sin hardware).
// Cada celda guarda sus paredes en bits: 1 = norte, 2 = este, 4 = sur, 8 = oeste. Direcciones: 0 = N, 1 = E, 2 = S, 3 = O.
// En un robot real, libre() se reemplaza por la lectura de los sensores de distancia izquierdo, frontal y derecho.
const byte N = 6;
const int DF[4] = {-1, 0, 1, 0}, DC[4] = {0, 1, 0, -1};

struct Laberinto { const char *nombre; byte paredes[6][6]; byte f0, c0, d0, fm, cm; };
const Laberinto LAB[] = {
  {"perfecto", {{11, 11, 9, 5, 5, 3}, {10, 12, 4, 5, 7, 10}, {10, 9, 3, 9, 5, 2},
                {12, 6, 12, 6, 11, 10}, {11, 9, 1, 5, 6, 10}, {12, 6, 12, 5, 5, 6}}, 5, 0, 0, 0, 5},
  {"isla",     {{9, 1, 1, 1, 1, 3}, {8, 0, 2, 8, 0, 2}, {8, 0, 2, 8, 0, 2},
                {8, 0, 2, 8, 0, 2}, {8, 0, 0, 0, 0, 2}, {12, 4, 4, 4, 4, 6}}, 2, 2, 2, 5, 5},
  {"cerrado",  {{11, 11, 9, 5, 7, 15}, {10, 12, 4, 5, 7, 11}, {10, 9, 3, 9, 5, 2},
                {12, 6, 12, 6, 11, 10}, {11, 9, 1, 5, 6, 10}, {12, 6, 12, 5, 5, 6}}, 5, 0, 0, 0, 5}
};
const byte N_LAB = sizeof(LAB) / sizeof(LAB[0]);

bool libre(const Laberinto &L, int f, int c, int d) {            // ¿se puede pasar a la celda vecina en la dirección d?
  if (L.paredes[f][c] & (1 << d)) return false;
  const int nf = f + DF[d], nc = c + DC[d];
  return nf >= 0 && nf < N && nc >= 0 && nc < N;
}

// Regla de la mano izquierda (lado = -1) o derecha (lado = +1): prefiere ese lado, luego el frente, luego el otro lado, luego atrás.
const char *manoSigue(const Laberinto &L, int lado, int &pasos, int &celdas) {
  int f = L.f0, c = L.c0, d = L.d0;
  bool visto[6][6][4] = {}, pisada[6][6] = {};
  const int prefs[4] = {(lado < 0) ? 3 : 1, 0, (lado < 0) ? 1 : 3, 2};
  pasos = 0; celdas = 1; pisada[f][c] = true;
  while (!(f == L.fm && c == L.cm)) {
    if (visto[f][c][d]) return "bucle";                           // mismo estado (celda y orientación) por segunda vez
    visto[f][c][d] = true;
    for (byte i = 0; i < 4; i++) {
      const int nd = (d + prefs[i]) % 4;
      if (libre(L, f, c, nd)) { d = nd; f += DF[nd]; c += DC[nd]; break; }
    }
    pasos++;
    if (!pisada[f][c]) { pisada[f][c] = true; celdas++; }
    if (pasos > 500) return "limite";
  }
  return "llego";
}

// Exploración con marcas y retroceso (búsqueda en profundidad): nunca entra dos veces a una celda nueva.
const char *explorar(const Laberinto &L, int &pasos, int &celdas) {
  int f = L.f0, c = L.c0, d = L.d0;
  bool visitada[6][6] = {};
  byte pilaF[36], pilaC[36];
  int tope = 0;
  visitada[f][c] = true; pilaF[0] = f; pilaC[0] = c; tope = 1;
  pasos = 0; celdas = 1;
  while (!(f == L.fm && c == L.cm)) {
    int mov = -1;
    const int prefs[3] = {3, 0, 1};                                // izquierda, frente, derecha
    for (byte i = 0; i < 3 && mov < 0; i++) {
      const int nd = (d + prefs[i]) % 4;
      if (libre(L, f, c, nd) && !visitada[f + DF[nd]][c + DC[nd]]) mov = nd;
    }
    if (mov >= 0) {
      d = mov; f += DF[d]; c += DC[d];
      visitada[f][c] = true; celdas++;
      pilaF[tope] = f; pilaC[tope] = c; tope++;
    } else {
      tope--;
      if (tope <= 0) return "sin solucion";                        // volvió al inicio sin opciones
      const int pf = pilaF[tope - 1], pc = pilaC[tope - 1];
      for (byte k = 0; k < 4; k++) if (f + DF[k] == pf && c + DC[k] == pc) d = k;
      f = pf; c = pc;
    }
    pasos++;
  }
  return "llego";
}

// Seguimiento de pared (pared a la izquierda): rad/s a ordenar para llevar la distancia lateral a la referencia.
float omegaPared(float distLateral, float distRef, float k) {
  return constrain(k * (distLateral - distRef), -1.5, 1.5);       // más lejos de lo debido: girar hacia la pared
}

void setup() {
  Serial.begin(115200);
  Serial.println("Laberintos de 6 x 6 celdas:");
  for (byte i = 0; i < N_LAB; i++) {
    int pasos, celdas;
    const char *r;
    r = manoSigue(LAB[i], -1, pasos, celdas);
    Serial.print(LAB[i].nombre); Serial.print(" mano izquierda: "); Serial.print(r);
    Serial.print(" pasos="); Serial.print(pasos); Serial.print(" celdas="); Serial.println(celdas);
    r = manoSigue(LAB[i], +1, pasos, celdas);
    Serial.print(LAB[i].nombre); Serial.print(" mano derecha: "); Serial.print(r);
    Serial.print(" pasos="); Serial.print(pasos); Serial.print(" celdas="); Serial.println(celdas);
    r = explorar(LAB[i], pasos, celdas);
    Serial.print(LAB[i].nombre); Serial.print(" exploracion: "); Serial.print(r);
    Serial.print(" pasos="); Serial.print(pasos); Serial.print(" celdas="); Serial.println(celdas);
  }
  Serial.print("omega con la pared a 30 cm (ref 20, K=0,2): "); Serial.println(omegaPared(30, 20, 0.2), 2);
}

void loop() {}

La salida esperada coincide con la tabla de comparación: perfecto mano izquierda: llego pasos=42 celdas=28, perfecto mano derecha: llego pasos=12 celdas=13, perfecto exploracion: llego pasos=42 celdas=28; isla mano izquierda: bucle pasos=10 celdas=10, isla mano derecha: bucle pasos=4 celdas=4, isla exploracion: llego pasos=28 celdas=29; cerrado mano izquierda: bucle pasos=53 celdas=27, la derecha igual, y cerrado exploracion: sin solucion pasos=52 celdas=27. La última línea muestra 1.50 rad/s (el límite) para una pared a 30 cm con referencia de 20 cm.

Para llevarlo a un robot: leé los tres bits de pared con los sensores de distancia y sus umbrales con histéresis; implementá «avanzar una celda» con odometría y centrado lateral, y «girar» con el giroscopio; agregá un modo de parada ante una lectura inconsistente o un tiempo excedido (temas 32 y 33). Probalo primero en un laberinto de cartón con celdas amplias y con las ruedas en el aire.

09 · Exploración interactiva

Laboratorios: pared y laberinto

1. Mantener la distancia a una pared

El robot avanza junto a una pared a su izquierda; la referencia es 20 cm y parte a una distancia inicial elegida. La curva es la distancia a la pared durante 12 s. Con el ángulo del sensor podés pasar de un sensor perpendicular (0°) a uno inclinado. Con «hueco» activo, falta la pared entre los 100 y los 130 cm de avance. Si la distancia baja de 6 cm, el robot toca la pared.

Distancia a la pared en el tiempoCurva de la distancia lateral a la pared durante 12 segundos, con la referencia de 20 centímetros y, si hay hueco, una franja que marca cuándo el sensor lo observa.20 cm6 cm45 cm0 s12 sTiempo
¿Toca la pared?—
Amortiguamiento del modelo—
Tiempo de asentamiento (±1 cm)—
Oscilación final—
Distancia mínima y máxima—

Elegí los parámetros.

Con los valores iniciales (K = 0,2, sensor a 40°), el robot corrige 10 cm en 1,5 s sin oscilar. Poné el sensor a 0° (con el retardo de 80 ms): la oscilación crece y toca la pared, con cualquier ganancia. A 10° el amortiguamiento es bajo y quedan oscilaciones de 1 cm que tardan más de 11 s en apagarse. A 20° se asienta en 3,8 s. Activá el hueco con distancia inicial de 20 cm: sin descartar lecturas el robot gira hacia la pared y la toca; con la protección atraviesa el hueco a 20 cm.

2. Resolver un laberinto

Elegí un laberinto de 6 × 6 celdas y un algoritmo. El círculo blanco es la salida y el cuadrado rojo, la meta. Las celdas lima son las que el robot pisó (más oscuras cuanto más veces) y la línea celeste su camino.

Laberinto de seis por seis celdas con el camino del robotGrilla con paredes en blanco, la salida marcada con un círculo, la meta con un cuadrado rojo y el recorrido del algoritmo elegido.
Resultado—
Pasos—
Celdas distintas pisadas—

Elegí un laberinto y un algoritmo.

Con el laberinto perfecto, la mano izquierda llega en 42 pasos, la derecha en 12 y la exploración en 42. En el laberinto con isla, la mano izquierda rodea la isla (bucle tras 10 pasos), la derecha gira en círculos en el espacio abierto (bucle tras 4) y la exploración llega en 28. Con la meta aislada, las manos repiten estados tras 53 pasos y la exploración concluye «sin solución» tras 52 pasos y 27 celdas.

10 · Código ejecutable

Seguir y explorar en Python

Dos simulaciones en un archivo: el seguimiento de pared con sensor inclinado, retardo del motor, hueco y protección; y los tres algoritmos de laberinto sobre los tres laberintos, definidos como dibujos de caracteres que podés editar.

Probalo en el navegador. Python se descarga con Pyodide la primera vez y requiere conexión. El código es editable y no controla ningún robot.

import math

LABERINTOS = {
    "perfecto": ("""+--+--+--+--+--+--+
|  |  |           |
+  +  +  +--+--+  +
|  |           |  |
+  +--+--+--+--+  +
|  |     |        |
+  +  +  +  +--+  +
|     |     |  |  |
+--+--+--+--+  +  +
|  |           |  |
+  +  +  +--+--+  +
|     |           |
+--+--+--+--+--+--+""", (5, 0, 0), (0, 5)),
    "isla": ("""+--+--+--+--+--+--+
|                 |
+  +  +  +  +  +  +
|        |        |
+  +  +  +  +  +  +
|        |        |
+  +  +  +  +  +  +
|        |        |
+  +  +  +  +  +  +
|                 |
+  +  +  +  +  +  +
|                 |
+--+--+--+--+--+--+""", (2, 2, 2), (5, 5)),
}

def _cerrar(ascii_):
    filas = ascii_.split("\n")
    fila = list(filas[1]); fila[15] = "|"; filas[1] = "".join(fila)
    sep = list(filas[2]); sep[16] = sep[17] = "-"; filas[2] = "".join(sep)
    return "\n".join(filas)

LABERINTOS["cerrado"] = (_cerrar(LABERINTOS["perfecto"][0]), (5, 0, 0), (0, 5))

DR, DC = (-1, 0, 1, 0), (0, 1, 0, -1)

def leer(ascii_):
    filas = ascii_.split("\n")
    n = (len(filas) - 1) // 2
    paredes = {}
    for r in range(n):
        for c in range(n):
            norte = filas[2 * r][1 + 3 * c:3 + 3 * c] == "--"
            sur = filas[2 * r + 2][1 + 3 * c:3 + 3 * c] == "--"
            oeste = filas[2 * r + 1][3 * c] == "|"
            este = filas[2 * r + 1][3 * c + 3] == "|"
            paredes[(r, c)] = [norte, este, sur, oeste]
    return n, paredes

def libre(paredes, n, r, c, d):
    return not paredes[(r, c)][d] and 0 <= r + DR[d] < n and 0 <= c + DC[d] < n

def mano(paredes, n, inicio, meta, lado, limite=500):
    r, c, d = inicio
    visitas, vistos, camino = {}, set(), [(r, c)]
    giro_pref = (3, 0, 1, 2) if lado == "izq" else (1, 0, 3, 2)           # izquierda/derecha, frente, otro lado, atrás
    pasos = 0
    while (r, c) != meta:
        if (r, c, d) in vistos:
            return {"resultado": "bucle", "pasos": pasos, "celdas": len(set(camino)), "camino": camino}
        vistos.add((r, c, d))
        for t in giro_pref:
            nd = (d + t) % 4
            if libre(paredes, n, r, c, nd):
                d = nd; r += DR[nd]; c += DC[nd]; break
        pasos += 1; camino.append((r, c))
        if pasos > limite:
            return {"resultado": "limite", "pasos": pasos, "celdas": len(set(camino)), "camino": camino}
    return {"resultado": "llego", "pasos": pasos, "celdas": len(set(camino)), "camino": camino}

def dfs(paredes, n, inicio, meta):
    r, c, d = inicio
    visitado, pila, camino, pasos = {(r, c)}, [(r, c)], [(r, c)], 0
    while (r, c) != meta:
        movimiento = None
        for t in (3, 0, 1):                                                # izquierda, frente, derecha
            nd = (d + t) % 4
            if libre(paredes, n, r, c, nd) and (r + DR[nd], c + DC[nd]) not in visitado:
                movimiento = nd; break
        if movimiento is not None:
            d = movimiento; r += DR[d]; c += DC[d]
            visitado.add((r, c)); pila.append((r, c))
        else:
            pila.pop()
            if not pila:
                return {"resultado": "sin solucion", "pasos": pasos, "celdas": len(visitado), "camino": camino}
            pr, pc = pila[-1]
            d = [k for k in range(4) if (r + DR[k], c + DC[k]) == (pr, pc)][0]
            r, c = pr, pc
        pasos += 1; camino.append((r, c))
    return {"resultado": "llego", "pasos": pasos, "celdas": len(visitado), "camino": camino}


DT, CICLO, RANGO, W_MAX = 0.005, 4, 150.0, 1.5
HUECO = (100.0, 130.0)                       # tramo donde falta la pared (cm)

def simular(K, v=20.0, d_ref=20.0, ang_sensor=40.0, tau_m=0.08, d0=30.0, hueco=False, proteger=False, duracion=12.0):
    L = d_ref * math.tan(math.radians(ang_sensor))        # adelanto: dónde "mira" el sensor inclinado
    x, d, psi, w_real, w_cmd = 0.0, d0, 0.0, 0.0, 0.0
    serie, choco = [], False
    for k in range(round(duracion / DT)):
        if k % CICLO == 0:
            x_golpe = x + L
            hay_pared = not (hueco and HUECO[0] <= x_golpe < HUECO[1])
            medida = d - L * math.sin(psi) if hay_pared else RANGO
            e = medida - d_ref
            if proteger and e > 15.0:
                w_cmd = -3.0 * psi                         # dato inverosímil: conservar el rumbo con el giroscopio
            else:
                w_cmd = max(-W_MAX, min(W_MAX, K * e))
        w_real += (w_cmd - w_real) * DT / tau_m if tau_m > 0 else (w_cmd - w_real)
        d -= v * math.sin(psi) * DT
        x += v * math.cos(psi) * DT
        psi += w_real * DT
        serie.append(d)
        if d < 6.0:
            choco = True
            break
    return serie, choco

def resumen(serie, choco, d_ref=20.0):
    cola = serie[-200:]
    media = sum(cola) / len(cola)
    asent = next((i for i in range(len(serie) - 1, -1, -1) if abs(serie[i] - d_ref) > 1.0), -1)
    return {"choco": choco, "min": min(serie), "max": max(serie), "asentamiento": (asent + 1) * DT,
            "final": media, "amplitud": max(abs(v - media) for v in cola)}



# --- Laberintos: tres algoritmos en tres laberintos ---
print("Laberintos de 6 x 6 celdas:")
for nombre, (ascii_, inicio, meta) in LABERINTOS.items():
    n, paredes = leer(ascii_)
    for alg, f in (("mano izquierda", lambda: mano(paredes, n, inicio, meta, "izq")),
                   ("mano derecha", lambda: mano(paredes, n, inicio, meta, "der")),
                   ("exploracion con marcas", lambda: dfs(paredes, n, inicio, meta))):
        r = f()
        print(f"  {nombre:9s} {alg:22s}: {r['resultado']:12s} pasos={r['pasos']:3d} celdas distintas={r['celdas']}")

# --- Seguimiento de pared: sensor perpendicular frente a sensor inclinado ---
print("Pared recta, desvio inicial de 10 cm (K en rad/s por cm):")
for K in (0.05, 0.2, 1.0):
    for ang in (0, 40):
        zeta = 20.0 * math.tan(math.radians(ang)) / 2 * math.sqrt(K / 20.0)
        r = resumen(*simular(K, ang_sensor=ang))
        print(f"  K={K:4.2f} sensor a {ang:2d} grados: zeta={zeta:4.2f}; choca={r['choco']}; asentamiento={r['asentamiento']:5.2f} s; amplitud final={r['amplitud']:5.2f} cm")

# --- Un hueco en la pared ---
print("Hueco de 30 cm en la pared (K=0,2, sensor a 40 grados):")
for proteger in (False, True):
    r = resumen(*simular(0.2, hueco=True, proteger=proteger, d0=20.0))
    print(f"  protegido={proteger!s:5s}: choca={r['choco']}; distancia minima={r['min']:.1f} cm; maxima={r['max']:.1f} cm")

En los laberintos: el perfecto se resuelve con la mano izquierda en 42 pasos, con la derecha en 12 y con exploración en 42; la isla da bucle para ambas manos (10 y 4 pasos) y 28 pasos con exploración; la meta aislada da bucle (53 pasos) con las manos y «sin solución» (52 pasos) con la exploración. En la pared: con el sensor a 0° el robot choca para K = 0,05, 0,2 y 1; a 40° no choca y se asienta en 4,8 s, 1,5 s y 2,0 s. Con un hueco, el robot sin protección choca (distancia mínima 6 cm) y con protección se mantiene a 20 cm.

Probá dibujar tu propio laberinto en el formato de caracteres (cada celda mide tres columnas) y cambiá el orden de preferencia de la exploración. Modificá el umbral de plausibilidad de 15 cm y mirá a partir de cuándo deja de proteger.

11 · Método y diagnóstico

Pruebas y diagnóstico

  1. Calibrá cada sensor de distancia contra una regla (tema 32) y comprobá que las lecturas izquierda y derecha no están invertidas.
  2. Con las ruedas en el aire, acercá una pared a cada sensor y verificá la dirección de la corrección.
  3. Probá una pared larga y recta: subí la ganancia hasta ver oscilación y bajala un tercio; repetí con la velocidad de trabajo.
  4. Probá un hueco, una esquina externa y una interna por separado; verificá que el robot atraviesa, gira y no choca.
  5. En un laberinto de cartón de pocas celdas, probá cada decisión (izquierda, frente, derecha, media vuelta) y el avance de una celda.
  6. Medí el error de posición y de orientación tras recorrer varias celdas y comprobá que el centrado lo corrige.
  7. Provocá una situación sin solución y verificá que el robot se detiene en vez de repetir.
«Con un sensor al costado y una buena ganancia alcanza»

Un sensor perpendicular no tiene amortiguamiento; con el retardo del motor oscila y termina tocando la pared. Hace falta un sensor inclinado, dos sensores o un giroscopio.

«Si el sensor no ve la pared, giro hacia donde estaba»

Un hueco no es un error de distancia: es ausencia de dato. Girar con una lectura inverosímil apunta el robot contra la pared. Conservá el rumbo hasta que el dato vuelva a ser creíble.

«La regla de la mano izquierda resuelve cualquier laberinto»

Solo los que tienen todas las paredes conectadas al exterior. Con una isla, entra en bucle; con la meta inalcanzable, nunca se entera.

«La mano derecha es la imagen de la izquierda: da la misma cantidad de pasos»

Es simétrica en el método, no en el resultado: depende de la salida y la meta. En nuestro laberinto, 42 pasos contra 12.

«Basta con detectar que repite una celda para saber que hay bucle»

Hay que repetir la celda con la misma orientación: una celda puede pisarse varias veces con orientaciones distintas sin que sea un bucle.

«La exploración con marcas es demasiado cara para un Arduino»

Un laberinto de 16 × 16 necesita 256 bits de marcas y una pila de 256 posiciones: unas decenas de bytes. Lo difícil es medir bien las paredes, no la memoria.

12 · Práctica guiada

Predecí antes de comprobar

1. Adelanto del sensor

La referencia es 25 cm y el sensor está a 45° del costado. ¿Cuál es el adelanto L y cuánto vale ζ con K = 0,1 rad/s por cm y v = 20 cm/s?

Ver solución

L = 25·tan 45° = 25 cm. ζ = (L/2)·√(K/v) = 12,5·√(0,005) = 12,5·0,0707 ≈ 0,88: bien amortiguado.

2. Dos sensores

Con la pared a la izquierda, el sensor delantero lee 22 cm, el trasero 18 cm y están separados 10 cm. ¿Qué ángulo respecto de la pared y qué distancia perpendicular?

Ver solución

α = atan((22 − 18)/10) = atan(0,4) ≈ 21,8°. d = ((22 + 18)/2)·cos 21,8° = 20·0,928 ≈ 18,6 cm. El frente más lejos que atrás indica que el robot apunta alejándose de la pared.

3. Un hueco

La pared desaparece y el sensor devuelve 150 cm; la referencia es 20 cm y K = 0,2. ¿Qué ordenaría el control sin protección, con un límite de 1,5 rad/s?

Ver solución

e = 130 cm y K·e = 26 rad/s, que se limita a 1,5 rad/s hacia la pared inexistente: un giro fuerte, errado. Con la protección, el error supera 15 cm, se descarta y se conserva el rumbo.

4. Orden de preferencia

En un cruce en T con aberturas a la izquierda y a la derecha, y pared al frente, ¿qué hace la mano izquierda? ¿Y en un callejón sin salida?

Ver solución

Mano izquierda: prefiere la izquierda, que está abierta: gira a la izquierda. En un callejón (pared a izquierda, frente y derecha) no queda más que dar la vuelta, 180°.

5. Por qué falla con una isla

Explicá por qué la mano izquierda entra en bucle en el laberinto con isla si la meta no está junto a ella.

Ver solución

La regla sigue siempre la pared que tiene a la izquierda, y la pared de la isla es una curva cerrada, no conectada con el exterior. El robot la recorre entera y vuelve a empezar; nunca se separa de ella.

6. ¿Qué algoritmo?

Tu robot debe resolver laberintos desconocidos que pueden tener islas y metas imposibles. ¿Cuál elegís y qué paga a cambio?

Ver solución

La exploración con marcas y retroceso: encuentra la meta si existe y reconoce que no existe. A cambio necesita memoria (una marca por celda y una pila) y medir su posición en la grilla con cierta fiabilidad.

7. Memoria

¿Cuántos bytes ocupan las marcas y la pila para un laberinto de 16 × 16 si se guarda una marca por bit y cada posición de la pila ocupa un byte?

Ver solución

Marcas: 256 bits = 32 bytes. Pila: hasta 256 posiciones = 256 bytes (se puede codificar fila y columna en un byte). Total unos 290 bytes, una fracción de los 2 KB de un UNO.

13 · Síntesis

Ideas para recordar

  • Seguir una pared es el mismo lazo del tema 31: error d − dref y ω = K·e.
  • Un sensor perpendicular no amortigua (ζ = 0): con retardo de motor oscila y choca. Se necesita un sensor inclinado, dos sensores o un giroscopio.
  • El adelanto de un sensor inclinado es L = dref·tan θs y el amortiguamiento ζ = (L/2)·√(K/v).
  • Un hueco es ausencia de dato, no un error de distancia: se descarta y se conserva el rumbo.
  • Las esquinas piden estados propios: giro interno con el sensor frontal y esquina externa con avance medido.
  • La regla de la mano izquierda o derecha resuelve laberintos con paredes conectadas, sin memoria; falla con islas y no detecta metas imposibles.
  • La exploración con marcas y retroceso encuentra la meta si existe y reconoce que no, a cambio de memoria.
  • Ningún método es óptimo en general: dependen de la salida, la meta y la forma del laberinto.

¿Listo para el tema 35?

Antes de avanzar, dibujá un laberinto con una isla y otro con la meta aislada, y seguí a mano cada algoritmo. Anotá qué tendrías que medir en el robot para poder usar la exploración con marcas.

En el próximo tema cambiamos de dominio: la automatización de procesos. Empezamos por qué es la automatización, con procesos, entradas y salidas, lógica secuencial y la comparación con un PLC.