Juego de Simon con Arduino
Proyecto realizado por: Andrés Martín Esteban y Carlos Casado Suela.
Índice
- Descripción del proyecto
- Materiales utilizados
- Desarrollo del proyecto
- Problemas encontrados
- Modelado 3D
- Código y circuito del proyecto
- Resultado final
DESCRIPCIÓN DEL PROYECTO
Para el proyecto nos hemos propuesto recrear en Arduino el famoso juego del Simon, que es un juego que consiste en memorizar las secuencias de luces que van apareciendo y reproducirlas en orden en los botones indicados por el mismo color de la led iluminada. A cada nivel que pasa, se va añadiendo una nueva iluminación de led a la secuencia, por lo que avanzar de nivel cada vez es más difícil. ¿Cuántos puntos crees que puedas hacer? ¡Descúbrelo a continuación!
Al juego le hemos añadido funcionalidad aparte del original, como por ejemplo una 5º led con su respectivo botón (el juego original consta de 4 leds), la opción de seleccionar en que dificultad quiere jugar el usuario, una tabla con el histórico de las mejores puntuaciones registradas en el sistema, la cual funciona haciendo uso de la memoria EEPROM del Arduino, y un novedoso nuevo modo de juego en el que, en vez de mostrarse la secuencia entera a reproducir en los botones, solo se muestra la nueva led de la secuencia, teniendo el usuario que recordar como era la secuencia antes de añadir esta nueva led, ¡por lo que el usuario hace mayor uso de su memoria!
Además, le hemos añadido una pantalla al juego, para que el usuario pueda revisar en cada momento que tiempo le queda para responder o pueda seleccionar la dificultad del juego entre otras cosas.
También hemos añadido un pequeño altavoz, para que la memoria implicada en el juego no sea sólo visual sino también auditiva, siendo que cada color de led lleva asociado un sonido particular, ¿Te atreves a jugar con los ojos cerrados? Además, cuando pierdes o ganas, el altavoz reproduce una melodía que indica al usuario que ha fallado o que ha terminado todos los niveles.
El juego dispone de un máximo de 100 niveles, significando que si eres capaz de responder a los 100 niveles, ¡habrás ganado el juego!
MATERIALES UTILIZADOS
Nombre | Detalles | Unidades | Precio (€) |
Arduino Uno R3 | Placa de Arduino utilizada para el proyecto | 1 | – |
Resistencia | Resistencia 330Ω | 5 | – |
Led | Leds de diferentes colores | 5 | 0,3 |
Pulsador | Pulsadores de diferentes colores | 5 | 0,7 |
Lcd | Pantalla Lcd I2C 20×4 | 1 | 11 |
Pila | Una pila para dar energía al arduino (9V) | 1 | 3 |
Piezo | Piezo para reproducir diferentes sonidos | 1 | – |
Interruptor | Para el mecanismo de encendido/apagado | 1 | 0,3 |
DESARROLLO DEL PROYECTO
Lo primero que hicimos para desarrollar el proyecto fue implementar la lógica del juego. Para ello definimos como queríamos que funcionase el juego antes de empezar a programar nada, y decidimos que las dificultades vendrían dadas por el tiempo que se muestra la led encendida en cada nivel, siendo la dificultad más fácil la que más tiempo está encendidos los leds.
Luego empezamos a programar esto. Para programarlo pensamos en un array que fuese guardando la secuencia correcta, una función que leyera los botones pulsados en cada momento del nivel por el usuario, y una función nivel que comprobara si la secuencia introducida por el usuario es correcta o no, devolviendo -1 en caso de acertar o el número del nivel en caso de que no.
El número de nivel lo devolvemos porque es el que más tarde utilizaríamos para actualizar la puntuaciones en la memoria EEPROM.
Cuando realizamos esto, pensamos que nuestro proyecto era demasiado similar al juego original, por lo que pensamos en añadir el nuevo modo de juego que al final hemos implementado, que coge los tiempos de la dificultad más dificil anteriormente programada y, en vez de mostrar toda la secuencia guardada en el array mencionado anteriormente, solo mostraba la última entrada.
Aún con este nuevo modo de juego sentíamos insuficiente lo implementado, por lo que decidimos añadir el altavoz que reprodujera sonidos cuando se iluminaba una led específica o que reprodujera melodías características de una partida ganada/perdida, fácilmente reconocibles para el usuario, ya que están cogidas del famoso juego Super Mario. Esto mejoraba la experiencia del usuario con el sistema, pero seguía pareciendo insuficiente para el sistema la funcionalidad hasta ahora aplicada.
Entonces decidimos añadir una pantalla LCD, concretamente de protocolo I2C, ya que la que nos venía dada para la realización del proyecto ocupaba muchos pines que ya estaban siendo usador por las leds y los botones. Esta pantalla nos abrió un abanico de posibilidades para la implementación de funcionalidad nueva, como la visualización en todo momento del tiempo disponible para marcar la secuencia con los botones, la visualización de la puntuación al final de la partida o la opción de que el usuario sepa en que dificultad está jugando en todo momento.
Cuando ya implementamos todo lo mencionado anteriormente, fue cuando decidimos hacer uso de la memoria EEPROM del Arduino para guardar las puntuaciones finales, concretamente las 4 mejores.
Al terminar de montar lo necesario para testear el sistema y antes de montarlo en lo que será su caja definitiva, así se veía el proyecto:
PROBLEMAS ENCONTRADOS
Cuenta atrás
A la hora de implementar la cuenta atrás con el tiempo restante para introducir la secuencia, primeramente intentamos implementarlo con la función millis(), de tal forma que guardamos el tiempo inicial en una variable, y comparábamos si el tiempo actual (millis) era menor que el tiempo inicial mas 5 (tiempo máximo establecido), pero el uso de esta función daba problemas, ya que a veces no guardaba bien el tiempo actual y no terminaba de hacer la cuenta atrás, y otras veces no se reseteaba el tiempo, dando a casos en los que el usuario pierde sin tener opción a responder.
Nuestra solución fue utilizar una librería, ellapsedMillis, que tiene una funcionalidad parecida a millis, pero para usarla, en vez de guardar el valor de retorno de millis en una variable, directamente creamos una variable ellapsedMillis que va actualizándose cada milisegundo que pasa, por lo que para implementarlo en la función de leer botones, simplemente había que declarar una variable nueva, que empezara su cuenta en 0, y comparar todo el rato su valor con 5000 (5 segundos), de forma que la función se interrumpiera en caso de que la variable valiera más que 5000.
Mantener botón pulsado
Con nuestra forma de leer los botones, podía darse una situación en la que, si el usuario dejaba el botón presionado a la hora de introducir una secuencia, el botón seguía pasando su valor a la función de leer botones aunque se hubiera cambiado de nivel, por lo que se podía incluso llegar a situaciones en las que si un jugador dejaba el botón presionado más de la cuenta en el modo imposible, el cual sólo imprime la última led de la secuencia, leyera 2 veces el botón, y perdiera la partida, lo que podía llegar a ser muy frustrante para el usuario.
Para solucionarlo, introducimos al proyecto un array de bool, que representaba el estado del botón, siendo esto que si el botón estaba pulsado la variable bool del respectivo botón estaría establecida en true, mientras que si no estuviera pulsado, estaría establecida en false.
Con este array hacemos comparaciones en la función de leer botones. Si el botón tiene estado LOW( Recordar que los botones están establecidos en INPUT-PULLUP) y su variable booleana aparece como true, es decir, el botón no se ha soltado desde que se ha pulsado, entonces no se imprime ese botón como respuesta a la secuencia. Para que esto funcione correctamente, hemos implementado también un condicional que actualiza el estado de la variables booleanas, tanto si el botón deja de estar pulsado y la variable esta establecida en true, como si el botón está pulsado y la variable está establecida en false, en ambos casos cambia el valor de la variable.
Pantalla LCD
Para el sistema disponíamos de una pantalla LCD 16×2, que no cuenta con el protocolo I2C, por lo que para hacerla funcionar necesitábamos de una amplia cantidad de pines del Arduino, cantidad con la que no contábamos ya que estaban en su mayoría ocupados por las leds, y los botones.
Por lo que decidimos adquirir una nueva pantalla con protocolo I2C, que tan solo requería de los pines SDA, SCL, 5V, y GND, pines que no están ocupados en nuestro proyecto. Además, la nueva pantalla que hemos adquirido es más grande, de 20×4 caracteres, con lo que disponemos de más espacio para facilitar la información al usuario.
Esta pantalla al principio nos dio problemas, ya que desconocíamos si funcionamiento, y no sabíamos que había que ajustar la luminosidad de los caracteres con un potenciómetro que hay en la parte trasera de la pantalla, así que eso os dio problemas hasta que descubrimos el funcionamiento del potenciómetro.
MODELADO 3D
Para diseñar una caja para el sistema, nos hemos inspirado en un mando de la PlayStation, de forma que quedan 3 botones con sus respectivos leds en un lado del mando, donde típicamente en el mando tradicional de PlayStation estarían los botones, otros 2 botones con sus respectivos leds donde típicamente se encontraría las flechas direccionales del mando. Además, la pantalla se situaría en el centro del mando.
Con esta distribución de los botones conseguimos que el usuario tenga siempre en cuenta el tiempo restante para resolver, ya que la pantalla está relativamente cerca de los botones, y el usuario no tiene casi que desviar la mirada para atender al tiempo restante, lo que le permite tener una mejor experiencia de juego.
Además realizamos también una carcasa trasera para el mando pero fallo durante la impresión y no la pudimos añadir.
CÓDIGO Y CIRCUITO DEL PROYECTO
#include <elapsedMillis.h>
#include <EEPROM.h>
#include <LiquidCrystal_I2C.h>
#define NOTE_G3 196
#define NOTE_C4 262
#define NOTE_E4 330
#define NOTE_G5 784
#define NOTE_AS5 932
#define tonePin 2
LiquidCrystal_I2C lcd(0x27, 20, 4);
const int led[] = {7, 9, 3, 8, 4};//blanco,rojo,amarillo,verde,azul
const int button[] = {11, 13, 0, 12, 2};//blanco,rojo,amarillo,verde,azul
int niveles = 100;
const int gameTones[] = { NOTE_G3, NOTE_C4, NOTE_E4, NOTE_G5, NOTE_AS5};
int dificultad;
byte partida[100];
bool extremeMode = false, pulsado[] = {false, false, false, false, false};
byte ABIERTO[8] = {
B11111,
B10001,
B10001,
B10001,
B10001,
B10001,
B10001,
B11111,
};
byte CERRADO[8] = {
B11111,
B11111,
B11111,
B11111,
B11111,
B11111,
B11111,
B11111,
};
byte MEDIO[8] = {
B11111,
B10001,
B10001,
B10001,
B11111,
B11111,
B11111,
B11111,
};
void loosingSound() {
tone(tonePin, 523, 150.0);
delay(166.666666667);
delay(166.666666667);
tone(tonePin, 391, 150.0);
delay(166.666666667);
delay(166.666666667);
tone(tonePin, 329, 225.0);
delay(250.0);
tone(tonePin, 440, 172.5);
delay(191.666666667);
delay(2.77777777778);
tone(tonePin, 493, 172.5);
delay(191.666666667);
delay(2.77777777778);
tone(tonePin, 440, 175.0);
delay(194.444444444);
tone(tonePin, 415, 225.0);
delay(250.0);
tone(tonePin, 466, 225.0);
delay(250.0);
tone(tonePin, 415, 225.0);
delay(250.0);
tone(tonePin, 391, 112.5);
delay(125.0);
tone(tonePin, 349, 112.5);
delay(125.0);
tone(tonePin, 391, 30.0);
delay(33.3333333333);
tone(tonePin, 384, 500.0);
delay(666.666666667);
}
void winSound() {
tone(tonePin, 195, 98.2530375);
delay(109.170041667);
delay(5.02756770833);
tone(tonePin, 261, 98.2530375);
delay(109.170041667);
delay(5.74579166667);
tone(tonePin, 329, 97.6066359375);
delay(108.451817708);
delay(7.18223958333);
tone(tonePin, 391, 98.2530375);
delay(109.170041667);
delay(5.02756770833);
tone(tonePin, 523, 98.2530375);
delay(109.170041667);
delay(5.74579166667);
tone(tonePin, 659, 97.6066359375);
delay(108.451817708);
delay(7.18223958333);
tone(tonePin, 783, 294.7591125);
delay(327.510125);
delay(17.237375);
tone(tonePin, 659, 294.7591125);
delay(327.510125);
delay(17.237375);
tone(tonePin, 207, 98.2530375);
delay(109.170041667);
delay(5.02756770833);
tone(tonePin, 261, 98.2530375);
delay(109.170041667);
delay(5.74579166667);
tone(tonePin, 311, 97.6066359375);
delay(108.451817708);
delay(7.18223958333);
tone(tonePin, 415, 98.2530375);
delay(109.170041667);
delay(5.02756770833);
tone(tonePin, 523, 98.2530375);
delay(109.170041667);
delay(5.74579166667);
tone(tonePin, 622, 97.6066359375);
delay(108.451817708);
delay(7.18223958333);
tone(tonePin, 830, 294.7591125);
delay(327.510125);
delay(17.237375);
tone(tonePin, 622, 294.7591125);
delay(327.510125);
delay(17.237375);
tone(tonePin, 233, 98.2530375);
delay(109.170041667);
delay(5.02756770833);
tone(tonePin, 293, 98.2530375);
delay(109.170041667);
delay(5.74579166667);
tone(tonePin, 349, 97.6066359375);
delay(108.451817708);
delay(7.18223958333);
tone(tonePin, 466, 98.2530375);
delay(109.170041667);
delay(5.02756770833);
tone(tonePin, 587, 98.2530375);
delay(109.170041667);
delay(5.74579166667);
tone(tonePin, 698, 97.6066359375);
delay(108.451817708);
delay(7.18223958333);
tone(tonePin, 932, 294.7591125);
delay(327.510125);
delay(17.237375);
tone(tonePin, 932, 98.2530375);
delay(109.170041667);
delay(5.02756770833);
tone(tonePin, 932, 98.2530375);
delay(109.170041667);
delay(5.74579166667);
tone(tonePin, 932, 97.6066359375);
delay(108.451817708);
delay(7.18223958333);
tone(tonePin, 659, 294.7591125);
delay(327.510125);
}
void iluminarLed(int l){
digitalWrite(led[l], HIGH);
tone(tonePin, gameTones[l]);
delay(200 * dificultad);
digitalWrite(led[l], LOW);
noTone(tonePin);
delay(50);
}
void printTopScores(int punt){
lcd.clear();
lcd.setCursor(4, 0);
lcd.print("Puntuaciones");
lcd.setCursor(0,1);
for(int i = 0; i < 4; i++){
lcd.print(i+1);
lcd.print(": ");
lcd.print(EEPROM.read(i*4));
if(i%2==1){
lcd.setCursor(0,2);
}else{
lcd.print(", ");
}
}
lcd.setCursor(0, 3);
lcd.print("Tu Puntuacion:");
lcd.print(punt);
}
void updateTopScores(int puntuacionUsuario){
for(int i = 0; i < 4; i++){
if(puntuacionUsuario > EEPROM.read(4*i)){
if (i != 3){
for(int j=3; j>i; j--){
EEPROM.write(4*j, EEPROM.read(4*(j-1)));
}
}
EEPROM.write(4*i, puntuacionUsuario);
break;
}
}
}
int level(byte n){
lcd.setCursor(6, 2);
byte valor = 0;
int aux;
String nS;
partida[n] = random(5);
lcd.print(n);
if (!extremeMode){
for (byte i = 0; i < n; i++){
iluminarLed(partida[i]);
}
}else{
iluminarLed(partida[n]);
}
if (!extremeMode){
while(valor < n){
aux = readButtons();
if(aux!=-1)
iluminarLed(aux);
if (aux == partida[valor]){
valor++;
delay(5);
}else return n;
}
}else{
while(valor <= n){
aux = readButtons();
if(aux!=-1)
iluminarLed(aux);
if (aux == partida[valor]){
valor++;
delay(5);
}else return n;
}
}
return -1;
}
int readDifficulties() {
while (true) {
for (int i = 0; i < 5; i++) {
byte buttonPin = button[i];
if (digitalRead(buttonPin) == LOW && pulsado[i] == false) {
pulsado[i]=true;
return i;
}
if(digitalRead(buttonPin) == HIGH && pulsado[i] == true){
pulsado[i]=false;
}
}
delay(1);
}
}
int readButtons() {
elapsedMillis timer;
lcd.setCursor(2, 3);
lcd.print("Tiempo Restante:");
while (timer < 5000) {
lcd.setCursor(18, 3);
for (int i = 1; i < 6; i++){
if (timer < 1000*i){
lcd.print(5-i);
break;
}
}
for (int i = 0; i < 5; i++) {
byte buttonPin = button[i];
if (digitalRead(buttonPin) == LOW && pulsado[i] == false) {
pulsado[i]=true;
return i;
}
if(digitalRead(buttonPin) == HIGH && pulsado[i] == true){
pulsado[i]=false;
}
}
delay(1);
}
return -1;
}
void printDifficulties(){
lcd.createChar (0,ABIERTO);
lcd.createChar (1,CERRADO);
lcd.createChar (2,MEDIO);
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Dificultad");
lcd.setCursor(0,1);
lcd.write(byte (1));
lcd.write(byte (0));
lcd.write(byte (0));
lcd.print(":AZUL");
lcd.setCursor(0,2);
lcd.write(byte (1));
lcd.write(byte (2));
lcd.write(byte (0));
lcd.print(":VERDE");
lcd.setCursor(0,3);
lcd.write(byte (1));
lcd.write(byte (1));
lcd.write(byte (0));
lcd.print(":AMARILLO");
lcd.setCursor(10,1);
lcd.write(byte (1));
lcd.write(byte (1));
lcd.write(byte (2));
lcd.print(":ROJO");
lcd.setCursor(10,2);
lcd.write(byte (1));
lcd.write(byte (1));
lcd.write(byte (1));
lcd.print(":BLANCO");
}
void printLevel(int aux){
lcd.clear();
lcd.setCursor(0,1);
lcd.print("Dificultad:");
switch(aux){
case(0):
lcd.write(byte (1));
lcd.write(byte (1));
lcd.write(byte (1));
break;
case(1):
lcd.write(byte (1));
lcd.write(byte (1));
lcd.write(byte (2));
break;
case(2):
lcd.write(byte (1));
lcd.write(byte (1));
lcd.write(byte (0));
break;
case(3):
lcd.write(byte (1));
lcd.write(byte (2));
lcd.write(byte (0));
break;
case(4):
lcd.write(byte (1));
lcd.write(byte (0));
lcd.write(byte (0));
break;
}
lcd.setCursor(0, 2);
lcd.print("Nivel:");
}
void setup() {
lcd.init();
lcd.backlight();
randomSeed(analogRead(A0));
for (byte i =0; i < 5; i++){
pinMode(led[i], OUTPUT);
pinMode(button[i], INPUT_PULLUP);
}
}
void loop() {
int aa;
dificultad = 1;
printDifficulties();
int aux = readDifficulties();
iluminarLed(aux);
dificultad = (aux > 0)? aux : 1;
extremeMode = (aux == 0)?true:false;
niveles = (extremeMode==true)?niveles:niveles+1;
delay(500);
printLevel(aux);
for(byte i = 0; i <niveles; i++){
aa = level(i);
if(aa != -1){
lcd.clear();
lcd.setCursor(8,1);
lcd.print("Game");
lcd.setCursor(8,2);
lcd.print("Over");
loosingSound();
lcd.clear();
lcd.setCursor(0,1);
lcd.print("Puntuacion total:");
lcd.setCursor(0,2);
aa=(extremeMode==true)?aa:aa-1;
lcd.print(aa);
delay(1000);
break;
}
if(i==niveles-1){
lcd.clear();
lcd.setCursor(4,1);
lcd.print("Enhorabuena");
lcd.setCursor(4, 2);
lcd.print("Has Ganado!!");
winSound();
aa = niveles;
}
(extremeMode != true)?delay(1000):delay(250);
}
printTopScores(aa);
delay(2000);
updateTopScores(aa);
printTopScores(aa);
niveles = (extremeMode==true)?niveles:niveles-1;
delay (2000);
}