Tablero de Ajedrez con Inteligencia Artificial Integrada
Les presentamos nuestro proyecto de ajedrez con inteligencia artificial integrada. En este trabajo, hemos combinado la esencia del ajedrez con las capacidades de la inteligencia artificial todo ello representado mediante una matriz de leds
Nuestro equipo, compuesto por Lucia, Beltran, Ángel y Javier, se ha dedicado a diseñar y desarrollar un tablero de ajedrez, este proceso ha sido un reto que ha conllevado varios problemas a los que les hemos encontrado soluciones así como dificultades de las que hemos aprendido y superado llegando hasta llegar a nuestra meta.
Para facilitar la interacción con el juego, hemos creado un display, que, junto a unos botones, permiten manejar las posiciones de manera intuitiva y fluida. Además, hemos implementado una matriz LED 16×16 que refleja las casillas de origen y destino de la IA, brindando una experiencia visual envolvente y dinámica.
Como hemos mencionado anteriormente, con este proyecto buscamos una solución para esas personas a las que les encanta el ajedrez, pero ya sea por disponibilidad o cualquier otra razón, no les es posible encontrar un adversario con el que jugar. Con este ajedrez podrán disfrutar de una buena partida siempre que quieran
MAQUETA
Comenzando con lo más visual de nuestro proyecto, hablaremos de la maqueta:
Podemos apreciar que nuestro ajedrez se compone en primer lugar de una caja que alberga los circuitos, el display, el Arduino y sobre la cual se sitúa nuestro tablero. Esta caja, fundamental para el funcionamiento y la estética del proyecto, la elaboramos detalladamente utilizando contrachapado con medidas por lado de 19,8 cm x 10 cm. Para su construcción, nos hemos valido de herramientas simples pero efectivas: un cúter para establecer las medidas con precisión, el adhesivo «No más clavos» para asegurar una unión sólida y duradera, y pequeños tacos de madera para brindar estabilidad y robustez a la estructura.
Una vez completada la construcción de la caja, con la ayuda de unos pequeños topes de madera, hemos logrado una base sólida sobre la cual introducir todos los componentes electrónicos necesarios, incluyendo el circuito, el display, los botones y el Arduino. Estratégicamente, hemos realizado un hueco en la pared frontal de la caja para permitir la salida del display y los botones, dando lugar así a un acceso cómodo y práctico durante el juego.
Como toque final y para proporcionar una apariencia elegante y funcional al ajedrez, hemos optado por utilizar metacrilato seccionado en casillas como tapa, sobre la cual hemos colocado bordes de madera donde hemos marcado las letras y números correspondientes. Este diseño no solo añade un elemento estético al conjunto, sino que también facilita la identificación de las casillas durante el juego.
En resumen, mediante este proceso de diseño y fabricación, hemos logrado crear un ajedrez que no solo cumple con los requisitos funcionales y técnicos, sino que también destaca por su comodidad, estética y practicidad en cada partida.
BOTONES Y DISPLAY
A continuación, vamos a detallar de la forma más breve posible el código empleado en nuestro proyecto. Para facilitar la explicación dividiremos el código en tres partes, teniendo en cuenta el hardware que manejan, más el setup; sin embargo, habrá funciones que empleen elementos correspondientes a más de una parte:
1. Setup: En primer lugar, hay que incluir librerías y definir los pines y las variables globales que emplearemos en nuestro programa. Como podemos observar a simple vista, hay una gran cantidad de variables y datos definidos; la mayoría de los cuales pertenecen a la IA empleada como oponente de nuestro ajedrez. Procederemos a explicar sólo los elementos más relevantes.
Librerías: Empleamos LiquidCrystal para el manejo del display y Adafruit_NeoPixel para el manejo de la matriz LED.
Variables:
- IA: La variable que nosotros hemos introducido para poder manejar la IA a través de nuestro hardware es inputString, un String donde se almacenará el movimiento a realizar y que además emplearemos como herramienta de control para saber si se deben de seguir introduciendo datos para finalizar el movimiento, o si por el contrario este ya está completo. Además, también emplearemos lastM y lastH que representan el último movimiento realizado por la IA y el humano, respectivamente.
- LCD: Objeto para poder manejar e inicializar el display, de 16 columnas por 2 filas.
- Matriz: Objeto para poder manejar e inicializar la matriz LED de 16×16.
- Botones: Aquí hemos creado 3 variables globales de tipo entero, así como una de tipo char. Las variables de tipo entero consisten en posición, para llevar la cuenta de la última posición de inputString rellenada y que además nos indica la posición del cursor en el display; dígito, para poder guardar en inputString los dígitos del movimiento a realizar; y contador, variable la cual incrementará según se vayan pulsando los botones y que representará el dígito o caracter a introducir. La variable tipo char es letra, análoga a la variable dígito.
Pines: Reservamos los pines digitales 2, 4 y 7 para los botones, los pines digitales 8, 9, 10, 11, 12 y 13 para el display y el pin digital 0 para la matriz LED.
2. Botones y display: En esta sección encontramos una función principal, read_movement(). Esta función detecta la pulsación de los botones y realiza la acción pertinente a dicho botón pulsado. Además se encuentra en el programa principal (loop()) y se ejecuta en bucle mientras inputString no esté completo.
Cada botón, a los que llamaremos botón1, botón4 y botónSig, tiene una o más acciones posibles, que serán visibles en el display, y se decidirá cual ejecutar según el estado de inputString (recordemos que controlamos su estado a través de la variable posición).
- En primer lugar, botón1 tiene 3 posibles acciones: si la variable posición es par, representa que se desea introducir una letra para realizar el movimiento, por lo que cada pulsación de dicho botón iterará de una en una de manera ascendente las letras de ‘a’ a ‘h’. Después de llegar a ‘h’ y seguir iterando, se volverá al inicio ‘a’, lo cual será controlado mediante la función topeDigito().
De igual manera, si posición es impar se realizará el mismo procedimiento, pero con los números del 1 al 8.
Por último, el caso especial donde posición es igual a 4, el cual representa que se han introducido todos los datos necesarios para realizar el movimiento. Si se desea confirmar el movimiento introducido (en caso de haber cometido un error) se pulsará este botón y el movimiento quedará aceptado, a la espera de comprobar si es válido. En este caso especial se termina de completar inputString por lo que se cumple la función que finaliza el bucle donde se encontraba read_movement().
- Análogamente, botón4 tiene 3 posibles acciones análogas a botón1, con la diferencia que se itera de 4 en 4 en vez de 1 en 1, controlado por la función topeDigito4().
Para el caso donde posición es igual a 4, si se desea rechazar el movimiento (pues se ha introducido el movimiento con alguna errata) se pulsará este botón, lo cual provocará que inputString sea borrado y posición vuelva a 0, mediante la llamada a la función anulado(), y por tanto no se finalice el bucle de donde se encuentra read_movement().
- Por último, botónSig tiene 1 función: mientras posición sea distinto de 4, es decir, no se hayan introducido aún todos los datos necesarios, cada vez que se pulse este botón servirá como confirmación de que el elemento del movimiento que estábamos introduciendo actualmente, sea dígito o letra, es correcto, se almacenará en inputString y pasaremos a introducir el siguiente elemento del movimiento. Para ello mediante topePosición() incrementamos en 1 el valor de la posición. Además, al igual que antes, si posición es par significa que se está introduciendo una letra y si es impar, una cifra.
Para finalizar, las funciones hay tres funciones que emplean la lógica de los botones y otra que además mezcla dicha lógica con la de la matriz LED y la IA; sin embargo no se activan al pulsar dichos botones. Son invalid(), se activa cuando un movimiento confirmado por el humano no es correcto; win(), cuando el humano gana la partida; y lose(), cuando el humano pierde la partida. Las tres condiciones muestran por el display información relevante.
.
Sin embargo, no hemos indicado en ningún momento alguna función que se encargue únicamente de la lógica del display. Esto se debe a que no hay ninguna, todas las acciones que realiza el display, las cuales son mostrar por pantalla la información relacionada con el movimiento, están embebidas en las funciones mencionadas anteriormente.
La gestión de la posición del cursor del display se realiza mediante la variable posición, y una vez introducida la posición del cursor se procede a imprimir el mensaje deseado mediante la función predefinida LCD.write().
INTELIGENCIA ARTIFICIAL INTEGRADA
Para el proyecto, hemos implementado un módulo de ajedrez que incorpora inteligencia artificial (IA) para enfrentarse al usuario en las partidas de ajedrez. Este módulo central de nuestra aplicación se basa en un código fuente obtenido de internet, que ha sido adaptado y modificado para cumplir con nuestros requisitos específicos.
La inteligencia artificial que hemos adoptado y modificado es conocida como «Arduino UNO MicroMax Chess», originalmente desarrollada por Diego Cueva. Esta versión está basada en «Micro-Max», versión 4.8, una creación de H.G. Muller. «Micro-Max» que es el programa y la inteligencia artificial de ajedrez más compacto disponible, compuesta por menos de 2000 caracteres y apenas 133 líneas de código. A pesar de su tamaño reducido, es capaz de ejecutar completamente el juego de ajedrez, respetando todas las reglas establecidas por la Federación Internacional de Ajedrez (FIDE).
La elección de «Micro-Max» para nuestro proyecto no fue casual, ya que su tamaño extremadamente compacto lo hace ideal para la memoria limitada de nuestra placa Arduino. Sin embargo, este tamaño también presenta desafíos, principalmente debido a la abstracción y condensación del código. Por ejemplo, el nombre de todas las funciones y variables se limita a un solo carácter, lo que dificulta la tarea de entender y modificar el código sin una documentación detallada y una comprensión profunda de sus mecánicas internas.
Las principales adaptaciones que hemos tenido que implementar en el código incluyen la integración de la captura de movimientos a través de botones físicos, la implementación de animaciones mediante LEDs para una interacción más visual, así como la representación de información y movimientos en un display conectado a nuestra unidad central. Hemos tenido que sustituir la manera en que tenía el módulo de hacer la tareas de recibir movimiento por una llamada a nuestra propia función, que recogiera la entrada por los botones y mostrase la información necesaria por la pantalla display. Después de cada movimiento hecho por la inteligencia artificial hemos añadido la llamada a otra función que imprimiera en la pantalla display y en la matriz LED el movimiento realizado. Además, hemos localizado los puntos del código en los que se identificaba que se había realizado un movimiento inválido, se había ganado o se había perdido la partida, e insertado la llamada a las funciones que mostrasen la animación pertinente a cada una de ellas.
MATRIZ DE LEDS
Para la gestión de la placa de los leds hemos utilizado diferentes funciones.
Para empezar y como introducción, partíamos de una placa de leds de 16 x 16 por lo que cada casilla equivale a 4 leds (lo planteamos así para que fuera más visual, ya que utilizando sólo un LED por casilla el tablero sería demasiado pequeño).
En principio, la placa de LEDS podemos dividir en dos grandes procesos.
- El encendido y apagado de la casilla origen y destino: los movimientos de la IA se transfieren encendiendo al mismo tiempo tanto la casilla donde está la pieza que quiere mover; como la casilla a la que quiere mover dicha pieza.
Las casillas se quedarán encendidas hasta el siguiente movimiento.
LIGHTCASILLA(char, int, int, int, int) es la función que implementamos para llevar a cabo esta acción.
Dado que son pocas líneas de código, podemos incluir la misma función para desarrollar mejor su explicación y comprensión.
- Animaciones Varias. Estas animaciones ocurren en situaciones concretas y las hemos implementado para que el mensaje que queríamos transmitir fuera más visual.
- WINANIMATION(): una vez se ha terminado la partida, y el resultado ha sido “ganador” aparecerá una animación en la que aparecerán las letras secuencialmente de WIN, en color verde.
- LOSEANIMATION(): una vez se ha terminado la partida, y el resultado ha sido “perdedor” aparecerá una animación en la que aparecerán las letras secuencialmente de LOSE, en color rojo.
- INVALIDANIMATION(): está animación se implementa cuando se realiza un movimiento no válido, es decir, que no está permitido.
Esta animación, es muy simple, ya que lo único que muestra es una X en rojo por todo el tablero.
NOTA: para todas las funciones mencionadas anteriormente para las diferentes animaciones, hemos implementado una función auxiliar APAGARTODO(), que llamamos continuamente ya sea a la hora de mostrar las letras de ganar o perder; o cuando se inicia el siguiente movimiento trás el movimiento de la IA.
VÍDEO EXPLICATIVO
ENLACE AL CÓDIGO