Sistema de desactivación de bomba – Grupo 03
Grupo 03: Raúl Luaces Salamanca, Jonás Huertes Ramírez, Mengying Xia Ruan, Daniel Quijano Alonso

1. Índice
- Descripción y objetivos
- Hardware
- Costes del material
- Software
- Reparto de trabajo
- Problemas y resoluciones
- Funcionamiento
2. Descripción y objetivos
El proyecto consiste en el desarrollo de un sistema interactivo que simula una bomba que debe ser desactivada en un tiempo limitado mediante la superación de diferentes minijuegos.
El objetivo principal es integrar diferentes componentes hardware, como teclado, pantalla LCD, LEDs, botones, potenciómetro y sistema de audio, junto con la programación en Arduino que permita gestionar el flujo del juego de forma organizada.
Para ello, hemos diseñado minijuegos que ponen a prueba habilidades como el cálculo, la memoria o la precisión, combinando distintas formas de interacción con el usuario. Además, se ha trabajado en la coordinación entre hardware y software para garantizar un funcionamiento fluido y una experiencia dinámica
3. Hardware
En esta sección se muestran varias imágenes del montaje físico del proyecto, donde se puede ver cómo están distribuidos los distintos componentes dentro del sistema. La idea ha sido organizar todo de forma clara y funcional, conectando los diferentes módulos a una placa principal que actúa como controlador.

Entre los elementos más importantes se encuentran el Arduino, las protoboards utilizadas para el montaje, las resistencias y el cableado, además de los distintos dispositivos del proyecto: la pantalla LCD, el teclado, los botones con LEDs del minijuego de Simon, el potenciómetro y el sistema de audio con altavoz.


