Juego de batalla naval con Arduino, LCD y comunicación serial Deja un comentario


Introducción

No hace mucho me fue encargado un pequeño proyecto en donde tenía que simular el juego de una batalla naval usando un Arduino, dos módulos USB a TTL, algunos leds controlados con un 74hc595 y una LCD con el módulo I2C.

Trataré de ser lo más específico posible y no omitir ningún detalle.

Recuerda que todo esto es modificable, y con un poco de tiempo puedes adaptarlo a muchos otros escenarios. Por ejemplo, puede que no necesites los leds, o que en lugar de USB a TTL utilices Bluetooth, etcétera. Todo queda en ti.

Funcionamiento

Los barcos los ubicaría el jugador 1 (con coordenadas) e irían apareciendo sobre la LCD. Cada que se ubicaba uno de estos, se encendía un led indicando que el barco “estaba vivo”. Después, el jugador 2 trataba de darles sin ver la pantalla, usando al igual que el jugador 1, la comunicación serial.

Si le daba a un barco, el led que indicaba su estado se apagaba. Pero por ejemplo, para los que ocupan muchos espacios (es decir, los barcos grandes como submarinos, portaaviones, etcétera) se les tenía que destruir por completo para que su led se apagase.

El usuario 2 tenía 18 intentos para derribar todos los barcos. Ganaba si los desaparecía antes de que sus intentos se acabasen.

Adicional a esto, un buzzer sonaba indicando éxito o error en cada disparo. Y dichos disparos se marcaban en la LCD.

Materiales

  • Arduino UNO, o cualquier equivalente. (Lo he probado sólo con MEGA y UNO)
  • Pantalla LCD de 20 x 4 (o de 16 x 2) – para dibujar el escenario
  • Módulo I2C para LCD – para ahorrar cables
  • 7 leds – para el estado de los barcos
  • 7 resistencias –  para dichos leds
  • 1 Circuito integrado 74hc595 – para controlar los leds
  • Un buzzer – para indicar error o éxito
  • 2 módulos USB a TTL – uno a la pc del jugador 1. El otro a la del jugador 2

Requisitos

Tienes que saber la dirección del módulo I2C para la LCD, y también debes tener instalada la librería. Para ello, y para no hacer largo este post, separé esto en otros tutoriales:

Una vez que hayas leído eso, podemos continuar.

Paso 1: El escenario

El juego en sí no es complicado. Básicamente es un arreglo en donde estaremos cambiando los valores. Me decidí por un arreglo de tipo char, porque se me hace ligero y a su vez perfecto para los usos que le daremos.

Para comenzar vamos a definir la altura y anchura (o como se diga) del escenario; es decir, del arreglo, que debe medir lo mismo que nuestra LCD.

Si nuestra LCD es de 16×2, el arreglo será de 16×2. Si es de 20×4, el arreglo será de 20 x 4.

De todas maneras, haremos modificables las medidas. Así, primero podemos probar con una LCD pequeña, la de 16×2, que es la más popular. Y después, cambiar las constantes y conectar una de 20 x 4 u otras medidas.

Pero dejemos de hablar y veamos el código del escenario y las constantes que usaremos para pintar el arreglo, ya que los valores que habrán dentro de éste serán unos, y los que mostraremos al usuario final en la LCD serán otros:

Carguemos ese código a nuestra tarjeta, conectando a esta la LCD a través del módulo I2C. Veremos algo así:

¿Y eso es todo? claro que sí, no se ve a simple vista, pero el escenario ya está pintado. No se nota nada, porque declaramos la constante AGUA en un espacio en blanco.

Pero hagamos un experimento, cambiemos la constante AGUA y en lugar de ese espacio pongamos un punto (.), compilemos y veamos lo que sucede:

Si el lector no se ha dado cuenta, esto sirve de muchas cosas. En primer lugar ya vimos que gracias a las constantes podemos cambiar la forma en la que el usuario observa el juego.

Por otro lado, se ve la utilidad de la constante DEBERIA_IMPRIMIR_LETRAS, pues si está en true nos mostrará detalladamente el escenario, no sólo cosas transparentes.

