Estoy contento de anunciar que al fin he terminado el videojuego que comencé hace casi un mes. Se trata del juego de la serpiente, viborita, snake o como lo conozcas; pero en Arduino usando una LCD.
Antes que nada te invito a ver la primera parte en donde todavía no terminaba el proyecto, el cual pausé porque no tenía los componentes necesarios, pero recientemente llegó mi pedido y pude terminar el proyecto.
Características de snake en Arduino
- Lleva contador de puntaje
- La serpiente puede ser movida con cualquier entrada, en este caso es un Joystick pero puede ser un botón, Bluetooth, etcétera
- Opción para colocar comida en el tablero, de manera aleatoria
- Si la serpiente come, se hace más larga
- La pantalla no parpadea
Lecturas recomendadas
Vamos a usar cosas simples, pero si eres un principiante te recomiendo leer los enlaces que dejo a continuación. Para empezar, vamos a generar números aleatorios con Arduino; esto servirá para colocar la comida de manera aleatoria.
En segundo lugar vamos a usar un Joystick conectado al Arduino.
Circuito
Es como combinar el circuito de LCD I2C con Arduino y Arduino con Joystick. Aquí está:

Hay que notar que no he conectado el botón del joystick, pues por ahora no tiene ningún uso.
Nota: he usado un Arduino MEGA pero en un UNO debería funcionar como un encanto.
Comida de la serpiente
Como lo dije, una de las mejoras o actualizaciones es que se coloca comida de manera aleatoria en el escenario.
Para dibujar la comida tenemos dos funciones; una de ella calcula las coordenadas y la otra la coloca sobre el escenario:
// Calcula coordenadas aleatorias para colocar la comida
void randomizarComida() {
  comidaX = random(0, ANCHURA_TABLERO);
  comidaY = random(0, ALTURA_TABLERO);
}
// Coloca la comida en el escenrio
void acomodarComida() {
  escenario[comidaY][comidaX] = 1;
}
Colisiones y puntaje
Le damos seguimiento a la posición de la comida y calculamos si la serpiente colisiona con la misma, es decir, sabemos si la serpiente come, valga la redundancia, comida (podemos imaginar que la serpiente es una anaconda y la comida es un ratón).
De igual modo ahora el puntaje se aumenta de manera correcta.
// Saber en dónde está la comida para, más tarde, saber si la serpiente colisiona
int comidaX, comidaY;
// El puntaje. Se desborda cuando llega a 32767, creo
int puntaje = 0;
bool colisionaConComida() {
  return serpiente[0].x == comidaX && serpiente[0].y == comidaY;
}
void loop() {
  // ...
  if (colisionaConComida()) {
    puntaje++;
    randomizarComida();
    agregarPedazo(0, 0);// De hecho la posición del pedazo no importa al momento de agregar
  }
  // ...
}
Dirección con el joystick
También tenemos una función que lee un joystick y devuelve la dirección hacia la que se debe dirigir la serpiente:
// Leer del joystick
int obtenerDireccion() {
  int valorX = analogRead(pinX),
      valorY = analogRead(pinY);
  if (valorX > 1000) {
    return DIRECCION_ARRIBA;
  } else if (valorX < 200) {
    return DIRECCION_ABAJO;
  }
  if (valorY > 1000) {
    return DIRECCION_DERECHA;
  } else if (valorY < 200) {
    return DIRECCION_IZQUIERDA;
  }
  // Regresamos algo inválido, pues no se cambió,
  // y dependemos de la función cambiarDireccion
  return -1;
}
Evitar parpadeo de la pantalla
Me di cuenta de que no era necesario invocar al método clear de la pantalla, pues en cada momento se estaba dibujando el nuevo valor; eso redujo que la pantalla parpadee y sea desagradable a la vista.
Snake sobre Arduino - terminado
Finalmente dejaré la prueba de que todo funciona. Primero, el código:
/*
    Programado por Parzibyte
    14 de febrero de 2020
   ____          _____               _ _           _
  |  _ \        |  __ \             (_) |         | |
  | |_) |_   _  | |__) |_ _ _ __ _____| |__  _   _| |_ ___
  |  _ <| | | | |  ___/ _` | '__|_  / | '_ \| | | | __/ _ \
  | |_) | |_| | | |  | (_| | |   / /| | |_) | |_| | ||  __/
  |____/ \__, | |_|   \__,_|_|  /___|_|_.__/ \__, |\__\___|
         __/ |                               __/ |
        |___/                               |___/
    Blog:       https://parzibyte.me/blog/
    Ayuda:      https://parzibyte.me/#contacto
    Contacto:   https://parzibyte.me/#contacto/
*/
#include <LiquidCrystal_I2C.h>
#define ANCHURA_LCD 16
#define ALTURA_LCD 2
#define DIRECCION_LCD 0x3F // Si no sabes la dirección, visita [https://parzibyte.me/blog/posts/obtener-direccion-modulo-i2c-lcd-arduino/](https://parzibyte.me/blog/posts/obtener-direccion-modulo-i2c-lcd-arduino/)
#define ALTURA_TABLERO 16
#define ANCHURA_TABLERO 20
#define MAXIMA_LONGITUD_SERPIENTE (ALTURA_TABLERO * ANCHURA_TABLERO)
#define DIRECCION_DERECHA 0
#define DIRECCION_IZQUIERDA 1
#define DIRECCION_ARRIBA 2
#define DIRECCION_ABAJO 3
// Para leer el Joystick
const int pinX = 0,
          pinY = 1;