Estas imágenes ayudan a entender mejor cómo está construido el sistema en la práctica y cómo se han conectado todos los componentes para que funcionen de forma conjunta.
4. Costes del material
| Componentes | Origen del producto | Precio |
| Módulo microSD MP3 WTV020 | Compra externa | 8.00€ |
| Multiplexor | Compra externa | 1.12€ |
| Dos protoboards | Compra externa | 4.25€ |
| Altavoz extraplano Fonestar Mylar | Compra externa | 8.00€ |
| Pantalla LCD I2C 20×4 | Compra externa | 7.99€ |
| Teclado Matricial Membrana 4×4 | Compra externa | 2.00€ |
| Set de cables | Compra externa | 8.10€ |
| Potenciómetro | Compra externa | 0.40€ |
| Starter Kit Elegoo | Material de la asignatura | 0.00€ |
| Total | 40.77€ |
5. Software
En la parte del software hemos estructurado el proyecto en varios archivos independientes, con el objetivo de mantener el código organizado, fácil de entender y modular. De esta forma, cada bloque se encarga de una parte concreta del funcionamiento del sistema, lo que facilita tanto el desarrollo como las pruebas.
El archivo principal,» bomba_desactivar.ino», actúa como núcleo del programa. En él se controla el flujo general del juego, el sistema de estados (menú, minijuegos, victoria o explosión), el temporizador global y la lógica que decide qué módulo se ejecuta en cada momento.
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>
// Pantalla LCD compartida
LiquidCrystal_I2C lcd(0x27, 20, 4);
// Teclado
extern Keypad teclado;
// ESTADOS DEL JUEGO
enum Estado {
MENU,
JUEGO_MATES,
JUEGO_SIMON,
JUEGO_PACMAN,
JUEGO_POT,
BOMBA_EXPLOTA,
GANASTE
};
Estado estadoActual = MENU;
// NIVEL Y TIEMPO
int nivel = 1;
const unsigned long TIEMPOS_NIVEL[4] = {
180000,
150000,
120000,
90000
};
unsigned long tiempoInicio = 0;
unsigned long duracionTotal = 180000;
unsigned long tiempoRestante = 0;
// TIC-TAC DE LA BOMBA
unsigned long ultimoTickBomba = 0;
int intervaloTickBomba = 1000;
// PROGRESO
bool matesSuperado = false;
bool simonSuperado = false;
bool pacmanSuperado = false;
bool potSuperado = false;
// MENÚ
int menuCursor = 0;
// setup()
void setup() {
Serial.begin(9600);
lcd.init();
lcd.backlight();
setupTeclado();
setupSimon();
setupPacman();
setupPotAudio();
mostrarMenu();
}
// loop()
void loop() {
if (estadoActual != MENU &&
estadoActual != BOMBA_EXPLOTA &&
estadoActual != GANASTE) {
tiempoRestante = duracionTotal - (millis() - tiempoInicio);
if ((long)tiempoRestante <= 0) {
cambiarEstado(BOMBA_EXPLOTA);
return;
}
mostrarTiempo();
actualizarTickBomba();
}
switch (estadoActual) {
case MENU: loopMenu(); break;
case JUEGO_MATES: loopTeclado(); break;
case JUEGO_SIMON: loopSimon(); break;
case JUEGO_PACMAN: loopPacman(); break;
case JUEGO_POT: loopPotAudio(); break;
case BOMBA_EXPLOTA: loopBomba(); break;
case GANASTE: loopGanaste(); break;
}
}
// MENÚ
void mostrarMenu() {
lcd.clear();
lcd.setCursor(0, 0); lcd.print("== DESACTIVA BOMBA==");
lcd.setCursor(0, 1); lcd.print("2=arriba 8=abajo ");
lcd.setCursor(0, 2); lcd.print("# para empezar ");
actualizarCursorMenu();
}
void actualizarCursorMenu() {
const char* etiquetas[4] = {
"Nivel 1 - Facil ",
"Nivel 2 - Medio ",
"Nivel 3 - Dificil ",
"Nivel 4 - Extremo "
};
lcd.setCursor(0, 3);
lcd.print(">");
lcd.print(etiquetas[menuCursor]);
}
void loopMenu() {
char tecla = teclado.getKey();
if (!tecla) return;
if (tecla == '2') {
menuCursor = (menuCursor - 1 + 4) % 4;
actualizarCursorMenu();
} else if (tecla == '8') {
menuCursor = (menuCursor + 1) % 4;
actualizarCursorMenu();
} else if (tecla == '#') {
nivel = menuCursor + 1;
duracionTotal = TIEMPOS_NIVEL[menuCursor];
tiempoInicio = millis();
matesSuperado = false;
simonSuperado = false;
pacmanSuperado = false;
potSuperado = false;
ultimoTickBomba = millis();
intervaloTickBomba = 1000;
cambiarEstado(JUEGO_MATES);
}
}
// TIC-TAC ACELERADO
void actualizarTickBomba() {
long segRestantes = (long)tiempoRestante / 1000;
if (segRestantes > 15) {
intervaloTickBomba = 1000;
} else if (segRestantes > 10) {
intervaloTickBomba = map(segRestantes, 15, 10, 1000, 500);
} else {
if (segRestantes < 0) segRestantes = 0;
intervaloTickBomba = map(segRestantes, 10, 0, 500, 450);
}
if (intervaloTickBomba < 100) intervaloTickBomba = 100;
if (millis() - ultimoTickBomba >= (unsigned long)intervaloTickBomba) {
ultimoTickBomba = millis();
reproducirTick();
}
}
// CAMBIAR DE ESTADO
void cambiarEstado(Estado nuevoEstado) {
estadoActual = nuevoEstado;
lcd.clear();
switch (nuevoEstado) {
case JUEGO_MATES: iniciarTeclado(); break;
case JUEGO_SIMON: iniciarSimon(); break;
case JUEGO_PACMAN: iniciarPacman(); break;
case JUEGO_POT: iniciarPotAudio(); break;
case BOMBA_EXPLOTA: iniciarExplosion(); break;
case GANASTE: iniciarGanaste(); break;
default: break;
}
}
// TEMPORIZADOR
void mostrarTiempo() {
int segundos = tiempoRestante / 1000;
int min = segundos / 60;
int seg = segundos % 60;
lcd.setCursor(13, 0);
if (min < 10) lcd.print("0");
lcd.print(min);
lcd.print(":");
if (seg < 10) lcd.print("0");
lcd.print(seg);
}
// PANTALLAS FINALES
void iniciarExplosion() {
reproducirExplosion();
lcd.clear();
lcd.setCursor(3, 0); lcd.print("*** BOOM ***");
lcd.setCursor(2, 1); lcd.print("La bomba exploto!");
lcd.setCursor(0, 3); lcd.print("# para volver menu");
}
void loopBomba() {
char tecla = teclado.getKey();
if (tecla == '#') {
menuCursor = 0;
cambiarEstado(MENU);
mostrarMenu();
}
}
void iniciarGanaste() {
reproducirExito();
lcd.clear();
lcd.setCursor(1, 0); lcd.print("** BOMBA DESACT! **");
lcd.setCursor(3, 1); lcd.print("Felicidades!!!");
lcd.setCursor(0, 2); lcd.print("Nivel ");
lcd.print(nivel);
lcd.print(" completado!");
lcd.setCursor(0, 3); lcd.print("# para volver menu");
}
void loopGanaste() {
char tecla = teclado.getKey();
if (tecla == '#') {
menuCursor = 0;
cambiarEstado(MENU);
mostrarMenu();
}
}
A partir de aquí, el comportamiento del sistema se divide en los distintos minijuegos. El archivo «modulo_teclado.ino» implementa el juego de matemáticas. Este módulo se encarga de generar las operaciones, leer la entrada del jugador y comprobar si la respuesta es correcta.
#include <Keypad.h>
// --- CONFIG TECLADO ---
const byte FILAS = 4;
const byte COLS = 4;
char teclas[FILAS][COLS] = {
{'1','2','3','A'},
{'4','5','6','B'},
{'7','8','9','C'},
{'*','0','#','D'}
};
byte pinesFilas[FILAS] = {A0, 2, 3, 4};
byte pinesCols[COLS] = {5, 6, 7, 8};
Keypad teclado = Keypad(makeKeymap(teclas), pinesFilas, pinesCols, FILAS, COLS);
// --- VARIABLES JUEGO ---
int numA = 0, numB = 0, numC = 0;
char opA = '+', opB = '+';
int respuestaCorrecta = 0;
String respuestaUsuario = "";
bool negativo = false;
void setupTeclado() {}
void iniciarTeclado() {
respuestaUsuario = "";
negativo = false;
generarOperacion();
mostrarOperacionEnLCD();
}
void loopTeclado() {
char tecla = teclado.getKey();
if (!tecla) return;
if (tecla >= '0' && tecla <= '9') {
if (respuestaUsuario.length() < 5) {
respuestaUsuario += tecla;
actualizarRespuesta();
}
} else if (tecla == 'A') {
if (nivel >= 3 && respuestaUsuario.length() == 0) {
negativo = !negativo;
actualizarRespuesta();
}
} else if (tecla == '*') {
if (respuestaUsuario.length() > 0) {
respuestaUsuario.remove(respuestaUsuario.length() - 1);
} else if (negativo) {
negativo = false;
}
actualizarRespuesta();
} else if (tecla == '#') {
if (respuestaUsuario.length() == 0 && !negativo) {
return;
}
int respuesta = respuestaUsuario.toInt();
if (negativo) respuesta = -respuesta;
if (respuesta == respuestaCorrecta) {
lcd.setCursor(0, 3);
lcd.print("OK! ");
efectoNivelCompletado();
matesSuperado = true;
cambiarEstado(JUEGO_SIMON);
} else {
lcd.setCursor(0, 3);
lcd.print("MAL! Era:");
lcd.print(respuestaCorrecta);
delay(2000);
cambiarEstado(BOMBA_EXPLOTA);
}
}
}
// --- GENERAR OPERACION ---
void generarOperacion() {
switch (nivel) {
case 1:
numA = random(1, 11);
numB = random(1, 11);
opA = '+';
respuestaCorrecta = numA + numB;
break;
case 2:
numA = random(1, 21);
numB = random(1, 21);
opA = (random(2) == 0) ? '+' : '-';
if (opA == '-' && numB > numA) {
int tmp = numA; numA = numB; numB = tmp;
}
respuestaCorrecta = (opA == '+') ? numA + numB : numA - numB;
break;
case 3:
numA = random(1, 21);
numB = random(1, 21);
opA = (random(2) == 0) ? '*' : '-';
respuestaCorrecta = (opA == '*') ? numA * numB : numA - numB;
break;
case 4:
numA = random(1, 16);
numB = random(1, 16);
numC = random(1, 16);
{
char ops[3] = {'+', '-', '*'};
opA = ops[random(3)];
opB = ops[random(3)];
int parcial;
if (opA == '+') parcial = numA + numB;
else if (opA == '-') parcial = numA - numB;
else parcial = numA * numB;
if (opB == '+') respuestaCorrecta = parcial + numC;
else if (opB == '-') respuestaCorrecta = parcial - numC;
else respuestaCorrecta = parcial * numC;
}
break;
}
}
// --- LCD ---
void mostrarOperacionEnLCD() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("MATES");
lcd.setCursor(0, 1);
if (nivel < 4) {
lcd.print(numA);
lcd.print(opA);
lcd.print(numB);
lcd.print("=?");
} else {
lcd.print(numA);
lcd.print(opA);
lcd.print(numB);
lcd.print(opB);
lcd.print(numC);
lcd.print("=?");
}
lcd.setCursor(0, 2);
lcd.print("R:");
lcd.setCursor(0, 3);
if (nivel >= 3) {
lcd.print("A=+/- *=del #=ok");
} else {
lcd.print("*=del #=ok");
}
}
void actualizarRespuesta() {
lcd.setCursor(2, 2);
lcd.print(" ");
lcd.setCursor(2, 2);
if (negativo) lcd.print("-");
lcd.print(respuestaUsuario);
}
Más adelante, el » modulo_simon_pacman.ino» incluye dos minijuegos: Simon dice, donde el jugador debe repetir una secuencia de luces en orden, y un Pac-Man simplificado, en el que controla un personaje en una cuadrícula para recoger objetivos.
Ambos juegos están gestionados con el sistema de multiplexado de LEDs y botones, adaptando la dificultad según el nivel.
// --- Pines del multiplexor ---
#define MUX_S0 12
#define MUX_S1 13
#define MUX_S2 A3
#define MUX_SIG A2
// --- Canales multiplexor ---
#define BTN_ROJO 0
#define BTN_VERDE 1
#define BTN_AZUL 2
#define BTN_AMARILLO 3
#define LED_ROJO 4
#define LED_VERDE 5
#define LED_AZUL 6
#define LED_AMARILLO 7
// --- FUNCIONES MULTIPLEXOR ---
void muxSeleccionar(int canal) {
digitalWrite(MUX_S0, (canal >> 0) & 1);
digitalWrite(MUX_S1, (canal >> 1) & 1);
digitalWrite(MUX_S2, (canal >> 2) & 1);
}
void encenderLed(int canal) {
pinMode(MUX_SIG, OUTPUT);
muxSeleccionar(canal);
digitalWrite(MUX_SIG, HIGH);
}
void apagarLed(int canal) {
pinMode(MUX_SIG, OUTPUT);
muxSeleccionar(canal);
digitalWrite(MUX_SIG, LOW);
}
void apagarTodosLeds() {
for (int i = LED_ROJO; i <= LED_AMARILLO; i++) apagarLed(i);
}
void encenderTodosLeds() {
for (int i = LED_ROJO; i <= LED_AMARILLO; i++) encenderLed(i);
}
void efectoNivelCompletado() {
encenderTodosLeds();
reproducirNivelCompletado();
delay(900);
apagarTodosLeds();
}
bool leerBoton(int canal) {
pinMode(MUX_SIG, INPUT);
muxSeleccionar(canal);
delayMicroseconds(10);
return digitalRead(MUX_SIG) == HIGH;
}
int leerBotonNoBloquear() {
for (int i = 0; i < 4; i++) {
if (leerBoton(i)) {
delay(50);
while (leerBoton(i));
return i;
}
}
return -1;
}
// --- SIMON SAYS ---
#define SIMON_MAX 10
int simonSecuencia[SIMON_MAX];
int simonLongitud = 3;
int simonPaso = 0;
int simonVelocidad = 600;
const char c0[] PROGMEM = "ROJO";
const char c1[] PROGMEM = "VERD";
const char c2[] PROGMEM = "AZUL";
const char c3[] PROGMEM = "AMAR";
const char* const colores[] PROGMEM = {c0, c1, c2, c3};
void setupSimon() {
pinMode(MUX_S0, OUTPUT);
pinMode(MUX_S1, OUTPUT);
pinMode(MUX_S2, OUTPUT);
apagarTodosLeds();
}
void iniciarSimon() {
switch (nivel) {
case 1: simonLongitud = 3; simonVelocidad = 700; break;
case 2: simonLongitud = 4; simonVelocidad = 500; break;
case 3: simonLongitud = 5; simonVelocidad = 350; break;
case 4: simonLongitud = 6; simonVelocidad = 200; break;
}
simonPaso = 0;
for (int i = 0; i < simonLongitud; i++) {
simonSecuencia[i] = random(0, 4);
}
lcd.clear();
lcd.setCursor(0, 0); lcd.print("== SIMON SAYS ==");
lcd.setCursor(0, 1); lcd.print("Seq: ");
lcd.print(simonLongitud);
lcd.print(" colores");
lcd.setCursor(0, 2); lcd.print("Observa...");
delay(1500);
mostrarSecuenciaSimon();
mostrarTurnoJugador();
}
void loopSimon() {
int boton = leerBotonNoBloquear();
if (boton == -1) return;
encenderLed(boton + LED_ROJO);
delay(200);
apagarLed(boton + LED_ROJO);
if (boton == simonSecuencia[simonPaso]) {
simonPaso++;
if (simonPaso >= simonLongitud) {
lcd.setCursor(0, 3); lcd.print("Correcto!!! ");
efectoNivelCompletado();
delay(600);
simonSuperado = true;
cambiarEstado(JUEGO_PACMAN);
} else {
lcd.setCursor(0, 3);
lcd.print("Paso ");
lcd.print(simonPaso);
lcd.print("/");
lcd.print(simonLongitud);
lcd.print(" OK! ");
}
} else {
apagarTodosLeds();
lcd.setCursor(0, 3); lcd.print("Error! Repite... ");
delay(1500);
simonPaso = 0;
mostrarSecuenciaSimon();
mostrarTurnoJugador();
}
}
void mostrarSecuenciaSimon() {
for (int i = 0; i < simonLongitud; i++) {
int led = simonSecuencia[i] + LED_ROJO;
encenderLed(led);
lcd.setCursor(0, 3);
lcd.print(">> ");
char buffer[5];
strcpy_P(buffer, (char*)pgm_read_word(&(colores[simonSecuencia[i]])));
lcd.print(buffer);
lcd.print(" ");
delay(simonVelocidad);
apagarLed(led);
delay(150);
}
}
void mostrarTurnoJugador() {
lcd.clear();
lcd.setCursor(0, 0); lcd.print("== SIMON SAYS ==");
lcd.setCursor(0, 1); lcd.print("Tu turno!");
lcd.setCursor(0, 2); lcd.print("Repite secuencia");
lcd.setCursor(0, 3); lcd.print("Paso 0/");
lcd.print(simonLongitud);
}
// --- PACMAN ---
#define GRID_COLS 20
#define GRID_FILAS 3
#define CHR_PAC_R 0
#define CHR_PAC_L 1
#define CHR_PAC_U 2
#define CHR_PAC_D 3
#define CHR_GHOST 4
byte pacmanRight[8] = {
0b01110,
0b10111,
0b11100,
0b11000,
0b11100,
0b11111,
0b01110,
0b00000
};
byte pacmanLeft[8] = {
0b01110,
0b11101,
0b00111,
0b00011,
0b00111,
0b11111,
0b01110,
0b00000
};
byte pacmanUp[8] = {
0b01010,
0b11011,
0b11011,
0b11111,
0b10111,
0b11111,
0b01110,
0b00000
};
byte pacmanDown[8] = {
0b01110,
0b11111,
0b10111,
0b11111,
0b11011,
0b11011,
0b01010,
0b00000
};
byte fantasma[8] = {
0b01110,
0b11111,
0b10101,
0b11111,
0b11111,
0b11111,
0b10101,
0b00000
};
int pacX, pacY;
int pacDirX = 1;
int pacDirY = 0;
int comidasObjetivo = 4;
int comidasComidas = 0;
int foodX = 10, foodY = 1;
unsigned long pacUltimoMov = 0;
int pacVelocidad = 400;
void setupPacman() {}
void iniciarPacman() {
switch (nivel) {
case 1: comidasObjetivo = 4; pacVelocidad = 500; break;
case 2: comidasObjetivo = 6; pacVelocidad = 400; break;
case 3: comidasObjetivo = 8; pacVelocidad = 300; break;
case 4: comidasObjetivo = 10; pacVelocidad = 220; break;
}
lcd.createChar(CHR_PAC_R, pacmanRight);
lcd.createChar(CHR_PAC_L, pacmanLeft);
lcd.createChar(CHR_PAC_U, pacmanUp);
lcd.createChar(CHR_PAC_D, pacmanDown);
lcd.createChar(CHR_GHOST, fantasma);
lcd.clear();
lcd.setCursor(7, 0); lcd.print("PACMAN");
lcd.setCursor(0, 1); lcd.print("Comete los fantasmas");
lcd.setCursor(0, 2); lcd.print("Botones = mover");
lcd.setCursor(0, 3); lcd.print("Empezando...");
delay(1500);
pacX = GRID_COLS / 2;
pacY = 1;
pacDirX = 1;
pacDirY = 0;
comidasComidas = 0;
pacUltimoMov = millis();
generarComida();
dibujarPacman();
}
void loopPacman() {
if (leerBoton(BTN_ROJO)) { pacDirX = 0; pacDirY = -1; }
if (leerBoton(BTN_VERDE)) { pacDirX = 0; pacDirY = 1; }
if (leerBoton(BTN_AZUL)) { pacDirX = -1; pacDirY = 0; }
if (leerBoton(BTN_AMARILLO)) { pacDirX = 1; pacDirY = 0; }
if (millis() - pacUltimoMov < (unsigned long)pacVelocidad) return;
pacUltimoMov = millis();
int nuevoX = pacX + pacDirX;
int nuevoY = pacY + pacDirY;
if (nuevoX < 0) nuevoX = GRID_COLS - 1;
if (nuevoX >= GRID_COLS) nuevoX = 0;
if (nuevoY < 0) nuevoY = GRID_FILAS - 1;
if (nuevoY >= GRID_FILAS) nuevoY = 0;
pacX = nuevoX;
pacY = nuevoY;
if (pacX == foodX && pacY == foodY) {
comidasComidas++;
generarComida();
if (comidasComidas >= comidasObjetivo) {
lcd.clear();
lcd.setCursor(0, 0); lcd.print("PACMAN ");
lcd.print(comidasComidas);
lcd.print("/");
lcd.print(comidasObjetivo);
lcd.setCursor(3, 1); lcd.print("GANASTE!");
efectoNivelCompletado();
delay(600);
pacmanSuperado = true;
cambiarEstado(JUEGO_POT);
return;
}
}
dibujarPacman();
}
void generarComida() {
do {
foodX = random(0, GRID_COLS);
foodY = random(0, GRID_FILAS);
} while (foodX == pacX && foodY == pacY);
}
byte pacmanCharActual() {
if (pacDirX == 1) return CHR_PAC_R;
if (pacDirX == -1) return CHR_PAC_L;
if (pacDirY == -1) return CHR_PAC_U;
if (pacDirY == 1) return CHR_PAC_D;
return CHR_PAC_R;
}
void dibujarPacman() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("PAC ");
lcd.print(comidasComidas);
lcd.print("/");
lcd.print(comidasObjetivo);
lcd.setCursor(foodX, foodY + 1);
lcd.write(byte(CHR_GHOST));
lcd.setCursor(pacX, pacY + 1);
lcd.write(pacmanCharActual());
}
Por otro lado, en modulo_pot_audio.ino se implementa el minijuego del potenciómetro, donde el objetivo es ajustar un valor analógico hasta alcanzar un objetivo concreto dentro de un margen de error. Además, este módulo también integra la parte de audio relacionada con el DFPlayer.
#include <SoftwareSerial.h>
#include <DFRobotDFPlayerMini.h>
#define PIN_POT A1
#define DFPLAYER_RX 10
#define DFPLAYER_TX 11
SoftwareSerial dfSerial(DFPLAYER_RX, DFPLAYER_TX);
DFRobotDFPlayerMini myDFPlayer;
// Variables del minijuego
int valorObjetivo = 0;
int margenAcierto = 30;
int valorActual = 0;
int ultimoValorActual = -1;
unsigned long potUltimaLectura = 0;
// Setup e inicio
void setupPotAudio() {
dfSerial.begin(9600);
if (!myDFPlayer.begin(dfSerial)) {
// No bloquear si falla
} else {
myDFPlayer.volume(25);
}
delay(500);
}
void iniciarPotAudio() {
switch (nivel) {
case 1: margenAcierto = 30; valorObjetivo = random(200, 801); break;
case 2: margenAcierto = 20; valorObjetivo = random(150, 851); break;
case 3: margenAcierto = 12; valorObjetivo = random(100, 924); break;
case 4: margenAcierto = 8; valorObjetivo = random(100, 924); break;
}
ultimoValorActual = -1;
potUltimaLectura = millis();
lcd.clear();
lcd.setCursor(0, 0); lcd.print("POT");
lcd.setCursor(0, 1); lcd.print("Meta:");
lcd.print(valorObjetivo);
lcd.print(" +/-");
lcd.print(margenAcierto);
delay(1500);
lcd.clear();
mostrarPantallaPot();
}
// Loop
void loopPotAudio() {
if (millis() - potUltimaLectura < 80) return;
potUltimaLectura = millis();
valorActual = analogRead(PIN_POT);
if (abs(valorActual - ultimoValorActual) < 3) return;
ultimoValorActual = valorActual;
int diferencia = abs(valorActual - valorObjetivo);
actualizarBarraPot();
lcd.setCursor(0, 3);
if (diferencia <= margenAcierto) {
lcd.print("!! ZONA OK !! ");
} else if (diferencia < margenAcierto * 2) {
lcd.print("Muy cerca! ");
} else if (diferencia < margenAcierto * 4) {
lcd.print("Cerca... ");
} else if (valorActual < valorObjetivo) {
lcd.print("Gira derecha >> ");
} else {
lcd.print("<< Gira izquierda ");
}
if (diferencia <= margenAcierto) {
delay(600);
valorActual = analogRead(PIN_POT);
if (abs(valorActual - valorObjetivo) <= margenAcierto) {
lcd.setCursor(0, 3); lcd.print("Perfecto!! Ganaste! ");
efectoNivelCompletado();
delay(600);
potSuperado = true;
cambiarEstado(GANASTE);
}
}
}
// Pantalla
void mostrarPantallaPot() {
lcd.setCursor(0, 0); lcd.print("POT");
lcd.setCursor(0, 1); lcd.print("Meta:");
lcd.print(valorObjetivo);
lcd.print(" Ahora:");
}
void actualizarBarraPot() {
lcd.setCursor(0, 1);
lcd.print("Meta:");
lcd.print(valorObjetivo);
lcd.print(" Ahr:");
lcd.print(valorActual);
lcd.print(" ");
int posObj = map(valorObjetivo, 0, 1023, 0, 19);
int posActual = map(valorActual, 0, 1023, 0, 19);
lcd.setCursor(0, 2);
for (int i = 0; i < 20; i++) {
if (i == posObj && i == posActual) {
lcd.print("*");
} else if (i == posObj) {
lcd.print("X");
} else if (i == posActual) {
lcd.print("|");
} else if (i > min(posObj, posActual) && i < max(posObj, posActual)) {
lcd.print(".");
} else {
lcd.print("-");
}
}
}
// AUDIO — DFPLAYER MINI
void reproducirExplosion() {
myDFPlayer.playMp3Folder(1);
}
void reproducirExito() {
myDFPlayer.playMp3Folder(2);
}
void reproducirNivelCompletado() {
myDFPlayer.playMp3Folder(3);
}
void reproducirTick() {
myDFPlayer.playMp3Folder(4);
}
6. Reparto del trabajo
Para llevar a cabo el proyecto, hemos dividido el trabajo entre los miembros del grupo de forma equilibrada, principalmente en los minijuegos, intentando que todos participáramos tanto en la parte de hardware como en la de software. Además, también hemos colaborado en la integración del sistema y en la resolución de los problemas que fueron surgiendo durante el desarrollo.
Un aspecto importante de nuestra forma de trabajar fue la toma de decisiones en grupo. Siempre que aparecía un problema técnico, ya fuera en el código o en el montaje del hardware, no lo resolvía una sola persona. Nos parábamos los tres a analizar la situación y buscar juntos la mejor solución, lo que nos permitió avanzar de forma más segura y evitar errores.
A continuación, se muestra el reparto de tareas que hemos seguido a lo largo del proyecto:
| Tarea a realizar | Miembro encargado | Descripción |
| Diseño general del sistema y organización | Todos | Definición conjunta de la idea del proyecto, estructura del juego y funcionamiento global. |
| Desarrollo del menú principal y control del sistema | Todos | Implementación y ajuste del sistema de navegación entre estados y niveles. |
| Integración del audio con el altavoz y DFPlayer | Todos | Configuración y pruebas de sonido (tic-tac, explosión, victoria…). |
| Minijuego 1 (Cálculo matemático) | Raúl Luaces | Resolución de operaciones matemáticas generadas aleatoriamente mediante teclado |
| Minijuego 2 (Simón dice con LEDs y botones) | Daniel Quijano | Repetición de una secuencia luminosa aleatoria pulsando los botones en el orden correcto. |
| Minijuego 3 (Simulación de Pac-Man) | Mengying Xia | Control de Pac-Man en una cuadrícula para comer fantasmas y avanzar en el juego. |
| Minijuego 4 (Regulación de potenciómetro) | Jonás Huertes | Ajuste de un valor mediante potenciómetro hasta alcanzar un objetivo dentro de un margen de tolerancia. |
| Montaje del HW | Todos | Montaje del circuito, conexiones y distribución de componentes en protoboard y estructura física. |
7. Problemas y resoluciones
Durante el desarrollo del proyecto nos fuimos encontrando con varios problemas, tanto en la parte de hardware como en la de software.
Muchos de ellos tenían que ver con la conexión de los componentes, el uso de los pines o la organización del código, y en algunos casos nos obligaron a replantear partes del diseño.
- Pantalla mas pequeña de lo necesario: Inicialmente utilizamos una pantalla LCD 16×2, pero resultaba limitada para mostrar toda la información del sistema, especialmente los menús y algunos minijuegos. Además, requería un número elevado de conexiones y ocupaba varios pines del Arduino, por lo que decidimos sustituirla por una pantalla LCD 20×4 con comunicación I2C, lo que permitió reducir el cableado, liberar pines y disponer de más espacio para representar correctamente la información del juego.
- Pantalla defectuosa: Al solicitar la nueva pantalla LCD 20×4, la unidad que recibimos tenía un problema de fabricación, ya que dos de sus pines estaban soldados entre sí. intentamos solucionarlo desoldando la conexión, pero seguía sin funcionar correctamente. Finalmente, optamos por comprar una nueva unidad.
- Falta de pines: A medida que fuimos añadiendo componentes, nos encontramos con que no disponíamos de suficientes pines en el Arduino para conectar todo lo que queríamos. Para solucionarlo, decidimos incorporar un multiplexor. Gracias a esto pudimos ampliar el número de conexiones disponibles y continuar con el desarrollo sin tener que eliminar componentes.
- Espacio insuficiente en protoboard: Al montar el circuito, nos dimos cuenta que una sola protoboard no era suficiente para organizar todos los componentes de forma clara, ya que el cableado se volvía confuso y no cabía todo correctamente. Para solucionarlo, decidimos utilizar una segunda protoboard y conectarlas entre sí, lo que nos permitió distribuir mejor los componentes y trabajar de forma más ordenada.
- Problema con el joystick y los pines: Inicialmente queríamos utilizar un joystick para controlar el movimiento del personaje en el minijuego de PacMan. Sin embargo, debido a la falta de pines disponibles, incluso tras incorporar el multiplexor, no fue posible integrarlo en el sistema. Como alternativa, decidimos utilizar los cuatro botones del Simon Says para simular las direcciones de movimiento, lo que permitió mantener la funcionalidad del juego sin necesidad de añadir más hardware.
- Cinta conductora no deseada: Para mejorar la estética del montaje utilizamos una cinta adhesiva metálica, similar al papel de aluminio. Sin embargo, no tuvimos en cuenta que este material era conductor, lo que provocaba contactos no deseados entre algunos componentes. Esto generaba fallos en el circuito, por lo que tuvimos que eliminarla y remplazarla por un material aislante adecuado.
8. Funcionamiento
A continuación se muestra una prueba del sistema en el nivel de dificultad más bajo. En este modo, los minijuegos presentan condiciones más accesibles, permitiendo observar de forma clara el funcionamiento general del sistema, la interacción con el usuario y la transición entre los distintos módulos.
Y por otra parte, en el siguiente vídeo se muestra qué ocurre cuando el jugador no consigue completar los minijuegos a tiempo en el nivel de mayor dificultad. Debido a la reducción del tiempo y al aumento de la complejidad, el sistema finaliza con la explosión de la bomba.
Esta prueba permite comprobar el comportamiento del temporizador, el incremento de la tensión (tic-tac) y la respuesta final del sistema en caso de fallo.