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>¡Aprende los Números!</title>
<style>
/* ==========================================================================
VARIABLES Y CONFIGURACIÓN BASE
========================================================================== */
:root {
--primary: #FF6B6B;
--primary-dark: #E85A5A;
--secondary: #4ECDC4;
--secondary-dark: #3DBDB4;
--accent: #FFE66D;
--accent-dark: #F2D95C;
--success: #95E1D3;
--success-dark: #84CFC1;
--bg-color: #F8F9FA;
--text-dark: #2D3436;
--text-light: #FFFFFF;
--shadow: 0 8px 0 rgba(0,0,0,0.15);
--shadow-hover: 0 4px 0 rgba(0,0,0,0.15);
--border-radius: 25px;
--font-main: 'Nunito', 'Segoe UI Rounded', 'Comic Sans MS', system-ui, sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
body {
font-family: var(--font-main);
background-color: var(--bg-color);
color: var(--text-dark);
height: 100vh;
width: 100vw;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
touch-action: none;
background-image: radial-gradient(circle at 50% 50%, #ffffff 0%, #e0e5ec 100%);
}
#app {
width: 100%;
height: 100%;
max-width: 1024px;
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
background: rgba(255, 255, 255, 0.4);
box-shadow: 0 0 40px rgba(0,0,0,0.05);
}
/* ==========================================================================
COMPONENTES UI (BOTONES, TARJETAS, ETC)
========================================================================== */
.btn {
font-family: var(--font-main);
font-size: 1.5rem;
font-weight: 900;
padding: 15px 30px;
border: none;
border-radius: var(--border-radius);
cursor: pointer;
transition: all 0.1s ease;
box-shadow: var(--shadow);
color: var(--text-light);
text-transform: uppercase;
letter-spacing: 2px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
outline: none;
}
.btn:active {
transform: translateY(4px);
box-shadow: var(--shadow-hover);
}
.btn-primary { background-color: var(--primary); box-shadow: 0 8px 0 var(--primary-dark); }
.btn-primary:active { box-shadow: 0 4px 0 var(--primary-dark); }
.btn-secondary { background-color: var(--secondary); box-shadow: 0 8px 0 var(--secondary-dark); }
.btn-secondary:active { box-shadow: 0 4px 0 var(--secondary-dark); }
.btn-accent { background-color: var(--accent); color: var(--text-dark); box-shadow: 0 8px 0 var(--accent-dark); }
.btn-accent:active { box-shadow: 0 4px 0 var(--accent-dark); }
.btn-huge {
font-size: 3rem;
width: 100px;
height: 100px;
border-radius: 30px;
}
/* ==========================================================================
PANTALLAS (VISTAS)
========================================================================== */
.screen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: none;
flex-direction: column;
animation: fadeIn 0.5s ease;
}
.screen.active {
display: flex;
}
/* --- Menú Principal --- */
#menu-screen {
justify-content: flex-start;
align-items: center;
padding: 60px 20px 20px 20px;
text-align: center;
overflow-y: auto; /* Permite scroll si la pantalla es muy pequeña */
-webkit-overflow-scrolling: touch;
}
.hero-title {
font-size: 3.5rem;
color: var(--primary);
text-shadow: 3px 3px 0 var(--accent), 6px 6px 0px rgba(0,0,0,0.1);
margin-bottom: 5px;
animation: bounceIn 1s cubic-bezier(0.175, 0.885, 0.32, 1.275);
flex-shrink: 0;
}
.mascot {
font-size: 4rem;
margin-bottom: 20px;
animation: float 3s ease-in-out infinite;
flex-shrink: 0;
}
.section-title {
font-size: 1.5rem;
font-weight: 900;
color: var(--text-dark);
margin-bottom: 15px;
width: 100%;
max-width: 800px;
text-align: left;
padding-left: 10px;
}
.level-selector {
display: flex;
gap: 15px;
margin-bottom: 30px;
flex-wrap: wrap;
justify-content: center;
width: 100%;
max-width: 800px;
flex-shrink: 0;
}
.level-btn {
font-size: 1.2rem;
padding: 12px 20px;
border-radius: 20px;
background: white;
color: var(--text-dark);
border: 4px solid #eee;
cursor: pointer;
transition: all 0.2s;
font-weight: bold;
flex: 1;
min-width: 120px;
}
.level-btn.selected {
border-color: var(--secondary);
background: var(--secondary);
color: white;
transform: scale(1.05);
box-shadow: 0 5px 15px rgba(78, 205, 196, 0.4);
}
.game-selector {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 15px;
width: 100%;
max-width: 800px;
margin-bottom: 40px;
flex-shrink: 0;
}
.game-btn {
font-size: 1.1rem;
padding: 15px 10px;
border-radius: 20px;
background: white;
color: var(--text-dark);
border: 4px solid #eee;
cursor: pointer;
transition: all 0.2s;
font-weight: bold;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.game-btn span.icon {
font-size: 2.5rem;
}
.game-btn.selected {
border-color: var(--accent-dark);
background: var(--accent);
color: var(--text-dark);
transform: scale(1.05);
box-shadow: 0 5px 15px rgba(242, 217, 92, 0.4);
}
.controls {
position: absolute;
top: 20px;
right: 20px;
display: flex;
gap: 15px;
z-index: 10;
}
.icon-btn {
width: 50px;
height: 50px;
border-radius: 50%;
border: none;
background: white;
box-shadow: var(--shadow);
font-size: 1.5rem;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
transition: transform 0.1s;
}
.icon-btn:active { transform: translateY(4px); box-shadow: var(--shadow-hover); }
.btn-play-container {
position: sticky;
bottom: 20px;
width: 100%;
display: flex;
justify-content: center;
pointer-events: none; /* Para no bloquear clics si flota */
}
.btn-play-container .btn {
pointer-events: auto; /* Reactivar clics en el botón */
font-size: 2rem;
padding: 20px 50px;
border-radius: 40px;
animation: pulse 2s infinite;
}
/* --- Pantalla de Juego --- */
#game-screen {
background-color: transparent;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background: rgba(255,255,255,0.9);
border-bottom: 4px solid #eee;
z-index: 10;
}
.stats {
display: flex;
gap: 20px;
font-size: 1.5rem;
font-weight: bold;
color: var(--text-dark);
align-items: center;
}
.stat-badge {
background: var(--accent);
padding: 8px 20px;
border-radius: 20px;
display: flex;
align-items: center;
gap: 10px;
box-shadow: 0 4px 0 var(--accent-dark);
}
.game-container {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 20px;
position: relative;
}
.instruction {
font-size: 2rem;
color: var(--text-dark);
text-align: center;
margin-bottom: 20px;
font-weight: 900;
display: flex;
align-items: center;
gap: 15px;
background: white;
padding: 15px 30px;
border-radius: 40px;
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
}
.speaker-icon {
cursor: pointer;
transition: transform 0.2s;
font-size: 1.5rem;
}
.speaker-icon:active { transform: scale(0.9); }
.play-area {
width: 100%;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 20px;
}
.options-area {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 20px;
flex-wrap: wrap;
}
/* --- Pantalla de Refuerzo --- */
#reinforcement-screen {
background: rgba(255, 255, 255, 0.95);
z-index: 100;
justify-content: center;
align-items: center;
text-align: center;
}
.msg-title {
font-size: 4rem;
color: var(--primary);
margin-bottom: 20px;
animation: popIn 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.msg-stars {
font-size: 5rem;
animation: pulse 1s infinite;
}
/* ==========================================================================
ESTILOS ESPECÍFICOS DE LOS JUEGOS
========================================================================== */
.emoji-grid {
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: center;
max-width: 600px;
}
.count-emoji {
font-size: 4rem;
animation: popIn 0.3s ease-out backwards;
}
.match-columns {
display: flex;
width: 100%;
justify-content: space-around;
align-items: center;
}
.match-col {
display: flex;
flex-direction: column;
gap: 20px;
}
.match-item {
width: 100px;
height: 100px;
background: white;
border-radius: 20px;
display: flex;
justify-content: center;
align-items: center;
font-size: 3rem;
font-weight: bold;
box-shadow: 0 6px 0 #ddd;
cursor: pointer;
transition: all 0.2s;
border: 4px solid transparent;
}
.match-item.selected {
border-color: var(--secondary);
transform: scale(1.1);
box-shadow: 0 0 20px var(--secondary);
}
.match-item.matched {
opacity: 0;
pointer-events: none;
transform: scale(0);
}
.sort-slots {
display: flex;
gap: 10px;
margin-bottom: 30px;
flex-wrap: wrap;
justify-content: center;
}
.sort-slot {
width: 80px;
height: 80px;
border: 4px dashed #ccc;
border-radius: 20px;
display: flex;
justify-content: center;
align-items: center;
font-size: 2.5rem;
font-weight: bold;
color: var(--secondary);
}
.sort-items {
display: flex;
gap: 15px;
flex-wrap: wrap;
justify-content: center;
}
.sequence-box {
display: flex;
gap: 15px;
align-items: center;
background: white;
padding: 20px 40px;
border-radius: 30px;
box-shadow: var(--shadow);
font-size: 4rem;
font-weight: bold;
}
.missing-slot {
width: 80px;
height: 80px;
border-bottom: 6px solid var(--primary);
color: var(--primary);
display: flex;
justify-content: center;
align-items: center;
}
.balloon-container {
width: 100%;
height: 60vh;
position: relative;
overflow: hidden;
}
.balloon {
position: absolute;
width: 100px;
height: 120px;
background: var(--primary);
border-radius: 50% 50% 50% 50% / 40% 40% 60% 60%;
display: flex;
justify-content: center;
align-items: center;
font-size: 3rem;
font-weight: bold;
color: white;
cursor: pointer;
box-shadow: inset -10px -10px 0 rgba(0,0,0,0.1);
animation: floatUp linear infinite;
}
.balloon::after {
content: '';
position: absolute;
bottom: -20px;
left: 50%;
width: 2px;
height: 40px;
background: #999;
}
.memory-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
max-width: 500px;
}
.memory-card {
width: 100px;
height: 100px;
perspective: 1000px;
cursor: pointer;
}
.memory-card-inner {
width: 100%;
height: 100%;
transition: transform 0.5s;
transform-style: preserve-3d;
position: relative;
}
.memory-card.flipped .memory-card-inner {
transform: rotateY(180deg);
}
.memory-card-front, .memory-card-back {
width: 100%;
height: 100%;
position: absolute;
backface-visibility: hidden;
border-radius: 20px;
display: flex;
justify-content: center;
align-items: center;
font-size: 3rem;
font-weight: bold;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.memory-card-front {
background: var(--secondary);
color: white;
font-size: 4rem;
}
.memory-card-back {
background: white;
transform: rotateY(180deg);
border: 4px solid var(--secondary);
}
.roulette-display {
font-size: 8rem;
font-weight: 900;
color: var(--primary);
background: white;
width: 200px;
height: 200px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 10px 30px rgba(0,0,0,0.1), inset 0 0 20px rgba(0,0,0,0.05);
border: 10px solid var(--accent);
margin-bottom: 30px;
}
.spinning { animation: shake 0.1s infinite; }
/* ==========================================================================
ANIMACIONES
========================================================================== */
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes popIn { 0% { transform: scale(0); opacity: 0; } 80% { transform: scale(1.1); opacity: 1; } 100% { transform: scale(1); opacity: 1; } }
@keyframes bounceIn { 0% { transform: scale(0.1); opacity: 0; } 60% { transform: scale(1.1); opacity: 1; } 100% { transform: scale(1); opacity: 1; } }
@keyframes float { 0% { transform: translateY(0px); } 50% { transform: translateY(-15px); } 100% { transform: translateY(0px); } }
@keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } }
@keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-10px) rotate(-5deg); } 75% { transform: translateX(10px) rotate(5deg); } }
@keyframes floatUp { from { transform: translateY(100vh); } to { transform: translateY(-300px); } }
.wrong-shake { animation: shake 0.4s ease-in-out; }
.correct-pop { animation: popIn 0.5s ease-out; }
.confetti {
position: absolute;
width: 15px;
height: 15px;
z-index: 1000;
pointer-events: none;
animation: fall linear forwards;
}
@keyframes fall { to { transform: translateY(100vh) rotate(720deg); } }
/* ==========================================================================
MEDIA QUERIES (RESPONSIVE)
========================================================================== */
@media (max-width: 768px) {
.hero-title { font-size: 2.5rem; }
.mascot { font-size: 3rem; margin-bottom: 10px; }
.section-title { font-size: 1.2rem; text-align: center; }
.game-selector { grid-template-columns: repeat(2, 1fr); margin-bottom: 80px; }
.game-btn { padding: 10px; font-size: 1rem; }
.game-btn span.icon { font-size: 2rem; }
.btn { font-size: 1.2rem; padding: 12px 24px; }
.btn-huge { width: 80px; height: 80px; font-size: 2.5rem; }
.instruction { font-size: 1.5rem; padding: 10px 20px; flex-direction: column; gap: 5px; text-align: center; }
.count-emoji { font-size: 3rem; }
.match-item { width: 80px; height: 80px; font-size: 2rem; }
.sequence-box { font-size: 2.5rem; padding: 15px 20px; }
.missing-slot { width: 50px; height: 50px; }
.roulette-display { width: 150px; height: 150px; font-size: 6rem; border-width: 6px; }
.memory-grid { gap: 10px; }
.memory-card { width: 80px; height: 80px; }
.memory-card-front, .memory-card-back { font-size: 2.5rem; }
.top-bar { padding: 10px; }
.icon-btn { width: 40px; height: 40px; font-size: 1.2rem; }
}
@media (max-height: 500px) and (orientation: landscape) {
#menu-screen { padding-top: 20px; }
.hero-title { font-size: 2rem; }
.mascot { display: none; }
.btn-play-container { position: relative; margin-top: 20px; margin-bottom: 20px;}
.game-selector { grid-template-columns: repeat(4, 1fr); margin-bottom: 20px; }
.instruction { font-size: 1.2rem; margin-bottom: 10px; padding: 5px 20px; flex-direction: row;}
.game-container { padding: 10px; }
.balloon-container { height: 50vh; }
}
</style>
</head>
<body>
<div id="app">
<!-- PANTALLA: MENÚ PRINCIPAL -->
<div id="menu-screen" class="screen active">
<div class="controls">
<button class="icon-btn" id="btn-toggle-sound" aria-label="Alternar sonido">🔊</button>
<button class="icon-btn" id="btn-reset" aria-label="Reiniciar progreso">🔄</button>
</div>
<h1 class="hero-title">¡Aprende los Números!</h1>
<div class="mascot">🌟</div>
<div class="section-title">1. Elige la dificultad:</div>
<div class="level-selector" id="level-selector">
<button class="level-btn selected" data-level="1">Nivel 1<br><small>(Fácil)</small></button>
<button class="level-btn" data-level="2">Nivel 2<br><small>(Medio)</small></button>
<button class="level-btn" data-level="3">Nivel 3<br><small>(Experto)</small></button>
</div>
<div class="section-title">2. Elige a qué jugar:</div>
<div class="game-selector" id="game-selector">
<button class="game-btn selected" data-game="random">
<span class="icon">🎲</span> Sorpresa
</button>
<button class="game-btn" data-game="1">
<span class="icon">🍎</span> Contar
</button>
<button class="game-btn" data-game="2">
<span class="icon">🃏</span> Relacionar
</button>
<button class="game-btn" data-game="3">
<span class="icon">👆</span> Arrastrar
</button>
<button class="game-btn" data-game="4">
<span class="icon">🔢</span> Ordenar
</button>
<button class="game-btn" data-game="5">
<span class="icon">❓</span> Faltante
</button>
<button class="game-btn" data-game="6">
<span class="icon">⬆️</span> Mayor
</button>
<button class="game-btn" data-game="7">
<span class="icon">⬇️</span> Menor
</button>
<button class="game-btn" data-game="8">
<span class="icon">🎈</span> Globos
</button>
<button class="game-btn" data-game="9">
<span class="icon">🧠</span> Memoria
</button>
<button class="game-btn" data-game="10">
<span class="icon">🎡</span> Ruleta
</button>
</div>
<div class="btn-play-container">
<button class="btn btn-primary" id="btn-start">
¡JUGAR! ▶
</button>
</div>
</div>
<!-- PANTALLA: JUEGO -->
<div id="game-screen" class="screen">
<div class="top-bar">
<button class="icon-btn" id="btn-home" aria-label="Volver al inicio">🏠</button>
<div class="stats">
<div class="stat-badge">⭐ <span id="ui-score">0</span></div>
</div>
</div>
<div class="game-container">
<div class="instruction" id="game-instruction">
<span id="instruction-text">Instrucción aquí</span>
<span class="speaker-icon" id="btn-speak" title="Escuchar">🗣️</span>
</div>
<div class="play-area" id="play-area">
<!-- Contenido dinámico del juego -->
</div>
<div class="options-area" id="options-area">
<!-- Botones de respuesta -->
</div>
</div>
</div>
<!-- PANTALLA: REFUERZO POSITIVO -->
<div id="reinforcement-screen" class="screen">
<h2 class="msg-title" id="reinforcement-msg">¡Fantástico!</h2>
<div class="msg-stars">⭐⭐⭐</div>
</div>
</div>
<script>
/* ==========================================================================
ESTADO DE LA APLICACIÓN Y PERSISTENCIA
========================================================================== */
const state = {
level: 1,
selectedGameType: 'random', // 'random' o un número del 1 al 10
score: 0,
consecutiveHits: 0,
soundEnabled: true,
currentGameId: 0,
locked: false
};
// Emojis amigables para usar en los juegos
const emojis = ['🍎', '🐶', '🚗', '🎈', '⭐', '🍓', '🐸', '🚀', '⚽', '🦋', '🧸', '🍭'];
/* ==========================================================================
SISTEMA DE AUDIO
========================================================================== */
const AudioSys = {
ctx: null,
init: function() {
if (!this.ctx) this.ctx = new (window.AudioContext || window.webkitAudioContext)();
if (this.ctx.state === 'suspended') this.ctx.resume();
},
playTone: function(freq, type, duration, vol=0.1) {
if (!state.soundEnabled || !this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
gain.gain.setValueAtTime(vol, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + duration);
},
success: function() {
if (!state.soundEnabled) return;
this.init();
setTimeout(() => this.playTone(523.25, 'sine', 0.1), 0);
setTimeout(() => this.playTone(659.25, 'sine', 0.1), 100);
setTimeout(() => this.playTone(783.99, 'sine', 0.2), 200);
setTimeout(() => this.playTone(1046.50, 'sine', 0.4), 300);
},
error: function() {
if (!state.soundEnabled) return;
this.init();
this.playTone(300, 'triangle', 0.2, 0.2);
setTimeout(() => this.playTone(250, 'triangle', 0.3, 0.2), 150);
},
pop: function() {
if (!state.soundEnabled) return;
this.init();
this.playTone(800, 'sine', 0.1, 0.05);
},
celebrate: function() {
if (!state.soundEnabled) return;
this.init();
const notes = [523.25, 587.33, 659.25, 698.46, 783.99, 880.00, 987.77, 1046.50];
notes.forEach((n, i) => {
setTimeout(() => this.playTone(n, 'square', 0.1, 0.05), i * 100);
});
},
speak: function(text) {
if (!state.soundEnabled || !('speechSynthesis' in window)) return;
window.speechSynthesis.cancel();
const msg = new SpeechSynthesisUtterance(text);
msg.lang = 'es-ES';
msg.rate = 0.9;
msg.pitch = 1.2;
window.speechSynthesis.speak(msg);
}
};
/* ==========================================================================
UTILIDADES
========================================================================== */
const Utils = {
rand: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min,
shuffle: (array) => {
let currentIndex = array.length, randomIndex;
while (currentIndex !== 0) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
}
return array;
},
getRangeByLevel: () => {
if (state.level === 1) return { min: 1, max: 5 };
if (state.level === 2) return { min: 1, max: 10 };
return { min: 1, max: 10 }; // Nivel 3 hace variaciones más difíciles pero mismo rango
},
createConfetti: () => {
for (let i = 0; i < 50; i++) {
const conf = document.createElement('div');
conf.className = 'confetti';
conf.style.left = Math.random() * 100 + 'vw';
conf.style.backgroundColor = `hsl(${Math.random() * 360}, 100%, 50%)`;
conf.style.animationDuration = (Math.random() * 2 + 1) + 's';
conf.style.top = '-20px';
document.body.appendChild(conf);
setTimeout(() => conf.remove(), 3000);
}
}
};
/* ==========================================================================
LÓGICA PRINCIPAL DE LA APLICACIÓN
========================================================================== */
const App = {
init: function() {
this.bindEvents();
this.updateUI();
},
bindEvents: function() {
// Selector de Nivel
document.querySelectorAll('.level-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
AudioSys.pop();
document.querySelectorAll('.level-btn').forEach(b => b.classList.remove('selected'));
e.currentTarget.classList.add('selected');
state.level = parseInt(e.currentTarget.dataset.level);
});
});
// Selector de Juego
document.querySelectorAll('.game-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
AudioSys.pop();
document.querySelectorAll('.game-btn').forEach(b => b.classList.remove('selected'));
e.currentTarget.classList.add('selected');
const selected = e.currentTarget.dataset.game;
state.selectedGameType = selected === 'random' ? 'random' : parseInt(selected);
});
});
// Botón Jugar
document.getElementById('btn-start').addEventListener('click', () => {
AudioSys.init();
AudioSys.pop();
this.showScreen('game-screen');
this.nextGame();
});
// Botón Inicio
document.getElementById('btn-home').addEventListener('click', () => {
AudioSys.pop();
this.showScreen('menu-screen');
window.speechSynthesis.cancel();
});
// Sonido y Reseteo
const toggleSound = () => {
state.soundEnabled = !state.soundEnabled;
const icon = state.soundEnabled ? '🔊' : '🔇';
document.getElementById('btn-toggle-sound').textContent = icon;
if (state.soundEnabled) AudioSys.init();
else window.speechSynthesis.cancel();
};
document.getElementById('btn-toggle-sound').addEventListener('click', toggleSound);
document.getElementById('btn-reset').addEventListener('click', () => {
state.score = 0;
state.consecutiveHits = 0;
this.updateUI();
AudioSys.pop();
// Animación visual de reseteo en el botón
const btn = document.getElementById('btn-reset');
btn.style.transform = 'rotate(360deg)';
setTimeout(() => btn.style.transform = '', 300);
});
// Altavoz manual
document.getElementById('btn-speak').addEventListener('click', () => {
const text = document.getElementById('instruction-text').textContent;
AudioSys.speak(text);
});
},
showScreen: function(screenId) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(screenId).classList.add('active');
// Si volvemos al menú, asegurarse de hacer scroll arriba
if(screenId === 'menu-screen') {
document.getElementById('menu-screen').scrollTop = 0;
}
},
updateUI: function() {
document.getElementById('ui-score').textContent = state.score;
},
setInstruction: function(text) {
document.getElementById('instruction-text').textContent = text;
AudioSys.speak(text);
},
handleCorrect: function(element) {
if (state.locked) return;
state.locked = true;
AudioSys.success();
state.score += 10;
state.consecutiveHits++;
this.updateUI();
if (element) {
element.classList.add('correct-pop');
element.style.backgroundColor = 'var(--success)';
element.style.color = 'white';
}
setTimeout(() => {
if (state.consecutiveHits > 0 && state.consecutiveHits % 5 === 0) {
this.showReinforcement();
} else {
this.nextGame();
}
}, 1000);
},
handleWrong: function(element) {
if (state.locked) return;
AudioSys.error();
state.consecutiveHits = 0;
if (element) {
element.classList.add('wrong-shake');
element.style.opacity = '0.5';
setTimeout(() => { element.classList.remove('wrong-shake'); }, 400);
}
},
showReinforcement: function() {
const msgs = ["¡Fantástico!", "¡Lo estás haciendo genial!", "¡Eres un campeón!", "¡Excelente trabajo!", "¡Sigue así!", "¡Qué bien cuentas!", "¡Increíble!"];
document.getElementById('reinforcement-msg').textContent = msgs[Utils.rand(0, msgs.length - 1)];
this.showScreen('reinforcement-screen');
AudioSys.celebrate();
Utils.createConfetti();
setTimeout(() => {
this.showScreen('game-screen');
this.nextGame();
}, 3000);
},
nextGame: function() {
state.locked = false;
const playArea = document.getElementById('play-area');
const optsArea = document.getElementById('options-area');
playArea.innerHTML = '';
optsArea.innerHTML = '';
let nextId;
// Si el usuario eligió un juego específico, jugamos a ese
if (state.selectedGameType !== 'random') {
nextId = state.selectedGameType;
} else {
// Modo aleatorio: elegimos según el nivel pero evitamos repetir el mismo inmediatamente
let availableGames = [];
if (state.level === 1) availableGames = [1, 2, 3, 6, 8, 9];
if (state.level === 2) availableGames = [1, 2, 3, 4, 6, 7, 8, 9, 10];
if (state.level === 3) availableGames = [4, 5, 6, 7, 8, 9, 10];
nextId = state.currentGameId;
while (nextId === state.currentGameId && availableGames.length > 1) {
nextId = availableGames[Utils.rand(0, availableGames.length - 1)];
}
}
state.currentGameId = nextId;
switch (state.currentGameId) {
case 1: Games.countObjects(); break;
case 2: Games.matchNumber(); break;
case 3: Games.dragMatch(); break;
case 4: Games.orderNumbers(); break;
case 5: Games.missingNumber(); break;
case 6: Games.compareNumbers(true); break;
case 7: Games.compareNumbers(false); break;
case 8: Games.popBalloon(); break;
case 9: Games.memoryCard(); break;
case 10: Games.roulette(); break;
default: Games.countObjects();
}
}
};
/* ==========================================================================
MÓDULO DE JUEGOS
========================================================================== */
const Games = {
getContainers: () => ({
play: document.getElementById('play-area'),
opts: document.getElementById('options-area')
}),
generateOptions: (correct, rangeMin, rangeMax, count = 3) => {
let opts = [correct];
while(opts.length < count) {
let n = Utils.rand(rangeMin, rangeMax);
if (!opts.includes(n)) opts.push(n);
}
return Utils.shuffle(opts);
},
// 1. Contar
countObjects: function() {
const { play, opts } = this.getContainers();
const range = Utils.getRangeByLevel();
const target = Utils.rand(range.min, range.max);
const emoji = emojis[Utils.rand(0, emojis.length - 1)];
App.setInstruction("¿Cuántos hay?");
const grid = document.createElement('div');
grid.className = 'emoji-grid';
for(let i=0; i<target; i++) {
const el = document.createElement('div');
el.className = 'count-emoji';
el.textContent = emoji;
el.style.animationDelay = `${i * 0.05}s`;
grid.appendChild(el);
}
play.appendChild(grid);
const options = this.generateOptions(target, range.min, range.max, state.level === 3 ? 4 : 3);
options.forEach(num => {
const btn = document.createElement('button');
btn.className = 'btn btn-primary btn-huge';
btn.textContent = num;
btn.onclick = () => num === target ? App.handleCorrect(btn) : App.handleWrong(btn);
opts.appendChild(btn);
});
},
// 2. Relacionar
matchNumber: function() {
const { play, opts } = this.getContainers();
const range = Utils.getRangeByLevel();
const target = Utils.rand(range.min, range.max);
const emoji = emojis[Utils.rand(0, emojis.length - 1)];
App.setInstruction(`Encuentra la tarjeta con ${target}`);
const numDisplay = document.createElement('div');
numDisplay.style.fontSize = '8rem';
numDisplay.style.fontWeight = 'bold';
numDisplay.style.color = 'var(--primary)';
numDisplay.textContent = target;
play.appendChild(numDisplay);
const options = this.generateOptions(target, range.min, range.max, state.level === 3 ? 4 : 3);
options.forEach(num => {
const card = document.createElement('div');
card.className = 'btn btn-secondary';
card.style.flexDirection = 'row';
card.style.flexWrap = 'wrap';
card.style.width = '140px';
card.style.height = '140px';
card.style.gap = '5px';
card.style.padding = '10px';
card.style.alignContent = 'center';
for(let i=0; i<num; i++) {
const e = document.createElement('span');
e.textContent = emoji;
e.style.fontSize = num > 6 ? '1.5rem' : '2rem';
card.appendChild(e);
}
card.onclick = () => num === target ? App.handleCorrect(card) : App.handleWrong(card);
opts.appendChild(card);
});
},
// 3. Arrastrar (Tap to match)
dragMatch: function() {
const { play } = this.getContainers();
App.setInstruction("Toca un número y luego su cantidad");
const range = Utils.getRangeByLevel();
const maxLimit = state.level === 3 ? 10 : 6; // Nivel 3 usa hasta el 10
const numbers = [];
while(numbers.length < 3) {
let n = Utils.rand(range.min, Math.min(range.max, maxLimit));
if(!numbers.includes(n)) numbers.push(n);
}
const colNums = Utils.shuffle([...numbers]);
const colQtys = Utils.shuffle([...numbers]);
const emoji = emojis[Utils.rand(0, emojis.length - 1)];
const container = document.createElement('div');
container.className = 'match-columns';
const c1 = document.createElement('div'); c1.className = 'match-col';
const c2 = document.createElement('div'); c2.className = 'match-col';
let selectedNumEl = null;
let selectedQtyEl = null;
let pairsFound = 0;
const checkMatch = () => {
if(selectedNumEl && selectedQtyEl) {
if(selectedNumEl.dataset.val === selectedQtyEl.dataset.val) {
AudioSys.success();
selectedNumEl.classList.add('matched');
selectedQtyEl.classList.add('matched');
pairsFound++;
selectedNumEl = selectedQtyEl = null;
if(pairsFound === 3) setTimeout(() => App.handleCorrect(null), 500);
} else {
App.handleWrong(selectedNumEl);
App.handleWrong(selectedQtyEl);
selectedNumEl.classList.remove('selected');
selectedQtyEl.classList.remove('selected');
selectedNumEl = selectedQtyEl = null;
}
}
};
colNums.forEach(n => {
const el = document.createElement('div');
el.className = 'match-item num-item';
el.textContent = n;
el.dataset.val = n;
el.style.color = 'var(--primary)';
el.onclick = () => {
if(el.classList.contains('matched')) return;
AudioSys.pop();
document.querySelectorAll('.num-item').forEach(e=>e.classList.remove('selected'));
el.classList.add('selected');
selectedNumEl = el;
checkMatch();
};
c1.appendChild(el);
});
colQtys.forEach(n => {
const el = document.createElement('div');
el.className = 'match-item qty-item';
el.dataset.val = n;
el.style.flexWrap = 'wrap';
el.style.gap = '2px';
for(let i=0; i<n; i++) {
const s = document.createElement('span');
s.textContent = emoji;
s.style.fontSize = n > 5 ? '1.2rem' : '1.8rem';
el.appendChild(s);
}
el.onclick = () => {
if(el.classList.contains('matched')) return;
AudioSys.pop();
document.querySelectorAll('.qty-item').forEach(e=>e.classList.remove('selected'));
el.classList.add('selected');
selectedQtyEl = el;
checkMatch();
};
c2.appendChild(el);
});
container.appendChild(c1);
container.appendChild(c2);
play.appendChild(container);
},
// 4. Ordenar
orderNumbers: function() {
const { play } = this.getContainers();
// Si el usuario eligió este juego en Nivel 1 forzado, limitamos la cantidad
const max = state.level === 1 ? 3 : (state.level === 2 ? 5 : 7);
let start = state.level === 3 ? Utils.rand(1, 4) : 1;
App.setInstruction("Tócalos en orden");
const targetSequence = Array.from({length: max}, (_, i) => start + i);
const scrambled = Utils.shuffle([...targetSequence]);
let currentIndex = 0;
const slotsContainer = document.createElement('div');
slotsContainer.className = 'sort-slots';
targetSequence.forEach(() => {
const slot = document.createElement('div');
slot.className = 'sort-slot';
slotsContainer.appendChild(slot);
});
play.appendChild(slotsContainer);
const itemsContainer = document.createElement('div');
itemsContainer.className = 'sort-items';
scrambled.forEach(num => {
const btn = document.createElement('button');
btn.className = 'btn btn-accent btn-huge';
btn.textContent = num;
btn.onclick = () => {
if(num === targetSequence[currentIndex]) {
AudioSys.pop();
btn.style.visibility = 'hidden';
slotsContainer.children[currentIndex].textContent = num;
slotsContainer.children[currentIndex].style.background = 'var(--success)';
slotsContainer.children[currentIndex].style.color = 'white';
slotsContainer.children[currentIndex].style.borderStyle = 'solid';
currentIndex++;
if(currentIndex === targetSequence.length) {
setTimeout(() => App.handleCorrect(null), 300);
}
} else {
App.handleWrong(btn);
}
};
itemsContainer.appendChild(btn);
});
play.appendChild(itemsContainer);
},
// 5. Faltante
missingNumber: function() {
const { play, opts } = this.getContainers();
const start = Utils.rand(1, 7);
const sequence = [start, start+1, start+2, start+3];
const missingIndex = Utils.rand(0, 3);
const target = sequence[missingIndex];
App.setInstruction("¿Qué número falta?");
const seqBox = document.createElement('div');
seqBox.className = 'sequence-box';
sequence.forEach((num, i) => {
if(i === missingIndex) {
const slot = document.createElement('div');
slot.className = 'missing-slot';
slot.textContent = '?';
seqBox.appendChild(slot);
} else {
const span = document.createElement('span');
span.textContent = num;
seqBox.appendChild(span);
}
});
play.appendChild(seqBox);
const options = this.generateOptions(target, Math.max(1, start-2), start+5, 4);
options.forEach(num => {
const btn = document.createElement('button');
btn.className = 'btn btn-primary btn-huge';
btn.textContent = num;
btn.onclick = () => {
if(num === target) {
document.querySelector('.missing-slot').textContent = num;
App.handleCorrect(btn);
} else {
App.handleWrong(btn);
}
};
opts.appendChild(btn);
});
},
// 6 & 7. Comparar
compareNumbers: function(findMax) {
const { play } = this.getContainers();
const range = Utils.getRangeByLevel();
let num1 = Utils.rand(range.min, range.max);
let num2 = Utils.rand(range.min, range.max);
while(num1 === num2) num2 = Utils.rand(range.min, range.max);
App.setInstruction(findMax ? "Toca el número MAYOR" : "Toca el número MENOR");
const target = findMax ? Math.max(num1, num2) : Math.min(num1, num2);
const cont = document.createElement('div');
cont.style.display = 'flex';
cont.style.gap = '40px';
[num1, num2].forEach(num => {
const btn = document.createElement('button');
btn.className = 'btn btn-secondary';
btn.style.width = '150px';
btn.style.height = '150px';
btn.style.fontSize = '5rem';
btn.style.borderRadius = '30px';
btn.textContent = num;
btn.onclick = () => num === target ? App.handleCorrect(btn) : App.handleWrong(btn);
cont.appendChild(btn);
});
play.appendChild(cont);
},
// 8. Globos
popBalloon: function() {
const { play } = this.getContainers();
const range = Utils.getRangeByLevel();
const target = Utils.rand(range.min, range.max);
App.setInstruction(`¡Explota el globo número ${target}!`);
const container = document.createElement('div');
container.className = 'balloon-container';
const amount = state.level === 1 ? 4 : 6;
const numbers = this.generateOptions(target, range.min, range.max, amount);
numbers.forEach((num, i) => {
const balloon = document.createElement('div');
balloon.className = 'balloon';
balloon.textContent = num;
balloon.style.left = `${10 + (i * (80/amount))}%`;
balloon.style.animationDuration = `${Utils.rand(6, 12)}s`;
balloon.style.animationDelay = `${Utils.rand(0, 3)}s`;
balloon.style.backgroundColor = `hsl(${Utils.rand(0,360)}, 80%, 60%)`;
balloon.onmousedown = balloon.ontouchstart = (e) => {
e.preventDefault();
if(num === target) {
AudioSys.pop();
balloon.style.display = 'none';
App.handleCorrect(null);
} else {
App.handleWrong(balloon);
}
};
container.appendChild(balloon);
});
play.appendChild(container);
},
// 9. Memoria
memoryCard: function() {
const { play } = this.getContainers();
App.setInstruction("Encuentra las parejas");
const range = Utils.getRangeByLevel();
const amountPairs = state.level === 3 ? 4 : 3; // Nivel 3 tiene 8 cartas, los demás 6
let values = [];
while(values.length < amountPairs) {
let n = Utils.rand(range.min, Math.min(range.max, 8));
if(!values.includes(n)) values.push(n);
}
const emoji = emojis[Utils.rand(0, emojis.length - 1)];
let cardsData = [];
values.forEach(v => {
cardsData.push({ type: 'num', val: v });
cardsData.push({ type: 'qty', val: v });
});
cardsData = Utils.shuffle(cardsData);
const grid = document.createElement('div');
grid.className = 'memory-grid';
if (amountPairs === 4) grid.style.gridTemplateColumns = 'repeat(4, 1fr)';
let flippedCards = [];
let pairsFound = 0;
cardsData.forEach(data => {
const card = document.createElement('div');
card.className = 'memory-card';
const inner = document.createElement('div');
inner.className = 'memory-card-inner';
const front = document.createElement('div');
front.className = 'memory-card-front';
front.textContent = '?';
const back = document.createElement('div');
back.className = 'memory-card-back';
if(data.type === 'num') {
back.textContent = data.val;
back.style.color = 'var(--primary)';
} else {
back.style.flexWrap = 'wrap';
back.style.gap = '2px';
for(let i=0; i<data.val; i++) {
const s = document.createElement('span');
s.textContent = emoji;
s.style.fontSize = data.val > 4 ? '1.5rem' : '2rem';
back.appendChild(s);
}
}
inner.appendChild(front);
inner.appendChild(back);
card.appendChild(inner);
card.onclick = () => {
if(card.classList.contains('flipped') || flippedCards.length >= 2) return;
AudioSys.pop();
card.classList.add('flipped');
flippedCards.push({el: card, val: data.val});
if(flippedCards.length === 2) {
if(flippedCards[0].val === flippedCards[1].val) {
AudioSys.success();
pairsFound++;
flippedCards = [];
if(pairsFound === amountPairs) setTimeout(() => App.handleCorrect(null), 500);
} else {
setTimeout(() => {
App.handleWrong(null);
flippedCards[0].el.classList.remove('flipped');
flippedCards[1].el.classList.remove('flipped');
flippedCards = [];
}, 1000);
}
}
};
grid.appendChild(card);
});
play.appendChild(grid);
},
// 10. Ruleta
roulette: function() {
const { play, opts } = this.getContainers();
const range = Utils.getRangeByLevel();
const target = Utils.rand(range.min, range.max);
App.setInstruction("¡Mira la ruleta!");
const display = document.createElement('div');
display.className = 'roulette-display spinning';
play.appendChild(display);
let counter = 0;
let interval = setInterval(() => {
display.textContent = Utils.rand(range.min, range.max);
AudioSys.playTone(400 + (counter*10), 'triangle', 0.05, 0.05);
counter++;
if(counter > 20) {
clearInterval(interval);
display.classList.remove('spinning');
display.textContent = target;
display.style.transform = 'scale(1.2)';
display.style.transition = 'transform 0.3s';
AudioSys.success();
setTimeout(() => {
App.setInstruction(`¡Encuentra el número ${target}!`);
display.style.transform = 'scale(1)';
const options = this.generateOptions(target, range.min, range.max, state.level === 1 ? 3 : 4);
options.forEach(num => {
const btn = document.createElement('button');
btn.className = 'btn btn-primary btn-huge';
btn.textContent = num;
btn.onclick = () => num === target ? App.handleCorrect(btn) : App.handleWrong(btn);
opts.appendChild(btn);
});
}, 1000);
}
}, 100);
}
};
/* Inicializar */
document.addEventListener('DOMContentLoaded', () => {
App.init();
});
</script>
</body>
</html>