Muy bien, con esto ya tenemos una pequeña parte. Ahora veremos cómo modificar ese arreglo, parsear coordenadas, encender leds, apagarlos, y finalmente cómo llamar a estos métodos escuchando el puerto Serial.

Paso 2: la ubicación de los barcos | coordenadas

Como lo dije arriba, haremos que esto sea flexible. Así que crearemos funciones que sin importar de dónde vengan los datos, funcionarán.

Para ello, declararemos una función para poner determinado barco en determinada coordenada. Como estamos trabajando realmente con un arreglo, las coordenadas son índices o posiciones del mismo. Y como estamos pintando exactamente ese arreglo en la LCD, sólo tenemos que actualizar el dato y dibujar. ¿Inteligente, no?

Pedir coordenadas de barcos

Comenzaremos pidiendo la ubicación de cada barco. No me gusta mucho esta parte del código, me habría gustado hacerla un poco más independiente del número de barcos.

Recordemos que el usuario introducirá las coordenadas a través de la comunicación serial, ya sea utilizando el Monitor serial que el IDE de Arduino provee, u otro.

Como dijimos que se utilizarán 2 comunicaciones seriales, tenemos que escuchar a una u otra. Escuchamos entonces a la del jugador uno. Mandamos un mensaje de bienvenida y comenzamos a pedir las ubicaciones:

Es importante notar la línea que dice:

while (!serialJugadorUno.available());

Sin ella, no habría podido esperar a que el usuario introdujera las coordenadas. Lo que hace ese fragmento es pausar el programa indefinidamente, hasta que haya algo que leer en el Serial. También están los métodos encenderLedDeesCoordenadaValida e intentarDibujarBarco, que veremos más abajo.

Comprobar coordenadas válidas

De este método me siento muy orgulloso, me gustó la manera en la que lo hice. Primero verifica si existe una coma en la cadena. En caso de que no, de una vez regresamos falso.

En caso de que sí, la cortamos. X sería lo que hay desde la posición 0 hasta en donde hayamos encontrado la coma. Y sería desde donde encontramos la coma, hasta el final de la cadena.

Acabo de darme cuenta de que cuando lo hice (que por cuestiones de tiempo no pude arreglar, jaja) había un error al parsear las coordenadas si ponía length – 1. Pero al poner length – 2, todo iba bien. Creo (y sólo creo) que es porque hay un salto de línea por ahí. Y si no, igual no importa.

Bueno bueno, luego comprobamos si la coordenada es un número, para ello utilicé una función que encontré en internet.

Finalmente, la última prueba es que las coordenadas estén en el rango de la LCD. Es decir, que X no sea menor que 0 ni mayor que la longitud de la pantalla. Y para Y lo mismo. ¿Ya vieron cómo las constantes que declaramos nos siguen ayudando?

Dibujar un barco

Este método también fue difícil de hacer. En las instrucciones no especificaba cómo debería ir el barco. Veamos un submarino, creo que ocupa 3 espacios. ¿Cómo los iba a ocupar? ¿Se iba a extender hacia la derecha, hacia arriba, abajo o a la izquierda?

Para eliminar todas estas dudas, hice una función que primero dibujara el barco de derecha a izquierda. Luego hacia abajo, izquierda y finalmente hacia arriba. Si no podía dibujarlo, entonces regresaba false, pues ya estaban ocupadas esas posiciones o el barco no cabía.

También es importante notar el método estaVacioEn que como su nombre lo dice, te indica si no has ocupado una coordenada.

Para dibujar:

Y el método para ver si está vacío:

Espero que el lector se dé cuenta de que las constantes siguen siendo de gran ayuda. Utilizamos de nuevo las que indican las medidas de la pantalla, y también la constante AGUA.

Paso 3: Encender y apagar LEDS

¿Ya dije difícil? bueno, este método también es difícil.

Encender un LED es una de las cosas más simples en el mundo de Arduino, de hecho es casi como el Hola mundo. La pequeña diferencia es que en este caso se tenía que utilizar un circuito integrado 74HC595, del cual ya he hablado.

Lo que tenía que estar modificando era un byte. Sí, un byte, de esos que tienen 8 bits. Y cada que encendía o apagaba un led, tenía que recorrerlo y utilizar bitSet para cambiar el valor de 0 a 1. Luego, llamaba a refrescarLeds para que por medio de shiftOut se escribiera el valor en el integrado.

