Enlace a la aplicación html
Aplicación html en un Site
Código html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>EduAnimales - Aprende Jugando</title>
<style>
:root {
/* Paleta de Alto Contraste */
--bg-color: #121212;
--text-color: #FFFFFF;
--primary: #FFEB3B; /* Amarillo brillante */
--secondary: #00E5FF; /* Cian brillante */
--tertiary: #FF4081; /* Rosa brillante */
--success: #00E676; /* Verde brillante */
--error: #FF5252; /* Rojo claro */
--card-bg: #2C2C2C;
--focus-outline: 6px solid #FFEB3B;
--font-main: 'Segoe UI', system-ui, -apple-system, sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none; /* Evitar selección de texto al tocar */
-webkit-tap-highlight-color: transparent;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: var(--font-main);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden; /* Evitar scroll */
}
/* Utilidades de Accesibilidad */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
*:focus-visible {
outline: var(--focus-outline);
outline-offset: 4px;
border-radius: 8px;
z-index: 10;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
height: 15vh;
min-height: 88px;
}
main {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 1rem;
position: relative;
}
button {
font-family: inherit;
cursor: pointer;
border: 4px solid #FFF;
border-radius: 24px;
background-color: var(--card-bg);
color: var(--text-color);
font-size: 2rem;
font-weight: 800;
min-width: 88px;
min-height: 88px;
padding: 1rem 2rem;
box-shadow: 0 8px 0 #000;
transition: transform 0.1s, box-shadow 0.1s, filter 0.2s;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
}
button:active {
transform: translateY(8px);
box-shadow: 0 0 0 #000;
}
.btn-icon {
font-size: 3rem;
padding: 1rem;
border-radius: 50%;
}
.btn-primary { background-color: var(--primary); color: #000; border-color: #000; }
.btn-secondary { background-color: var(--secondary); color: #000; border-color: #000; }
.btn-tertiary { background-color: var(--tertiary); color: #FFF; border-color: #FFF; }
.btn-success { background-color: var(--success); color: #000; border-color: #000; }
.screen {
display: none;
width: 100%;
height: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
animation: fadeIn 0.4s ease-out;
}
.screen.active {
display: flex;
}
h1, h2 {
font-size: clamp(2rem, 5vw, 4rem);
text-align: center;
margin-bottom: 2rem;
line-height: 1.2;
}
.game-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 2rem;
width: 100%;
max-width: 900px;
padding: 1rem;
}
.animal-btn {
background-color: var(--card-bg);
border: 6px solid #FFF;
aspect-ratio: 1 / 1;
padding: 1rem;
}
.animal-btn svg, .animal-btn img {
width: 100%;
height: 100%;
max-height: 150px;
object-fit: cover;
border-radius: 8px;
}
/* Animaciones */
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-20px); }
}
.bounce { animation: bounce 0.5s ease; }
#celebration {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.85);
z-index: 100;
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 3rem;
text-align: center;
padding: 2rem;
}
.star {
position: absolute;
font-size: 4rem;
color: var(--primary);
animation: bounce 1s infinite alternate;
}
#teacher-panel {
background: var(--bg-color);
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
z-index: 200;
padding: 2rem;
display: none;
flex-direction: column;
overflow-y: auto;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin-top: 2rem;
}
.stat-card {
background: var(--card-bg);
padding: 1.5rem;
border-radius: 16px;
text-align: center;
border: 2px solid #FFF;
font-size: 1.5rem;
}
/* Pantalla inicial de desbloqueo (Necesaria para Audio API en navegadores) */
#unlock-screen {
position: fixed; inset: 0; background: var(--bg-color); z-index: 999;
display: flex; flex-direction: column; justify-content: center; align-items: center;
}
</style>
</head>
<body>
<!-- Región ARIA Live para anuncios de lectores de pantalla -->
<div id="aria-announcer" class="sr-only" aria-live="assertive" aria-atomic="true"></div>
<div id="unlock-screen">
<button id="btn-start" class="btn-primary" style="padding: 3rem; font-size: 3rem; border-radius: 40px;" aria-label="Tocar la pantalla para empezar a jugar">
<svg width="80" height="80" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
¡Toca para Empezar!
</button>
</div>
<header>
<button id="btn-back" class="btn-icon btn-secondary" aria-label="Volver al menú principal" style="display: none;">
<svg width="40" height="40" viewBox="0 0 24 24" fill="currentColor"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<button id="btn-sound" class="btn-icon" aria-label="Desactivar sonido" style="background:#FFF; color:#000; margin-left: auto;">
<svg width="40" height="40" viewBox="0 0 24 24" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>
</button>
</header>
<main>
<!-- Menú Principal -->
<div id="screen-menu" class="screen active" role="region" aria-label="Menú principal">
<h1 id="menu-title">Elige un juego</h1>
<div class="game-grid" style="max-width: 600px;">
<button class="btn-primary" onclick="app.startGame(1)" aria-label="Juego 1: ¿Qué animal es? Adivina el sonido">
<svg width="60" height="60" viewBox="0 0 24 24" fill="currentColor"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>
¿Qué animal es?
</button>
<button class="btn-secondary" onclick="app.startGame(2)" aria-label="Juego 2: Escucha y Toca. Encuentra al animal">
<svg width="60" height="60" viewBox="0 0 24 24" fill="currentColor"><path d="M9 11.24V7.5C9 6.12 10.12 5 11.5 5S14 6.12 14 7.5v3.74c1.21-.81 2-2.18 2-3.74C16 5.01 13.99 3 11.5 3S7 5.01 7 7.5c0 1.56.79 2.93 2 3.74zm9.84 4.63l-4.54-2.26c-.17-.07-.35-.11-.54-.11H13v-6c0-.83-.67-1.5-1.5-1.5S10 6.67 10 7.5v10.74l-3.43-.72c-.08-.01-.15-.03-.24-.03-.31 0-.59.13-.79.33l-.79.8 4.94 4.94c.27.27.65.44 1.04.44h6.79c.75 0 1.33-.55 1.44-1.28l.75-5.27c.01-.07.02-.14.02-.2 0-.62-.38-1.16-.91-1.38z"/></svg>
Escucha y Toca
</button>
<button class="btn-tertiary" onclick="app.startGame(3)" aria-label="Juego 3: Parejas. Encuentra los animales iguales">
<svg width="60" height="60" viewBox="0 0 24 24" fill="currentColor"><path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z"/></svg>
Parejas
</button>
</div>
<!-- Botón discreto para docentes (Requiere 3 toques rápidos) -->
<button id="btn-teacher" aria-label="Área de profesores. Toca 3 veces para acceder." style="position:absolute; bottom: 10px; right: 10px; min-width:60px; min-height:60px; opacity: 0.3; border: 2px solid #555; background: transparent;"></button>
</div>
<!-- Contenedor de Minijuegos -->
<div id="screen-game" class="screen" role="region" aria-label="Zona de juego">
<h2 id="game-instruction" style="margin-bottom: 1rem;">Escucha atentamente...</h2>
<button id="btn-repeat" class="btn-icon btn-primary" aria-label="Repetir instrucción" style="margin-bottom: 2rem; border-radius: 50%; padding: 1rem;">
<svg width="40" height="40" viewBox="0 0 24 24" fill="currentColor"><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/></svg>
</button>
<div id="game-board" class="game-grid">
<!-- Los botones de animales se inyectarán aquí -->
</div>
<div id="post-game-controls" style="display: none; flex-direction: column; gap: 1.5rem; margin-top: 2rem; width: 100%; max-width: 400px; align-items: center;">
<button id="btn-repeat-exercise" class="btn-primary" style="font-size: 2rem; border-radius: 40px; padding: 1.5rem 2rem; width: 100%; flex-direction: row; gap: 1rem;" aria-label="Repetir este mismo ejercicio">
<svg width="36" height="36" viewBox="0 0 24 24" fill="currentColor"><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/></svg>
Repetir Ejercicio
</button>
<button id="btn-next" class="btn-success bounce" style="font-size: 2.5rem; border-radius: 40px; padding: 1.5rem 3rem; width: 100%; flex-direction: row; gap: 1rem;" aria-label="Jugar otro">
Siguiente <svg width="40" height="40" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
</button>
</div>
</div>
<!-- Panel Docente -->
<div id="teacher-panel" role="region" aria-label="Panel de estadísticas para docentes">
<h2>Panel Docente (Estadísticas)</h2>
<div style="text-align: center; margin-bottom: 1rem;">
<p style="font-size: 1.5rem;">Total Aciertos Históricos: <strong id="stat-total" style="color: var(--primary);">0</strong></p>
</div>
<div id="stats-container" class="stats-grid"></div>
<div style="margin-top: 3rem; display: flex; gap: 1rem; justify-content: center;">
<button class="btn-error" onclick="app.resetStats()" aria-label="Borrar todos los datos" style="background: var(--error); color: #FFF; border-color:#FFF; font-size: 1.5rem;">Borrar Datos</button>
<button class="btn-secondary" onclick="app.hideTeacherPanel()" aria-label="Cerrar panel y volver al menú" style="font-size: 1.5rem;">Cerrar Panel</button>
</div>
</div>
</main>
<!-- Overlay de Celebración -->
<div id="celebration" aria-hidden="true">
<div style="font-size: 8rem; margin-bottom: 1rem;">🏆</div>
<h2 id="celebration-text" style="color: var(--primary);">¡Lo estás haciendo genial!</h2>
</div>
<script>
// --- 1. DATOS DE LOS ANIMALES (Imágenes y Sonidos Reales) ---
// Se usan fuentes externas de alta fiabilidad (Wikimedia Commons y Archive.org).
const ANIMALS = [
{
id: 'leon', name: 'León', color: '#FF9800',
imgUrl: 'https://upload.wikimedia.org/wikipedia/commons/7/73/Lion_waiting_in_Namibia.jpg',
audioUrl: 'https://archive.org/download/valentinosfxdvd1cd9/51.%20Lion%20-%20Roar%2C%20Single.mp3'
},
{
id: 'caballo', name: 'Caballo', color: '#795548',
imgUrl: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Nokota_Horses_cropped.jpg',
audioUrl: 'https://archive.org/download/valentinosfxdvd1cd9/64.%20Horse%20-%20Whinnying.mp3'
},
{
id: 'gato', name: 'Gato', color: '#9C27B0',
imgUrl: 'https://upload.wikimedia.org/wikipedia/commons/4/4d/Cat_November_2010-1a.jpg',
audioUrl: 'https://archive.org/download/valentinosfxdvd1cd9/43.%20Cat%20-%20Meow.mp3'
},
{
id: 'perro', name: 'Perro', color: '#03A9F4',
imgUrl: 'https://upload.wikimedia.org/wikipedia/commons/4/43/Cute_dog.jpg',
audioUrl: 'https://archive.org/download/animals-and-birds-sound-effects/14%20Small%20Dog%20Barks.mp3'
},
{
id: 'pato', name: 'Pato', color: '#FFEB3B',
imgUrl: 'https://upload.wikimedia.org/wikipedia/commons/b/bf/Anas_platyrhynchos_male_female_quadrat.jpg',
audioUrl: 'https://archive.org/download/valentinosfxdvd1cd9/65.%20Bird%20-%20Duck%20-%20Quacking.mp3'
},
{
id: 'gallo', name: 'Gallo', color: '#FFFFFF',
imgUrl: 'https://upload.wikimedia.org/wikipedia/commons/5/52/Brown_Leghorn_rooster_in_Australia.jpg',
audioUrl: 'https://archive.org/download/Red_Library_Animals_Birds/R01-73-Rooster%20Crowing.mp3'
},
{
id: 'oveja', name: 'Oveja', color: '#EEEEEE',
imgUrl: 'https://upload.wikimedia.org/wikipedia/commons/2/2c/Flock_of_sheep.jpg',
audioUrl: 'https://archive.org/download/animals-and-birds-sound-effects/58%20Sheep%20Baby%20Crying%2C%20Adult%20Calling%20X%204.mp3'
},
{
id: 'vaca', name: 'Vaca', color: '#FFFFFF',
imgUrl: 'https://upload.wikimedia.org/wikipedia/commons/0/0c/Cow_female_black_white.jpg',
audioUrl: 'https://archive.org/download/animals-and-birds-sound-effects/32%20Cow%20Single%20MOO%20X%206.mp3'
}
];
const FRASES_REFUERZO = [
"¡Lo estás haciendo genial!",
"¡Eres un crack de los animales!",
"¡Muy bien hecho, sigue así!",
"¡Fantástico! Qué inteligente eres.",
"¡Increíble, has acertado!"
];
const FRASES_ACIERTO = [
"¡Muy bien!",
"¡Correcto!",
"¡Excelente!",
"¡Fantástico!",
"¡Genial!"
];
// --- 2. SISTEMA DE AUDIO (Web Speech API y Web Audio API) ---
class AudioSystem {
constructor() {
this.enabled = true;
this.audioCtx = null;
this.synth = window.speechSynthesis;
this.voicesLoaded = false;
this.currentRealAudio = null; // Guardar referencia al audio real
// Forzar carga de voces (workaround para algunos navegadores)
if (speechSynthesis.onvoiceschanged !== undefined) {
speechSynthesis.onvoiceschanged = () => { this.voicesLoaded = true; };
}
}
init() {
if (!this.audioCtx) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
this.audioCtx = new AudioContext();
}
if (this.audioCtx.state === 'suspended') {
this.audioCtx.resume();
}
// Desbloqueo silencioso del TTS en iOS
const u = new SpeechSynthesisUtterance('');
u.volume = 0;
this.synth.speak(u);
}
toggle() {
this.enabled = !this.enabled;
if (!this.enabled) {
this.synth.cancel();
if (this.currentRealAudio) this.currentRealAudio.pause();
}
return this.enabled;
}
speak(text, isAnimalSound = false, callback = null) {
if (!this.enabled) {
if (callback) callback();
return;
}
this.synth.cancel(); // Parar habla actual
// NO pausamos el sonido real aquí para permitir que la voz descriptiva
// y el efecto de sonido del animal puedan coexistir fluidamente.
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'es-ES'; // Forzar Español de España
utterance.volume = 1;
if (isAnimalSound) {
utterance.pitch = 1.4; // Tono más alto para hacerlo "divertido/onomatopéyico"
utterance.rate = 0.8; // Un poco más lento
} else {
utterance.pitch = 1.0;
utterance.rate = 0.95; // Habla pausada y clara para niños
}
if (callback) utterance.onend = callback;
this.synth.speak(utterance);
}
// Sintetizador básico para efectos sonoros de interfaz (Acierto, Error, Clic)
playSoundEffect(type) {
if (!this.enabled || !this.audioCtx) return;
const osc = this.audioCtx.createOscillator();
const gain = this.audioCtx.createGain();
osc.connect(gain);
gain.connect(this.audioCtx.destination);
const now = this.audioCtx.currentTime;
switch(type) {
case 'click':
osc.type = 'sine';
osc.frequency.setValueAtTime(600, now);
osc.frequency.exponentialRampToValueAtTime(300, now + 0.1);
gain.gain.setValueAtTime(0.5, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
osc.start(now);
osc.stop(now + 0.1);
break;
case 'success':
osc.type = 'triangle';
osc.frequency.setValueAtTime(440, now);
osc.frequency.setValueAtTime(554.37, now + 0.1); // C#
osc.frequency.setValueAtTime(659.25, now + 0.2); // E
gain.gain.setValueAtTime(0.5, now);
gain.gain.linearRampToValueAtTime(0, now + 0.5);
osc.start(now);
osc.stop(now + 0.5);
break;
case 'error':
// Sonido suave y menos estridente (onda senoidal en lugar de sierra)
osc.type = 'sine';
osc.frequency.setValueAtTime(300, now);
osc.frequency.exponentialRampToValueAtTime(200, now + 0.4);
gain.gain.setValueAtTime(0.5, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.4);
osc.start(now);
osc.stop(now + 0.4);
break;
case 'celebration':
// Arpegio feliz
osc.type = 'square';
[261.63, 329.63, 392.00, 523.25].forEach((freq, i) => {
osc.frequency.setValueAtTime(freq, now + i*0.15);
});
gain.gain.setValueAtTime(0.3, now);
gain.gain.linearRampToValueAtTime(0, now + 0.8);
osc.start(now);
osc.stop(now + 0.8);
break;
}
}
playRealSound(url, callback = null) {
if (!this.enabled) {
if (callback) callback();
return;
}
// NO detenemos el sintetizador de voz (TTS) para evitar que
// bloquee el nombre del animal si el alumno explora con el ratón.
if (this.currentRealAudio) {
this.currentRealAudio.pause();
this.currentRealAudio.currentTime = 0;
}
this.currentRealAudio = new Audio(url);
this.currentRealAudio.volume = 1.0;
if (callback) {
this.currentRealAudio.onended = callback;
}
this.currentRealAudio.play().catch(e => {
console.error("Error reproduciendo audio real:", e);
if (callback) callback(); // Continuar el flujo si falla
});
}
}
const audio = new AudioSystem();
// --- 3. GESTIÓN DEL ESTADO Y APP ---
class App {
constructor() {
this.state = this.loadState();
this.currentGame = 0;
this.currentQuestion = null;
this.gameOptions = [];
this.memoryState = { first: null, second: null, locked: false, matches: 0 };
this.teacherClicks = 0;
this.teacherTimeout = null;
this.cacheDOM();
this.bindEvents();
}
loadState() {
const defaultState = {
totalCorrect: 0,
streak: 0,
stats: {}
};
ANIMALS.forEach(a => defaultState.stats[a.id] = { correct: 0, mistakes: 0 });
const saved = localStorage.getItem('eduAnimalesState');
return saved ? JSON.parse(saved) : defaultState;
}
saveState() {
localStorage.setItem('eduAnimalesState', JSON.stringify(this.state));
}
cacheDOM() {
this.dom = {
unlock: document.getElementById('unlock-screen'),
btnStart: document.getElementById('btn-start'),
menu: document.getElementById('screen-menu'),
game: document.getElementById('screen-game'),
btnBack: document.getElementById('btn-back'),
btnSound: document.getElementById('btn-sound'),
gameBoard: document.getElementById('game-board'),
gameInst: document.getElementById('game-instruction'),
btnRepeat: document.getElementById('btn-repeat'),
postGameControls: document.getElementById('post-game-controls'),
btnRepeatExercise: document.getElementById('btn-repeat-exercise'),
btnNext: document.getElementById('btn-next'),
announcer: document.getElementById('aria-announcer'),
celebration: document.getElementById('celebration'),
btnTeacher: document.getElementById('btn-teacher'),
teacherPanel: document.getElementById('teacher-panel')
};
}
bindEvents() {
this.dom.btnStart.addEventListener('click', () => {
audio.init();
this.dom.unlock.style.display = 'none';
this.announce("Bienvenido a EduAnimales. Estás en el menú principal. Elige un juego.");
audio.speak("¡Bienvenido! Toca un botón para elegir un juego.");
});
this.dom.btnBack.addEventListener('click', () => {
audio.playSoundEffect('click');
this.showScreen('menu');
this.announce("Menú principal");
audio.speak("Menú principal");
});
this.dom.btnSound.addEventListener('click', () => {
const isEnabled = audio.toggle();
// Actualizar icono
this.dom.btnSound.innerHTML = isEnabled
? `<svg width="40" height="40" viewBox="0 0 24 24" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>`
: `<svg width="40" height="40" viewBox="0 0 24 24" fill="currentColor"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>`;
this.dom.btnSound.setAttribute('aria-label', isEnabled ? 'Desactivar sonido' : 'Activar sonido');
if(isEnabled) {
audio.playSoundEffect('click');
audio.speak("Sonido activado");
}
});
this.dom.btnRepeat.addEventListener('click', () => {
audio.playSoundEffect('click');
if (this.currentGame === 1) {
// En el juego de adivinar, repetir significa volver a escuchar al animal
audio.playRealSound(this.currentQuestion.audioUrl);
this.announce("Reproduciendo sonido misterioso nuevamente");
} else {
this.playCurrentInstruction();
}
});
this.dom.btnRepeatExercise.addEventListener('click', () => {
audio.playSoundEffect('click');
this.startCurrentRound();
});
this.dom.btnNext.addEventListener('click', () => {
audio.playSoundEffect('click');
this.nextRound();
});
// Lógica panel oculto profesor
this.dom.btnTeacher.addEventListener('click', () => {
this.teacherClicks++;
clearTimeout(this.teacherTimeout);
if (this.teacherClicks >= 3) {
this.teacherClicks = 0;
this.showTeacherPanel();
} else {
this.teacherTimeout = setTimeout(() => { this.teacherClicks = 0; }, 1000);
}
});
}
// Escribe en la región ARIA Live para ser leido por el lector de pantallas
announce(message) {
this.dom.announcer.textContent = '';
setTimeout(() => { this.dom.announcer.textContent = message; }, 50);
}
showScreen(screenName) {
this.dom.menu.classList.remove('active');
this.dom.game.classList.remove('active');
this.dom.teacherPanel.style.display = 'none';
if (screenName === 'menu') {
this.dom.menu.classList.add('active');
this.dom.btnBack.style.display = 'none';
this.currentGame = 0;
} else if (screenName === 'game') {
this.dom.game.classList.add('active');
this.dom.btnBack.style.display = 'block';
}
}
startGame(gameType) {
audio.playSoundEffect('click');
this.currentGame = gameType;
this.showScreen('game');
this.nextRound();
}
nextRound() {
if (this.currentGame === 1 || this.currentGame === 2) {
// Seleccionar un animal aleatorio como objetivo
this.currentQuestion = ANIMALS[Math.floor(Math.random() * ANIMALS.length)];
// Generar opciones (2 a 4 dependiendo del juego, fijamos en 3 para simplicidad y accesibilidad táctil)
let numOpciones = 3;
this.gameOptions = [this.currentQuestion];
while(this.gameOptions.length < numOpciones) {
const randomAnimal = ANIMALS[Math.floor(Math.random() * ANIMALS.length)];
if (!this.gameOptions.find(a => a.id === randomAnimal.id)) {
this.gameOptions.push(randomAnimal);
}
}
// Mezclar opciones
this.gameOptions.sort(() => Math.random() - 0.5);
} else if (this.currentGame === 3) {
// Para 4 cartas, necesitamos 2 parejas
const animal1 = ANIMALS[Math.floor(Math.random() * ANIMALS.length)];
let animal2 = ANIMALS[Math.floor(Math.random() * ANIMALS.length)];
while(animal2.id === animal1.id) animal2 = ANIMALS[Math.floor(Math.random() * ANIMALS.length)];
this.memoryCards = [animal1, animal1, animal2, animal2];
this.memoryCards.sort(() => Math.random() - 0.5);
}
this.startCurrentRound();
}
startCurrentRound() {
this.dom.postGameControls.style.display = 'none';
this.dom.btnRepeat.style.display = 'flex';
this.dom.gameBoard.style.display = 'grid';
this.dom.gameBoard.innerHTML = '';
this.renderGameUI();
this.playCurrentInstruction();
}
renderGameUI() {
if (this.currentGame === 1) {
this.dom.gameInst.textContent = "¿Qué animal hace este sonido?";
this.gameOptions.forEach(animal => {
const btn = this.createAnimalButton(animal, () => this.handleAnswer(animal));
this.dom.gameBoard.appendChild(btn);
});
}
else if (this.currentGame === 2) {
this.dom.gameInst.textContent = "Busca y toca al...";
this.gameOptions.forEach(animal => {
const btn = this.createAnimalButton(animal, () => this.handleAnswer(animal));
this.dom.gameBoard.appendChild(btn);
});
}
else if (this.currentGame === 3) {
// Juego de parejas adaptado (Audio Memory)
this.dom.gameInst.textContent = "Encuentra las parejas";
this.memoryState = { first: null, second: null, locked: false, matches: 0 };
this.memoryCards.forEach((animal, index) => {
const btn = document.createElement('button');
btn.className = 'animal-btn';
btn.setAttribute('aria-label', `Carta ${index + 1}`);
// Estado oculto (signo de interrogación gigante)
btn.innerHTML = `<span style="font-size: 5rem; color: #FFF;">?</span>`;
// Leer al pasar el ratón (si está volteada dice el animal, si no dice "Carta oculta")
btn.addEventListener('mouseenter', () => {
if (btn.classList.contains('matched') || btn === this.memoryState.first?.btn) {
this.announce(animal.name);
audio.speak(animal.name);
} else {
this.announce("Carta oculta");
audio.speak("Carta");
}
});
btn.addEventListener('mouseleave', () => {
audio.synth.cancel();
});
btn.onclick = () => this.handleMemoryClick(btn, animal, index);
this.dom.gameBoard.appendChild(btn);
});
}
}
createAnimalButton(animal, onClickCallback) {
const btn = document.createElement('button');
btn.className = 'animal-btn';
btn.style.backgroundColor = animal.color;
// Usar imagen real con fallback visual de alto contraste
const fallbackImg = `https://placehold.co/400x400/${animal.color.substring(1)}/000000?text=${animal.name}`;
btn.innerHTML = `<img src="${animal.imgUrl}" alt="${animal.name}" onerror="this.onerror=null;this.src='${fallbackImg}';" />`;
btn.setAttribute('aria-label', animal.name); // Vital para lector de pantallas
// Efecto extra: Al recibir foco por teclado o lector, anunciarlo
btn.addEventListener('focus', () => {
this.announce(animal.name);
audio.speak(animal.name);
});
// Efecto extra: Al pasar el ratón por encima, decir el nombre
btn.addEventListener('mouseenter', () => {
this.announce(animal.name);
audio.speak(animal.name);
});
// Detener el habla si se quita el ratón rápido
btn.addEventListener('mouseleave', () => {
audio.synth.cancel();
});
btn.onclick = () => {
// Prevenir doble clic rápido
btn.style.pointerEvents = 'none';
onClickCallback();
setTimeout(() => { btn.style.pointerEvents = 'auto'; }, 1000);
};
return btn;
}
playCurrentInstruction() {
if (this.currentGame === 1) {
this.announce(`¿Qué animal hace este sonido?`);
audio.speak("¿Qué animal hace este sonido?");
// Usamos setTimeout independiente para garantizar que el sonido
// del animal siempre se reproduzca, incluso si el alumno mueve
// rápidamente el ratón y cancela la instrucción de voz.
setTimeout(() => {
audio.playRealSound(this.currentQuestion.audioUrl);
}, 2200);
} else if (this.currentGame === 2) {
const text = `Busca y toca al ${this.currentQuestion.name}`;
this.announce(text);
audio.speak(text);
} else if (this.currentGame === 3) {
const text = "Toca las cartas para escuchar al animal y encontrar las parejas iguales.";
this.announce(text);
audio.speak(text);
}
}
handleAnswer(selectedAnimal) {
if (selectedAnimal.id === this.currentQuestion.id) {
// ACIERTO
audio.playSoundEffect('success');
this.state.stats[this.currentQuestion.id].correct++;
this.state.totalCorrect++;
this.state.streak++;
this.saveState();
const fraseAcierto = FRASES_ACIERTO[Math.floor(Math.random() * FRASES_ACIERTO.length)];
// Animación visual de acierto en el botón y oscurecer el resto
const btns = this.dom.gameBoard.children;
for (let btn of btns) {
if (btn.getAttribute('aria-label') === selectedAnimal.name) {
btn.classList.add('bounce');
} else {
btn.style.opacity = '0.3';
btn.style.pointerEvents = 'none';
}
}
if (this.state.streak >= 5) {
this.triggerCelebration();
} else {
const successText = `${fraseAcierto} Es el ${selectedAnimal.name}. ¿Quieres jugar otro?`;
this.announce(successText);
audio.speak(successText); // Se quita el callback para evitar bloqueos del navegador
this.showNextButton(); // Mostrar los botones inmediatamente
}
} else {
// ERROR
audio.playSoundEffect('error');
this.state.stats[this.currentQuestion.id].mistakes++;
this.state.streak = 0; // Reset racha
this.saveState();
const errorText = `Uy, casi... Ese es el ${selectedAnimal.name}. Inténtalo de nuevo.`;
this.announce(errorText);
audio.speak(errorText);
}
}
handleMemoryClick(btn, animal, index) {
if (this.memoryState.locked || btn.classList.contains('matched') || btn === this.memoryState.first?.btn) return;
audio.playSoundEffect('click');
// Revelar carta
btn.style.backgroundColor = animal.color;
const fallbackImg = `https://placehold.co/400x400/${animal.color.substring(1)}/000000?text=${animal.name}`;
btn.innerHTML = `<img src="${animal.imgUrl}" alt="${animal.name}" onerror="this.onerror=null;this.src='${fallbackImg}';" />`;
// Anunciar el animal tocado de forma doble (nombre + sonido real)
const text = `${animal.name}.`;
this.announce(`Carta volteada: ${text}`);
audio.speak(text, false, () => {
audio.playRealSound(animal.audioUrl);
});
if (!this.memoryState.first) {
this.memoryState.first = { btn, animal, index };
} else {
this.memoryState.second = { btn, animal, index };
this.memoryState.locked = true;
// Comprobar coincidencia
if (this.memoryState.first.animal.id === this.memoryState.second.animal.id) {
// Pareja encontrada
setTimeout(() => {
audio.playSoundEffect('success');
audio.speak(`¡Pareja de ${animal.name} encontrada!`);
this.memoryState.first.btn.classList.add('matched', 'bounce');
this.memoryState.second.btn.classList.add('matched', 'bounce');
this.memoryState.first.btn.setAttribute('aria-label', `${animal.name} (encontrado)`);
this.memoryState.second.btn.setAttribute('aria-label', `${animal.name} (encontrado)`);
this.state.totalCorrect++;
this.state.streak++;
this.memoryState.matches++;
this.saveState();
this.resetMemoryTurn();
// Fin del juego de parejas
if (this.memoryState.matches === 2) {
if (this.state.streak >= 5) {
setTimeout(() => this.triggerCelebration(), 1500);
} else {
const fraseAcierto = FRASES_ACIERTO[Math.floor(Math.random() * FRASES_ACIERTO.length)];
setTimeout(() => {
audio.speak(`${fraseAcierto} ¡Completaste las parejas! ¿Quieres jugar otro?`);
this.showNextButton(); // Mostrar los botones inmediatamente
}, 2000);
}
}
}, 1500); // Esperar a que hable
} else {
// No coinciden
setTimeout(() => {
audio.playSoundEffect('error');
// Ocultar de nuevo
this.memoryState.first.btn.style.backgroundColor = 'var(--card-bg)';
this.memoryState.first.btn.innerHTML = `<span style="font-size: 5rem; color: #FFF;">?</span>`;
this.memoryState.second.btn.style.backgroundColor = 'var(--card-bg)';
this.memoryState.second.btn.innerHTML = `<span style="font-size: 5rem; color: #FFF;">?</span>`;
this.resetMemoryTurn();
}, 2500); // Dar tiempo a ver y escuchar antes de girar
}
}
}
resetMemoryTurn() {
this.memoryState.first = null;
this.memoryState.second = null;
this.memoryState.locked = false;
}
showNextButton() {
this.dom.gameBoard.style.display = 'none';
this.dom.btnRepeat.style.display = 'none';
this.dom.gameInst.textContent = "¡Reto superado!";
this.dom.postGameControls.style.display = 'flex';
this.dom.btnNext.focus(); // Vital para accesibilidad
this.announce("Reto superado. Puedes elegir Siguiente para un juego nuevo, o Repetir Ejercicio para jugar el mismo.");
}
triggerCelebration() {
this.state.streak = 0; // Reset para la siguiente meta
this.saveState();
audio.playSoundEffect('celebration');
const frase = FRASES_REFUERZO[Math.floor(Math.random() * FRASES_REFUERZO.length)];
this.dom.celebration.querySelector('#celebration-text').textContent = frase;
this.dom.celebration.style.display = 'flex';
this.dom.celebration.setAttribute('aria-hidden', 'false');
this.announce(`¡Celebración! ${frase}`);
audio.speak(frase);
// Crear confetti visual accesible (estrellas animadas)
const stars = [];
for(let i=0; i<10; i++) {
const star = document.createElement('div');
star.className = 'star';
star.textContent = '⭐';
star.style.left = Math.random() * 90 + 'vw';
star.style.top = Math.random() * 90 + 'vh';
star.style.animationDelay = (Math.random() * 0.5) + 's';
this.dom.celebration.appendChild(star);
stars.push(star);
}
// Ocultar después de 4 segundos y avanzar
setTimeout(() => {
this.dom.celebration.style.display = 'none';
this.dom.celebration.setAttribute('aria-hidden', 'true');
stars.forEach(s => s.remove());
this.showNextButton();
}, 4000);
}
showTeacherPanel() {
audio.playSoundEffect('click');
this.dom.teacherPanel.style.display = 'flex';
this.announce("Panel de profesores abierto");
document.getElementById('stat-total').textContent = this.state.totalCorrect;
const container = document.getElementById('stats-container');
container.innerHTML = '';
ANIMALS.forEach(animal => {
const stat = this.state.stats[animal.id];
const card = document.createElement('div');
card.className = 'stat-card';
card.innerHTML = `
<strong>${animal.name}</strong><br>
<span style="color: var(--success)">Aciertos: ${stat.correct}</span><br>
<span style="color: var(--error)">Errores: ${stat.mistakes}</span>
`;
container.appendChild(card);
});
}
hideTeacherPanel() {
audio.playSoundEffect('click');
this.dom.teacherPanel.style.display = 'none';
this.announce("Panel de profesores cerrado");
}
resetStats() {
if(confirm("¿Estás seguro de borrar todos los progresos del alumno? Esta acción no se puede deshacer.")) {
localStorage.removeItem('eduAnimalesState');
this.state = this.loadState();
this.showTeacherPanel(); // Refrescar vista
audio.playSoundEffect('click');
}
}
}
// Inicializar app cuando el DOM esté listo
let app;
document.addEventListener('DOMContentLoaded', () => {
app = new App();
});
</script>
</body>
</html>