LiquidCrystal_I2C pantalla(DIRECCION_LCD, ANCHURA_LCD, ALTURA_LCD);
int escenario[ALTURA_TABLERO][ANCHURA_TABLERO] = {
  {0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0, /*  */0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0,},
  {0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0, /*  */0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0,},
  {0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0, /*  */0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0,},
  {0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0, /*  */0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0,},
  {0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0, /*  */0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0,},
  {0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0, /*  */0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0,},
  {0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0, /*  */0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0,},
  {0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0, /*  */0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0,},
  /***************************************************************************/
  {0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0, /*  */0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0,},
  {0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0, /*  */0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0,},
  {0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0, /*  */0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0,},
  {0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0, /*  */0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0,},
  {0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0, /*  */0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0,},
  {0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0, /*  */0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0,},
  {0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0, /*  */0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0,},
  {0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0, /*  */0, 0, 0, 0, 0,/*  */0, 0, 0, 0, 0,},
};
class PedazoSerpiente {
  public:
    int x, y;
    PedazoSerpiente(int a, int b) {
      x = a; y = b;
    }
    PedazoSerpiente() {
    }
};
PedazoSerpiente serpiente[MAXIMA_LONGITUD_SERPIENTE];
int longitudSerpiente = 0;
int direccion = DIRECCION_DERECHA;
// Saber en dónde está la comida para, más tarde, saber si la serpiente colisiona
int comidaX, comidaY;
// El puntaje. Se desborda cuando llega a 32767, creo
int puntaje = 0;
// Leer del joystick
int obtenerDireccion() {
  int valorX = analogRead(pinX),
      valorY = analogRead(pinY);
  if (valorX > 1000) {
    return DIRECCION_ARRIBA;
  } else if (valorX < 200) {
    return DIRECCION_ABAJO;
  }
  if (valorY > 1000) {
    return DIRECCION_DERECHA;
  } else if (valorY < 200) {
    return DIRECCION_IZQUIERDA;
  }
  // Regresamos algo inválido, pues no se cambió,
  // y dependemos de la función cambiarDireccion
  return -1;
}
void cambiarDireccion(int nuevaDireccion) {
  if (
    nuevaDireccion != DIRECCION_DERECHA
    && nuevaDireccion != DIRECCION_IZQUIERDA
    && nuevaDireccion != DIRECCION_ARRIBA
    && nuevaDireccion != DIRECCION_ABAJO
  ) {
    return;
  }
  if (
    (nuevaDireccion == DIRECCION_DERECHA || nuevaDireccion == DIRECCION_IZQUIERDA)
    && (direccion == DIRECCION_DERECHA || direccion == DIRECCION_IZQUIERDA)
  ) return;
  if (
    (nuevaDireccion == DIRECCION_ARRIBA || nuevaDireccion == DIRECCION_ABAJO)
    && (direccion == DIRECCION_ARRIBA || direccion == DIRECCION_ABAJO)
  ) return;
  direccion = nuevaDireccion;
}
void agregarPedazo(int x, int y) {
  if (longitudSerpiente >= MAXIMA_LONGITUD_SERPIENTE) return;
  if (x + 1 >= ANCHURA_TABLERO || x < 0)return;
  if (y + 1 >= ALTURA_TABLERO || y < 0)return;
  serpiente[longitudSerpiente] = PedazoSerpiente(x, y);
  longitudSerpiente++;
}
void moverSerpiente() {
  for (int i = longitudSerpiente - 1; i >= 1; i--) {
    serpiente[i].x = serpiente[i - 1].x;
    serpiente[i].y = serpiente[i - 1].y;
  }
  switch (direccion) {
    case DIRECCION_DERECHA:
      if (serpiente[0].x + 1 >= ANCHURA_TABLERO)serpiente[0].x = 0;
      else serpiente[0].x++;
      break;
    case DIRECCION_IZQUIERDA:
      if (serpiente[0].x <= 0)serpiente[0].x = ANCHURA_TABLERO - 1;
      else serpiente[0].x--;
      break;
    case DIRECCION_ARRIBA:
      if (serpiente[0].y <= 0)serpiente[0].y = ALTURA_TABLERO - 1;
      else serpiente[0].y--;
      break;
    case DIRECCION_ABAJO:
      if (serpiente[0].y + 1 >= ALTURA_TABLERO)serpiente[0].y = 0;
      else serpiente[0].y++;
      break;
  }
}
void colocarSerpienteEnMatriz() {
  for (int i = 0; i < longitudSerpiente; i++) {
    int x = serpiente[i].y,
        y = serpiente[i].x;
    escenario[x][y] = 1;
  }
}
// Calcula coordenadas aleatorias para colocar la comida
void randomizarComida() {
  comidaX = random(0, ANCHURA_TABLERO);
  comidaY = random(0, ALTURA_TABLERO);
}
// Coloca la comid en el escenrio
void acomodarComida() {
  escenario[comidaY][comidaX] = 1;
}
void setup() {
  randomSeed(analogRead(0));
  pantalla.init();
  pantalla.backlight();
  for (int i = 0; i < 3; i++) {
    agregarPedazo(5, i);
  }
  randomizarComida();
  pantalla.setCursor(0, 0);
  pantalla.print("Snake");
  pantalla.setCursor(0, 1);
  pantalla.print("By Parzibyte");
  delay(700);
  pantalla.clear();
}
void dibujarPuntaje() {
  pantalla.setCursor(6, 0);
  pantalla.print("SCORE");
  pantalla.setCursor(6, 1);
  pantalla.print(puntaje);
}
void dibujarMatriz() {
  byte figura[8];
  int numeroFigura = 0;
  for (int cuadritoX = 0; cuadritoX < 4; cuadritoX++) {
    for (int cuadritoY = 0; cuadritoY < 2; cuadritoY++) {
      for (int x = 0; x < 8; x++) {
        int numero = 0;
        int indice = cuadritoY == 0 ? x : (x + 8);
        int inicio = cuadritoX * 5;
        // Quién te conoce math.pow
        if (escenario[indice][inicio + 0] == 1)
          numero += 16;
        if (escenario[indice][inicio + 1] == 1)
          numero += 8;
        if (escenario[indice][inicio + 2] == 1)
          numero += 4;
        if (escenario[indice][inicio + 3] == 1)
          numero += 2;
        if (escenario[indice][inicio + 4] == 1)
          numero += 1;
        figura[x] = numero;
      }
      pantalla.createChar(numeroFigura, figura);
      pantalla.setCursor(cuadritoX, cuadritoY); // X, Y
      pantalla.write(byte(numeroFigura));
      numeroFigura++;
    }
  }
}
void limpiarMatriz() {
  for (int y = 0; y < 16; y++) {
    for (int x = 0; x < 20; x++) {
      escenario[y][x] = 0;
    }
  }
}
bool colisionaConComida() {
  return serpiente[0].x == comidaX && serpiente[0].y == comidaY;
}
void loop() {
  limpiarMatriz();
  cambiarDireccion(obtenerDireccion());
  moverSerpiente();
  colocarSerpienteEnMatriz();
  acomodarComida();
  dibujarMatriz();
  if (colisionaConComida()) {
    puntaje++;
    randomizarComida();
    agregarPedazo(0, 0);// De hecho la posición del pedazo no importa al momento de agregar
  }
  dibujarPuntaje();
  delay(10);// Mientras menor sea, más rápido va la serpiente, pero menor sensibilidad tiene el joystick
}
Aquí la demostración de mí jugando:

Lo tengo de igual modo en vídeo:
Por cierto, mi mayor puntaje creo que fue 169:

PD: lo sé, mi pantalla está oxidada, le tendré más cuidado.
Conclusión
Dejo el enlace a mi repositorio de GitHub en donde actualizaré el código si lo hago algún día; pero por ahora estoy muy satisfecho con el resultado. No olvides leer la primera parte, pues explico detalles importantes.
De hecho nadie solicitó este juego y no fue un requisito de ningún modo, simplemente fue un capricho. Lo intenté hacer hace algún largo tiempo y no pude, pero al intentarlo de nuevo, lo logré y quedó estupendo.
Si quieres puedes leer más sobre Arduino o Electrónica en mi blog.