Para encender determinado led:

Para apagarlo:

Y el que refresca los leds es:

Las constantes nos siguen ayudando. Y ya con esto se acaba el turno del jugador 1.

Paso 5: Alisten, Apunten, ¡Fuego!

Ahora va el jugador 2. Escuchamos la conexión serial 2, le pediremos igualmente una coordenada, reutilizando los métodos que vimos más arriba para validarlas y todo eso.

Comenzamos imprimiendo los intentos restantes. Dichos intentos son el resultado de  restar a una constante (que podemos aumentar o disminuir según sea el caso) el valor de una variable. Dicha variable son los intentos que el usuario ya ha hecho.

Si se agotan los intentos, pierde. Si no, pues no.

Vemos que hay una variable booleana para restar o no el intento. Esto es porque, si el usuario pone una coordenada mal, no se le resta intento. Tampoco si le dispara a un lugar en donde había disparado antes.

Encerramos todo en un ciclo infinito, que será roto únicamente cuando se acaben los intentos o se destruyan todos los barcos. Veamos ahora los métodos que no he explicado.

Comprobar a qué cosa se disparó

Para esto utilizamos el método disparar y obtener objetivo. El método es simple y explicativo. Simplemente te dice a qué lugar disparaste, y dependiendo de ello puedes marcar un disparo acertado, o no hacerlo. También ayuda a saber si se tiene que restar o no un intento.

Por cierto, este método es inseguro, ya que no comprueba que las coordenadas son válidas por sí mismo, sino que confía en que alguien más lo haya hecho por él. Y claro, lo hicimos nosotros antes de llamarlo.

Marcar e indicar disparos acertados o erróneos

Ya casi olvidaba que tenemos un buzzer que indica si acertamos o no. Para marcar el disparo, simplemente ponemos el valor de la constante en esa posición del arreglo. Igual por si fue erróneo.

Y hay otros dos métodos que sirven para hacer sonar al buzzer. Aquí el código de todo:

Comprobar si quedan partes de un barco

Tomando de nuevo como ejemplo al submarino, vemos que ocupa muchos espacios. Si le damos con un disparo, se hundirá una parte de él, pero no lo hemos vencido. Es decir, si ocupa 3 espacios y le damos a uno, quedarán 2 en donde seguirá vivo.

¿Y qué significa esto? que no podremos apagar el LED hasta que se haya hundido completamente. Y tampoco contará como barco derribado si no lo hundimos completamente.

Para ello, existe el método hayMasInstanciasDe, que como su nombre lo dice, comprueba si quedan todavía algunos (aunque sea “heridos”) espacios ocupados por determinado barco. Aquí el código:

Indicando victoria o fracaso

Ya para terminar todo este código, dependiendo de cómo haya jugado el jugador indicamos la victoria o su fracaso. El código es muy simple, sólo imprime mensajes para ambos jugadores.

Y el juego se queda así hasta que el Arduino se reinicia. Si quisiéramos que se repita una vez que pierde o gana, pondríamos lo que está en el setup dentro del loop.

Paso 6: código fuente completo

Para que no andes copiando y pegando parte por parte, aquí dejo el código que compone al Sketch. Recuerda que puede tener errores.

Paso 7: Circuito en fritzing

Hice el circuito para un Arduino UNO, pero se puede modificar fácilmente para cualquier otro modelo. El círculo negro a la derecha es el buzzer.

Circuito de batalla naval en Arduino, diseñado con Fritzing
Circuito de batalla naval en Arduino, diseñado con Fritzing

Conclusión

Hemos terminado. Si tuve errores en el código, o tienes algunas dudas, no dudes en comentar. Cabe mencionar que podemos utilizar una LCD de 16 x 2, o de 20 x 4, o como sea mientras se pueda controlar con el módulo I2C.

En caso de que cambiemos la medida, también tenemos que cambiar las constantes en el código fuente.


Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

0
    0
    Tu Carrito
    Tu carrito esta vacíoVolver a la tienda
    Enviar Whatsapp
    Hola 👋
    ¿En qué podemos ayudarte?