El juego Simón Dice es un ejercicio de memoria basado en repetir secuencias de luces y sonidos que crecen en cada ronda.
A primera vista, puede parecer un proyecto sencillo (encender luces y leer botones). Sin embargo, bajo el capó, este proyecto es la excusa perfecta para introducir conceptos fundamentales en informática como el manejo de Arrays dinámicos, la generación de números pseudo-aleatorios y, sobre todo, el diseño de Máquinas de Estados Finitos (FSM).
El objetivo es crear un dispositivo que genere una secuencia aleatoria de luces y sonidos, la cual el jugador debe repetir. Si acierta, la secuencia crece; si falla, el juego se reinicia.
Este proyecto es ideal para consolidar la lectura de entradas digitales, el uso de arrays y la lógica de control de flujo. Al final veremos cómo evolucionarlo para usar millis() sin bloquear el programa.
La lógica por fases
Antes de conectar un solo cable, tenemos que pensar en la lógica. Un error común al empezar es intentar escribir todo el código dentro de un loop gigante lleno de if y delay. Eso es insostenible.
La forma correcta de abordar un juego como el Simón es mediante una Máquina de Estados Finitos. Nuestro sistema pasará por distintas fases claramente diferenciadas.
Podemos definir los siguientes estados:
IDLE (Esperando): El juego está quieto, esperando a que alguien pulse un botón para empezar.
SHOWING (Mostrando): Arduino reproduce la secuencia actual de luces y sonidos. Aquí el usuario solo observa.
INPUT (Jugando): Arduino espera a que el usuario pulse la secuencia. Aquí es donde debemos leer botones y comparar en tiempo real.
GAMEOVER (Perdedor): El usuario falló. Hacemos un sonido de error, reiniciamos variables y volvemos a IDLE.
LEVEL_UP (Nivel completado): El usuario acertó. Añadimos un paso más a la secuencia y volvemos a SHOWING.
Esta estructura nos permite tener el código organizado y modular. Si queremos cambiar la animación de “Game Over”, solo tocamos esa parte del código sin romper la lectura de botones.
Componentes
- [1x|blue] Arduino Uno | El cerebro del proyecto
- [4x|green] LEDs de diferentes colores | Indican la secuencia (Rojo, Verde, Azul, Amarillo)
- [4x|yellow] Resistencia 220Ω o 330Ω | Limitan corriente de LEDs
- [4x|orange] Pulsadores (Pushbuttons) | Entrada del jugador
- [1x|purple] Zumbador Pasivo (Buzzer) | Genera notas musicales {pasivo}
- [Varios|gray] Protoboard y cables | Conexiones
Para este montaje vamos a necesitar componentes muy básicos. Lo interesante aquí es la cantidad, ya que gestionaremos 4 canales idénticos (uno por color).
Para los pulsadores usaremos las resistencias de Pull-up internas de Arduino. Esto nos ahorra colocar 4 resistencias físicas adicionales en la protoboard, simplificando mucho el montaje.
Esquema de conexión
El montaje es repetitivo, lo cual es bueno para practicar el orden en el cableado.
LEDs de colores
Conectamos cada LED con su resistencia en serie. La resistencia puede ir en el ánodo o en el cátodo, porque eléctricamente está en serie igualmente.
[ { “from”: “LED Verde + resistencia”, “to”: “Pin 2”, “color”: “green”, “note”: “Ánodo al pin, cátodo a GND” }, { “from”: “LED Amarillo + resistencia”, “to”: “Pin 3”, “color”: “yellow” }, { “from”: “LED Azul + resistencia”, “to”: “Pin 4”, “color”: “blue” }, { “from”: “LED Rojo + resistencia”, “to”: “Pin 5”, “color”: “red” }, { “from”: “Cátodos LED”, “to”: “GND Arduino”, “color”: “black” } ]
Pulsadores
Usaremos INPUT_PULLUP, así que cada botón se conecta entre el pin de entrada y GND. La lógica queda invertida: LOW significa pulsado.
[ { “from”: “Botón 1”, “to”: “Pin 6”, “color”: “orange”, “note”: “INPUT_PULLUP, otro lado a GND” }, { “from”: “Botón 2”, “to”: “Pin 7”, “color”: “orange”, “note”: “INPUT_PULLUP, otro lado a GND” }, { “from”: “Botón 3”, “to”: “Pin 8”, “color”: “orange”, “note”: “INPUT_PULLUP, otro lado a GND” }, { “from”: “Botón 4”, “to”: “Pin 9”, “color”: “orange”, “note”: “INPUT_PULLUP, otro lado a GND” }, { “from”: “Otro lado de botones”, “to”: “GND Arduino”, “color”: “black” } ]
Buzzer
El buzzer nos dará el feedback sonoro de cada color.
[ { “from”: “Buzzer (+)”, “to”: “Pin 10”, “color”: “purple”, “note”: “Señal para tone()” }, { “from”: “Buzzer (-)”, “to”: “GND Arduino”, “color”: “black” } ]
Es recomendable usar pines consecutivos para facilitar el uso de bucles for en la configuración inicial.
El código
La parte interesante empieza al usar arrays para mapear nuestro hardware. Esto hace que el código sea escalable. Si mañana quieres un Simón de 6 colores, solo cambias el tamaño de los arrays.
Definiciones y variables globales
Primero, definimos los pines y las notas musicales. Observa cómo usamos arrays constantes para asociar el LED i con el botón i y su tono i.
// --- Hardware Mapping ---
const int MAX_LEVEL = 100; // Nivel máximo del juego
const int NUM_COLORS = 4;
// Pines (Verde, Amarillo, Azul, Rojo)
const int ledPins[] = {2, 3, 4, 5};
const int buttonPins[] = {6, 7, 8, 9};
const int buzzerPin = 10;
// Frecuencias para cada color (Do, Mi, Sol, Do alta)
const int gameTones[] = {261, 329, 392, 523};
// --- Game State Variables ---
int gameSequence[MAX_LEVEL]; // Array para guardar la secuencia
int currentLevel = 0; // Nivel actual
Configuración (setup)
En el setup, inicializamos los pines. Lo más importante aquí es la semilla aleatoria randomSeed. Si no hacemos esto, el juego siempre generará la misma secuencia cada vez que lo encendamos, lo cual sería muy aburrido.
void setup() {
Serial.begin(9600);
for (int i = 0; i < NUM_COLORS; i++) {
pinMode(ledPins[i], OUTPUT);
pinMode(buttonPins[i], INPUT_PULLUP); // Importante: Pull-up interno
}
pinMode(buzzerPin, OUTPUT);
// Semilla aleatoria usando ruido de un pin analógico al aire
randomSeed(analogRead(A0));
}
El bucle principal y la lógica
Aquí implementamos la lógica del juego. En lugar de usar una máquina de estados no bloqueante con switch, la simplificamos en funciones secuenciales dentro de loop() para que sea más legible.
El flujo es:
- Si empezamos nivel 0 -> Generar secuencia inicial.
- Mostrar secuencia (
showSequence). - Leer intento del jugador (
readSequence). - Si acierta -> Aumentar nivel y esperar un poco.
- Si falla -> Game Over y reiniciar.
void loop() {
if (currentLevel == 0) {
// Inicio del juego (podríamos poner una animación aquí)
delay(1000);
generateNextStep();
}
showSequence(); // Fase 1: Arduino muestra
if (readSequence()) { // Fase 2: Jugador repite
// Si devuelve true, el jugador acertó toda la secuencia
delay(1000); // Pausa entre niveles
if (currentLevel < MAX_LEVEL) {
generateNextStep();
} else {
victoria();
}
} else {
// Si devuelve false, el jugador falló
gameOver();
}
}
Funciones auxiliares
Aquí es donde “picamos piedra”. Necesitamos funciones para encender luces con sonido y para gestionar la lógica.
1. Añadir un paso a la secuencia:
void generateNextStep() {
if (currentLevel >= MAX_LEVEL) return;
// Añadimos un color aleatorio (0 a 3) al final de la secuencia actual
gameSequence[currentLevel] = random(0, NUM_COLORS);
currentLevel++;
}
La comprobación del límite evita escribir fuera del array gameSequence al superar el nivel 100.
2. Mostrar la secuencia: Recorremos el array hasta el nivel actual, encendiendo el LED y sonando el buzzer correspondiente.
void showSequence() {
for (int i = 0; i < currentLevel; i++) {
int colorIndex = gameSequence[i];
flashLED(colorIndex);
delay(250); // Pausa entre luces
}
}
void flashLED(int colorIndex) {
digitalWrite(ledPins[colorIndex], HIGH);
tone(buzzerPin, gameTones[colorIndex]);
delay(300); // Duración del encendido/sonido
digitalWrite(ledPins[colorIndex], LOW);
noTone(buzzerPin);
}
3. Leer la entrada del jugador (La parte crítica): Esta función debe esperar a que el usuario pulse un botón, identificar cuál es, encender su luz (feedback visual) y comprobar si coincide con la secuencia guardada.
bool readSequence() {
for (int i = 0; i < currentLevel; i++) {
int expectedButton = gameSequence[i]; // El color que debería tocar
int actualButton = -1; // El que realmente toca
// Esperamos a que pulse un botón
while (actualButton == -1) {
for (int j = 0; j < NUM_COLORS; j++) {
if (digitalRead(buttonPins[j]) == LOW) { // LOW porque es INPUT_PULLUP
actualButton = j;
flashLED(j); // Feedback visual/sonoro al pulsar
// Anti-rebote simple: esperar a que suelte
while (digitalRead(buttonPins[j]) == LOW) { delay(10); }
}
}
}
// Comprobamos si acertó
if (actualButton != expectedButton) {
return false; // Falló
}
}
return true; // Completó la secuencia correctamente
}
4. Game Over: Un pequeño drama sonoro y visual para indicar que ha perdido, y reiniciamos el nivel a 0.
void gameOver() {
// Sonido grave y largo
tone(buzzerPin, 150);
// Parpadeo de todos los LEDs
for(int i=0; i<3; i++){
for(int j=0; j<NUM_COLORS; j++) digitalWrite(ledPins[j], HIGH);
delay(200);
for(int j=0; j<NUM_COLORS; j++) digitalWrite(ledPins[j], LOW);
delay(200);
}
noTone(buzzerPin);
currentLevel = 0; // Reiniciar juego
delay(1000);
}
void victoria() {
for (int i = 0; i < NUM_COLORS; i++) {
flashLED(i);
}
currentLevel = 0;
delay(1000);
}
Posibles mejoras
Este código es una base sólida, pero todavía admite mejoras. Aquí tienes algunas ideas para seguir jugando:
- Aumentar la velocidad: Hacer que el
delaydentro deshowSequencesea menor a medida quecurrentLevelaumenta. - Modo de bajo consumo: Dormir el Arduino si no se pulsa nada en 30 segundos.
- Guardar récord: Usar la memoria EEPROM para guardar la puntuación máxima incluso si se apaga el Arduino.