/*
* SISTEMA DE QUIZ PARA ARDUINO CON LCD I2C
*
* Funcionalidades:
* - 3 modos de juego: Verdadero/Falso, Opción Múltiple, y Modo Mixto
* - Sistema de navegación con 3 botones (UP, DOWN, OK)
* - Pantalla LCD 16x2 con interfaz I2C
* - Sistema de puntajes y retroalimentación
* - Manejo de caracteres especiales latinos
*
* Conexiones:
* - LCD I2C: SDA -> A4, SCL -> A5, VCC -> 5V, GND -> GND
* - Botón OK: Pin 2 (con resistencia pull-up interna)
* - Botón UP: Pin 3 (con resistencia pull-up interna)
* - Botón DOWN: Pin 4 (con resistencia pull-up interna)
*
* Autor: [Tu nombre]
* Fecha: [Fecha actual]
* Versión: 2.0
*/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Arduino.h>
// ===== CONFIGURACIÓN DE HARDWARE =====
LiquidCrystal_I2C lcd(0x27, 16, 2);
// Pulsadores
const int btnOK = 2; // Pulsador de confirmación
const int btnUp = 3; // Pulsador para navegar hacia arriba
const int btnDown = 4; // Pulsador para navegar hacia abajo
// ===== CONSTANTES DE INTERFAZ =====
// Mensajes de pantalla
const char* bienvenidaF1 = "QUIZ ARDUINO";
const char* bienvenidaF2 = "Cargando...";
const char* msjCorrecto = "CORRECTO!";
const char* msjIncorrecto = "INCORRECTO!";
const char* msjAciertos80 = "EXCELENTE!";
const char* msjAciertos60 = "BIEN HECHO!";
const char* msjAciertosBajo = "DEBES PRACTICAR!";
const char* msjFinal = "QUIZ TERMINADO";
const char* msjContinuar1 = "Presiona OK";
const char* msjContinuar2 = "para continuar";
const char* msjVolver1 = "OK para volver";
const char* msjVolver2 = "al menu";
// ===== CONSTANTES DE CONFIGURACIÓN =====
// Controles de visualización (true = activar, false = desactivar)
const bool mostrarAciertos = true; // Mostrar porcentaje de aciertos al final
const bool mostrarRetroalimentacion = true; // Mostrar mensajes motivacionales
const bool mostrarPuntosTotal = true; // Mostrar puntos totales obtenidos
const bool mostrarOpCorrecta = true; // Mostrar respuesta correcta cuando falla
const bool mostrarPuntosLogrados = true; // Mostrar puntaje actual tras respuesta correcta
const bool mostrarNumeroPreg_Punt = true; // Mostrar número de pregunta y puntos actuales
const bool mostrarTotalPreg = true; // Mostrar total de preguntas en info del juego
const bool mostrarModoJuego = true; // Mostrar tipo de modo de juego
// ===== CONFIGURACIÓN DE PREGUNTAS =====
// MODO 1: Solo Verdadero/Falso
bool preguntasVF[] = {
true, // P1: Verdadero
false, // P2: Falso
true, // P3: Verdadero
false // P4: Falso
// Agregar más preguntas aquí
};
// MODO 2: Solo Múltiple (0=A, 1=B, 2=C)
int preguntasMultiple[] = {
1, // P1: Opción B
0, // P2: Opción A
2, // P3: Opción C
1 // P4: Opción B
// Agregar más preguntas aquí
};
// MODO 3: Mixto
struct PreguntaMixta {
int tipo; // 0 = V/F, 1 = Múltiple
int respuesta; // V/F: 0=Falso,1=Verdadero | Múltiple: 0=A,1=B,2=C
};
// Define el tipo y respuesta de cada pregunta mixta
PreguntaMixta preguntasMixtas[] = {
{0, 1}, // P1: V/F - Verdadero
{1, 2}, // P2: Múltiple - C
{0, 0}, // P3: V/F - Falso
{1, 1}, // P4: Múltiple - B
// Agregar más preguntas aquí
};
// Cálculo automático de totales
int totalVF = sizeof(preguntasVF) / sizeof(preguntasVF[0]);
int totalMultiple = sizeof(preguntasMultiple) / sizeof(preguntasMultiple[0]);
int totalMixto = sizeof(preguntasMixtas) / sizeof(preguntasMixtas[0]);
// ===== VARIABLES DEL SISTEMA =====
enum EstadoSistema {
MENU_PRINCIPAL, // Navegando por el menú principal
JUGANDO, // Respondiendo preguntas
RESULTADO_PREGUNTA, // Viendo resultado de pregunta individual
RESULTADO_FINAL // Viendo resultado final del quiz
};
EstadoSistema estadoActual = MENU_PRINCIPAL;
int modoJuego = 0; // 0=V/F, 1=Múltiple, 2=Mixto
int opcionMenu = 0; // Opción seleccionada en el menú
int opcionPregunta = 0; // Opción seleccionada en la pregunta
int preguntaActual = 0; // Índice de pregunta actual
int puntaje = 0; // Puntaje acumulado
bool esperandoInput = true; // Control de entrada de botones
// Opciones de menú y respuestas
String menuPrincipal[] = {"Verdadero/Falso", "Opcion Multiple", "Modo Mixto"};
int totalMenus = 3;
String opcionesVF[] = {"Verdadero", "Falso"};
String opcionesMultiple[] = {"Opcion A", "Opcion B", "Opcion C"};
// ===== CARACTERES ESPECIALES =====
// Slot 0: Flecha (ya definida)
byte flecha[] = {B00000,B00100,B00110,B11111,B00110,B00100,B00000,B00000};
// Slots 1-5: Vocales acentuadas minúsculas (por defecto)
byte a_acento[] = {B00010,B00100,B01110,B00001,B01111,B10001,B01111,B00000}; // á
byte e_acento[] = {B00010,B00100,B01110,B10001,B11111,B10000,B01110,B00000}; // é
byte i_acento[] = {B00010,B00100,B00000,B01110,B00100,B00100,B01110,B00000}; // í
byte o_acento[] = {B00010,B00100,B01110,B10001,B10001,B10001,B01110,B00000}; // ó
byte u_acento[] = {B00010,B00100,B00000,B10001,B10001,B10001,B01111,B00000}; // ú
// Caracteres especiales adicionales para uso dinámico
byte A_acento[] = {B00010,B00100,B01110,B10001,B11111,B10001,B10001,B00000}; // Á
byte E_acento[] = {B00010,B00100,B11111,B10000,B11110,B10000,B11111,B00000}; // É
byte I_acento[] = {B00010,B00100,B01110,B00100,B00100,B00100,B01110,B00000}; // Í
byte O_acento[] = {B00010,B00100,B01110,B10001,B10001,B10001,B01110,B00000}; // Ó
byte U_acento[] = {B00010,B00100,B10001,B10001,B10001,B10001,B01110,B00000}; // Ú
byte n_tilde[] = {B01010,B10100,B00000,B11110,B10001,B10001,B10001,B00000}; // ñ
byte N_tilde[] = {B01010,B10100,B10001,B11001,B10101,B10011,B10001,B00000}; // Ñ
// Estructura para mapeo de caracteres especiales
struct CaracterEspecial {
String caracter; // Usamos String para manejar caracteres UTF-8
byte* patron;
char reemplazo; // Carácter de reemplazo si no se puede mostrar
};
// Lista de caracteres especiales disponibles
CaracterEspecial caracteresEspeciales[] = {
{"á", a_acento, 'a'},
{"é", e_acento, 'e'},
{"í", i_acento, 'i'},
{"ó", o_acento, 'o'},
{"ú", u_acento, 'u'},
{"Á", A_acento, 'A'},
{"É", E_acento, 'E'},
{"Í", I_acento, 'I'},
{"Ó", O_acento, 'O'},
{"Ú", U_acento, 'U'},
{"ñ", n_tilde, 'n'},
{"Ñ", N_tilde, 'N'}
};
int totalCaracteresEspeciales = sizeof(caracteresEspeciales) / sizeof(caracteresEspeciales[0]);
// Mapeo de caracteres especiales actualmente cargados
struct MapeoCaracter {
String caracter; // Cambiado a String para manejar caracteres UTF-8
int slot;
bool enUso;
};
MapeoCaracter mapeoActual[8]; // Slots 0-7
// ===== PROTOTIPOS DE FUNCIONES =====
// Funciones principales
void mostrarBienvenida();
void mostrarMenuPrincipal();
void mostrarInfoJuego();
void mostrarPreguntaActual();
void mostrarOpcionesPregunta(int tipo);
void mostrarCorrecto();
void mostrarIncorrecto();
void mostrarResultadoFinal();
// Manejadores de estado
void manejarMenuPrincipal();
void manejarJuego();
void manejarResultadoPregunta();
void manejarResultadoFinal();
// Lógica del juego
void iniciarJuego();
void reiniciarSistema();
void procesarRespuesta();
void siguientePregunta();
// Funciones auxiliares
int obtenerTipoPreguntaActual();
int obtenerTotalPreguntas();
int obtenerRespuestaCorrecta();
// Funciones de caracteres especiales
void inicializarCaracteresEspeciales();
void restaurarVocalesAcentuadas();
String procesarTextoEspecial(String texto);
int asignarCaracterEspecial(char caracter);
void liberarSlotsTemporales();
// ===== CONFIGURACIÓN INICIAL =====
void setup() {
// Configurar botones con resistencias pull-up internas
pinMode(btnUp, INPUT_PULLUP);
pinMode(btnDown, INPUT_PULLUP);
pinMode(btnOK, INPUT_PULLUP);
// Inicializar LCD
lcd.init();
lcd.backlight();
// Configurar caracteres especiales
inicializarCaracteresEspeciales();
// Mostrar pantalla de bienvenida
mostrarBienvenida();
delay(3000);
mostrarMenuPrincipal();
}
// ===== BUCLE PRINCIPAL =====
void loop() {
// Máquina de estados principal
switch (estadoActual) {
case MENU_PRINCIPAL:
manejarMenuPrincipal();
break;
case JUGANDO:
manejarJuego();
break;
case RESULTADO_PREGUNTA:
manejarResultadoPregunta();
break;
case RESULTADO_FINAL:
manejarResultadoFinal();
break;
}
}
// ===== MANEJADORES DE ESTADO =====
/**
* Maneja la navegación en el menú principal
*/
void manejarMenuPrincipal() {
if (digitalRead(btnUp) == LOW) {
delay(200); // Debounce
opcionMenu--;
if (opcionMenu < 0) opcionMenu = totalMenus - 1;
mostrarMenuPrincipal();
}
if (digitalRead(btnDown) == LOW) {
delay(200); // Debounce
opcionMenu++;
if (opcionMenu >= totalMenus) opcionMenu = 0;
mostrarMenuPrincipal();
}
if (digitalRead(btnOK) == LOW) {
delay(200); // Debounce
iniciarJuego();
}
}
/**
* Maneja la navegación durante el juego
*/
void manejarJuego() {
if (!esperandoInput) return;
// Determinar tipo y opciones de pregunta actual
int tipoPregunta = obtenerTipoPreguntaActual();
int maxOpciones = (tipoPregunta == 0) ? 2 : 3; // V/F = 2, Múltiple = 3
if (digitalRead(btnUp) == LOW) {
delay(200); // Debounce
opcionPregunta--;
if (opcionPregunta < 0) opcionPregunta = maxOpciones - 1;
mostrarOpcionesPregunta(tipoPregunta);
}
if (digitalRead(btnDown) == LOW) {
delay(200); // Debounce
opcionPregunta++;
if (opcionPregunta >= maxOpciones) opcionPregunta = 0;
mostrarOpcionesPregunta(tipoPregunta);
}
if (digitalRead(btnOK) == LOW) {
delay(200); // Debounce
procesarRespuesta();
}
}
/**
* Maneja la transición después de mostrar resultado de pregunta
*/
void manejarResultadoPregunta() {
if (digitalRead(btnOK) == LOW) {
delay(200); // Debounce
siguientePregunta();
}
}
/**
* Maneja la transición desde el resultado final
*/
void manejarResultadoFinal() {
if (digitalRead(btnOK) == LOW) {
delay(200); // Debounce
reiniciarSistema();
}
}
// ===== FUNCIONES DE PANTALLA =====
/**
* Muestra la pantalla de bienvenida
*/
void mostrarBienvenida() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(procesarTextoEspecial(String(bienvenidaF1)));
lcd.setCursor(0, 1);
lcd.print(procesarTextoEspecial(String(bienvenidaF2)));
}
/**
* Muestra el menú principal con navegación
*/
void mostrarMenuPrincipal() {
lcd.clear();
// Mostrar opción actual con flecha
lcd.setCursor(0, 0);
lcd.write(byte(0)); // Flecha
lcd.setCursor(1, 0);
lcd.print(procesarTextoEspecial(menuPrincipal[opcionMenu]));
// Mostrar siguiente opción disponible
int siguienteOpcion = (opcionMenu + 1 < totalMenus) ? opcionMenu + 1 : 0;
lcd.setCursor(1, 1);
lcd.print(procesarTextoEspecial(menuPrincipal[siguienteOpcion]));
}
/**
* Muestra información del modo de juego seleccionado
*/
void mostrarInfoJuego() {
lcd.clear();
// Mostrar modo de juego si está habilitado
if (mostrarModoJuego) {
lcd.setCursor(0, 0);
lcd.print("Modo: ");
switch (modoJuego) {
case 0:
lcd.print("V/F");
break;
case 1:
lcd.print("Múltiple");
break;
case 2:
lcd.print("Mixto");
break;
}
}
// Mostrar total de preguntas si está habilitado
if (mostrarTotalPreg) {
lcd.setCursor(0, 1);
lcd.print("Total: ");
lcd.print(obtenerTotalPreguntas());
lcd.print(" pregs.");
}
}
/**
* Muestra la pregunta actual con información de contexto
*/
void mostrarPreguntaActual() {
lcd.clear();
// Mostrar número de pregunta y puntaje si está habilitado
if (mostrarNumeroPreg_Punt) {
lcd.setCursor(0, 0);
lcd.print("P");
lcd.print(preguntaActual + 1);
lcd.print("/");
lcd.print(obtenerTotalPreguntas());
lcd.print(" Pts:");
lcd.print(puntaje);
delay(2000);
}
// Mostrar opciones según el tipo de pregunta
int tipoPregunta = obtenerTipoPreguntaActual();
mostrarOpcionesPregunta(tipoPregunta);
}
/**
* Muestra las opciones de respuesta según el tipo de pregunta
* @param tipo 0 = Verdadero/Falso, 1 = Múltiple
*/
void mostrarOpcionesPregunta(int tipo) {
lcd.clear();
if (tipo == 0) { // Verdadero/Falso
if (opcionPregunta == 0) {
lcd.setCursor(0, 0);
lcd.write(byte(0)); // Flecha
lcd.print(" ");
lcd.print(procesarTextoEspecial(opcionesVF[0]));
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.print(procesarTextoEspecial(opcionesVF[1]));
} else {
lcd.setCursor(0, 0);
lcd.print(" ");
lcd.print(procesarTextoEspecial(opcionesVF[0]));
lcd.setCursor(0, 1);
lcd.write(byte(0)); // Flecha
lcd.print(" ");
lcd.print(procesarTextoEspecial(opcionesVF[1]));
}
} else { // Múltiple
if (opcionPregunta == 0) {
lcd.setCursor(0, 0);
lcd.write(byte(0)); // Flecha
lcd.print(" ");
lcd.print(procesarTextoEspecial(opcionesMultiple[0]));
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.print(procesarTextoEspecial(opcionesMultiple[1]));
} else if (opcionPregunta == 1) {
lcd.setCursor(0, 0);
lcd.print(" ");
lcd.print(procesarTextoEspecial(opcionesMultiple[0]));
lcd.setCursor(0, 1);
lcd.write(byte(0)); // Flecha
lcd.print(" ");
lcd.print(procesarTextoEspecial(opcionesMultiple[1]));
} else { // opcionPregunta == 2
lcd.setCursor(0, 0);
lcd.print(" ");
lcd.print(procesarTextoEspecial(opcionesMultiple[1]));
lcd.setCursor(0, 1);
lcd.write(byte(0)); // Flecha
lcd.print(" ");
lcd.print(procesarTextoEspecial(opcionesMultiple[2]));
}
}
}
/**
* Muestra mensaje de respuesta correcta
*/
void mostrarCorrecto() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(procesarTextoEspecial(String(msjCorrecto)));
// Mostrar puntaje logrado si está habilitado
if (mostrarPuntosLogrados) {
lcd.setCursor(0, 1);
lcd.print("Puntaje: ");
lcd.print(puntaje);
}
delay(2000);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(procesarTextoEspecial(String(msjContinuar1)));
lcd.setCursor(0, 1);
lcd.print(procesarTextoEspecial(String(msjContinuar2)));
}
/**
* Muestra mensaje de respuesta incorrecta con retroalimentación
*/
void mostrarIncorrecto() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(procesarTextoEspecial(String(msjIncorrecto)));
// Mostrar respuesta correcta si está habilitado
if (mostrarOpCorrecta) {
int tipoPregunta = obtenerTipoPreguntaActual();
int respuestaCorrecta = obtenerRespuestaCorrecta();
if (tipoPregunta == 1) { // Solo para preguntas múltiples
lcd.setCursor(0, 1);
lcd.print("Era: Opcion ");
lcd.print((char)('A' + respuestaCorrecta));
}
}
delay(3000);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(procesarTextoEspecial(String(msjContinuar1)));
lcd.setCursor(0, 1);
lcd.print(procesarTextoEspecial(String(msjContinuar2)));
}
/**
* Muestra el resultado final del quiz con estadísticas
*/
void mostrarResultadoFinal() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(procesarTextoEspecial(String(msjFinal)));
// Mostrar puntos totales si está habilitado
if (mostrarPuntosTotal) {
lcd.setCursor(0, 1);
lcd.print("Puntos: ");
lcd.print(puntaje);
lcd.print("/");
lcd.print(obtenerTotalPreguntas());
}
delay(3000);
// Calcular y mostrar retroalimentación
int porcentaje = (puntaje * 100) / obtenerTotalPreguntas();
lcd.clear();
lcd.setCursor(0, 0);
// Mostrar mensaje motivacional si está habilitado
if (mostrarRetroalimentacion) {
if (porcentaje >= 80) {
lcd.print(procesarTextoEspecial(String(msjAciertos80)));
} else if (porcentaje >= 60) {
lcd.print(procesarTextoEspecial(String(msjAciertos60)));
} else {
lcd.print(procesarTextoEspecial(String(msjAciertosBajo)));
}
}
// Mostrar porcentaje de aciertos si está habilitado
if (mostrarAciertos) {
lcd.setCursor(0, 1);
lcd.print("Aciertos: ");
lcd.print(porcentaje);
lcd.print("%");
}
delay(3000);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(procesarTextoEspecial(String(msjVolver1)));
lcd.setCursor(0, 1);
lcd.print(procesarTextoEspecial(String(msjVolver2)));
}
// ===== LÓGICA DEL JUEGO =====
/**
* Inicializa un nuevo juego
*/
void iniciarJuego() {
modoJuego = opcionMenu;
preguntaActual = 0;
puntaje = 0;
opcionPregunta = 0;
mostrarInfoJuego();
delay(2000);
estadoActual = JUGANDO;
esperandoInput = true;
mostrarPreguntaActual();
}
/**
* Procesa la respuesta del usuario y actualiza el puntaje
*/
void procesarRespuesta() {
bool esCorrecta = false;
int respuestaCorrecta = obtenerRespuestaCorrecta();
// Evaluar respuesta según el tipo de pregunta
esCorrecta = (opcionPregunta == respuestaCorrecta);
if (esCorrecta) {
puntaje++;
mostrarCorrecto();
} else {
mostrarIncorrecto();
}
esperandoInput = false;
estadoActual = RESULTADO_PREGUNTA;
}
/**
* Avanza a la siguiente pregunta o finaliza el quiz
*/
void siguientePregunta() {
preguntaActual++;
if (preguntaActual >= obtenerTotalPreguntas()) {
mostrarResultadoFinal();
estadoActual = RESULTADO_FINAL;
} else {
opcionPregunta = 0;
esperandoInput = true;
estadoActual = JUGANDO;
mostrarPreguntaActual();
}
}
/**
* Reinicia el sistema al menú principal
*/
void reiniciarSistema() {
estadoActual = MENU_PRINCIPAL;
opcionMenu = 0;
preguntaActual = 0;
puntaje = 0;
opcionPregunta = 0;
esperandoInput = true;
// Restaurar caracteres especiales por defecto
restaurarVocalesAcentuadas();
mostrarMenuPrincipal();
}
// ===== FUNCIONES AUXILIARES =====
/**
* Obtiene el total de preguntas según el modo de juego
*/
int obtenerTotalPreguntas() {
switch (modoJuego) {
case 0: return totalVF;
case 1: return totalMultiple;
case 2: return totalMixto;
default: return 0;
}
}
/**
* Obtiene el tipo de la pregunta actual (0=V/F, 1=Múltiple)
*/
int obtenerTipoPreguntaActual() {
switch (modoJuego) {
case 0: return 0; // Solo V/F
case 1: return 1; // Solo Múltiple
case 2: return preguntasMixtas[preguntaActual].tipo; // Modo Mixto
default: return 0;
}
}
/**
* Obtiene la respuesta correcta para la pregunta actual
*/
int obtenerRespuestaCorrecta() {
switch (modoJuego) {
case 0: return preguntasVF[preguntaActual] ? 1 : 0;
case 1: return preguntasMultiple[preguntaActual];
case 2: return preguntasMixtas[preguntaActual].respuesta;
default: return 0;
}
}
// ===== FUNCIONES DE CARACTERES ESPECIALES =====
/**
* Inicializa los caracteres especiales en el LCD
*/
void inicializarCaracteresEspeciales() {
// Slot 0: Flecha de navegación
lcd.createChar(0, flecha);
// Inicializar mapeo
for (int i = 0; i < 8; i++) {
mapeoActual[i].caracter = "";
mapeoActual[i].slot = i;
mapeoActual[i].enUso = false;
}
mapeoActual[0].enUso = true; // Slot 0 reservado para flecha
// Cargar vocales acentuadas por defecto
restaurarVocalesAcentuadas();
}
/**
* Restaura las vocales acentuadas minúsculas en los slots 1-5
*/
void restaurarVocalesAcentuadas() {
lcd.createChar(1, a_acento);
lcd.createChar(2, e_acento);
lcd.createChar(3, i_acento);
lcd.createChar(4, o_acento);
lcd.createChar(5, u_acento);
// Actualizar mapeo
/* mapeoActual[1] = {'á', 1, true};
mapeoActual[2] = {'é', 2, true};
mapeoActual[3] = {'í', 3, true};
mapeoActual[4] = {'ó', 4, true};
mapeoActual[5] = {'ú', 5, true};*/
mapeoActual[1].caracter = "á";
mapeoActual[1].slot = 1;
mapeoActual[1].enUso = true;
mapeoActual[2].caracter = "é";
mapeoActual[2].slot = 2;
mapeoActual[2].enUso = true;
mapeoActual[3].caracter = "í";
mapeoActual[3].slot = 3;
mapeoActual[3].enUso = true;
mapeoActual[4].caracter = "ó";
mapeoActual[4].slot = 4;
mapeoActual[4].enUso = true;
mapeoActual[5].caracter = "ú";
mapeoActual[5].slot = 5;
mapeoActual[5].enUso = true;
// Slots 6 y 7 disponibles para uso dinámico
mapeoActual[6].enUso = false;
mapeoActual[7].enUso = false;
}
/**
* Procesa texto con caracteres especiales y los mapea al LCD
* @param texto String a procesar
* @return String procesado con códigos de caracteres especiales
*/
String procesarTextoEspecial(String texto) {
String textoProcessado = texto;
// Contar caracteres especiales en el texto
int caracteresEspecialesEncontrados = 0;
bool necesitaSlotsDinamicos = false;
// Primer paso: identificar caracteres especiales
for (size_t i = 0; i < texto.length(); i++) {
char c = texto.charAt(i);
bool encontrado = false;
// Verificar si ya está mapeado
for (int j = 0; j < 8; j++) {
if (mapeoActual[j].enUso && mapeoActual[j].caracter == String(c)) {
encontrado = true;
break;
}
}
// Si no está mapeado, buscar en caracteres especiales
if (!encontrado) {
for (int k = 0; k < totalCaracteresEspeciales; k++) {
if (caracteresEspeciales[k].caracter == String(c)) {
caracteresEspecialesEncontrados++;
break;
}
}
}
}
// Si hay más de 2 caracteres especiales nuevos, usar slots dinámicos
if (caracteresEspecialesEncontrados > 2) {
necesitaSlotsDinamicos = true;
liberarSlotsTemporales();
}
// Segundo paso: asignar y reemplazar caracteres
for (size_t i = 0; i < texto.length(); i++) {
char c = texto.charAt(i);
// Buscar si ya está mapeado
int slotAsignado = -1;
for (int j = 0; j < 8; j++) {
if (mapeoActual[j].enUso && mapeoActual[j].caracter == String(c)) {
slotAsignado = j;
break;
}
}
// Si no está mapeado, intentar asignarlo
if (slotAsignado == -1) {
slotAsignado = asignarCaracterEspecial(c);
}
// Reemplazar en el texto si se asignó un slot
if (slotAsignado != -1) {
textoProcessado.setCharAt(i, (char)slotAsignado);
}
}
// Si se usaron slots dinámicos temporalmente, restaurar después de un tiempo
if (necesitaSlotsDinamicos) {
// Nota: La restauración se hará al finalizar la función que usa el texto
// o al cambiar de pantalla
}
return textoProcessado;
}
/**
* Asigna un carácter especial a un slot disponible del LCD
* @param caracter Carácter a asignar
* @return Slot asignado (-1 si no se pudo asignar)
*/
int asignarCaracterEspecial(char caracter) {
// Buscar el carácter en la lista de especiales
byte* patron = nullptr;
for (int i = 0; i < totalCaracteresEspeciales; i++) {
if (caracteresEspeciales[i].caracter == String(caracter)) {
patron = caracteresEspeciales[i].patron;
break;
}
}
if (patron == nullptr) return -1; // Carácter no encontrado
// Buscar slot disponible (prioridad: 6, 7, luego 1-5 si es necesario)
int slotDisponible = -1;
// Primero intentar slots dinámicos (6, 7)
if (!mapeoActual[6].enUso) {
slotDisponible = 6;
} else if (!mapeoActual[7].enUso) {
slotDisponible = 7;
} else {
// Si es crítico, usar slots 1-5 temporalmente
for (int i = 1; i <= 5; i++) {
if (!mapeoActual[i].enUso) {
slotDisponible = i;
break;
}
}
}
if (slotDisponible != -1) {
// Asignar el carácter al slot
lcd.createChar(slotDisponible, patron);
mapeoActual[slotDisponible].caracter = caracter;
mapeoActual[slotDisponible].enUso = true;
return slotDisponible;
}
return -1; // No hay slots disponibles
}
/**
* Libera los slots temporales 6 y 7 para nuevos caracteres
*/
void liberarSlotsTemporales() {
mapeoActual[6].caracter = "";
mapeoActual[6].enUso = false;
mapeoActual[7].caracter = "";
mapeoActual[7].enUso = false;
}
/**
* Libera slots 1-5 temporalmente si se necesita espacio adicional
* Solo debe usarse cuando hay más de 2 caracteres especiales en una pantalla
*/
void liberarVocalesTemporalmente() {
for (int i = 1; i <= 5; i++) {
mapeoActual[i].enUso = false;
}
}
/**
* Función auxiliar para debugging - muestra el estado actual del mapeo
* (Opcional, solo para desarrollo)
*/
void mostrarEstadoMapeo() {
// Esta función puede ser útil durante el desarrollo para verificar
// qué caracteres están cargados en cada slot
// No es necesaria en producción
}
// ===== FUNCIONES ADICIONALES DE UTILIDAD =====
/**
* Función para agregar delay con posibilidad de interrupción por botón
* Útil para delays largos que el usuario puede querer saltar
* @param tiempoMs Tiempo en milisegundos
* @return true si se completó el delay, false si se interrumpió
*/
bool delayInterrumpible(unsigned long tiempoMs) {
unsigned long inicio = millis();
while (millis() - inicio < tiempoMs) {
if (digitalRead(btnOK) == LOW || digitalRead(btnUp) == LOW || digitalRead(btnDown) == LOW) {
delay(200); // Debounce
return false; // Interrumpido
}
delay(10); // Pequeño delay para no saturar el procesador
}
return true; // Completado
}
/**
* Función para validar la configuración de preguntas
* Verifica que haya al menos una pregunta en cada modo
* @return true si la configuración es válida
*/
bool validarConfiguracion() {
if (totalVF < 1 || totalMultiple < 1 || totalMixto < 1) {
// Mostrar error en LCD si algún modo no tiene preguntas
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("ERROR CONFIG");
lcd.setCursor(0, 1);
lcd.print("Falta preguntas");
delay(3000);
return false;
}
return true;
}
/**
* Función para mostrar estadísticas detalladas (opcional)
* Puede activarse con una constante adicional
*/
void mostrarEstadisticasDetalladas() {
// Esta función podría mostrar información adicional como:
// - Tiempo promedio por pregunta
// - Preguntas más difíciles
// - Histórico de puntajes
// Se deja como extensión futura
}
/*
* ===== NOTAS DE IMPLEMENTACIÓN =====
*
* 1. GESTIÓN DE MEMORIA:
* - Los arrays de preguntas se almacenan en SRAM
* - Para proyectos con muchas preguntas, considerar usar PROGMEM
* - Los strings constantes podrían moverse a PROGMEM para ahorrar RAM
*
* 2. CARACTERES ESPECIALES:
* - El sistema maneja automáticamente hasta 5 caracteres especiales simultáneos
* - Los slots 1-5 están reservados para vocales acentuadas por defecto
* - Los slots 6-7 se usan dinámicamente para otros caracteres
* - Si una pantalla necesita más de 7 caracteres especiales, algunos se mostrarán sin acento
*
* 3. EXPANSIÓN FUTURA:
* - Fácil agregar nuevos modos de juego modificando el enum EstadoSistema
* - Las constantes de configuración permiten personalizar la interfaz sin tocar la lógica
* - El sistema de caracteres especiales es extensible agregando patrones al array
*
* 4. DEPURACIÓN:
* - Los delays incluyen debounce para evitar lecturas múltiples de botones
* - Los estados están claramente separados para facilitar el debugging
* - Cada función tiene una responsabilidad específica
*
* 5. PERSONALIZACIÓN:
* - Modificar las constantes al inicio para cambiar textos mostrados
* - Las constantes booleanas permiten activar/desactivar funcionalidades
* - Los arrays de preguntas son fáciles de modificar y expandir
*
* 6. LIMITACIONES CONOCIDAS:
* - Máximo 8 caracteres especiales simultáneos (limitación del LCD)
* - Los textos largos pueden no caber en pantalla (16x2)
* - No hay persistencia de datos (se reinicia con cada power cycle)
*/OK
Up
Down